From 3b079526a2ea78b68555094ca498faea31022759 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Sun, 27 Nov 2022 17:05:31 -0700 Subject: [PATCH] User fetching and inbox message cleaning --- core/models/config.py | 1 + stator/graph.py | 6 ++- stator/models.py | 28 ++++++++++---- users/admin.py | 2 +- .../migrations/0003_identity_followers_etc.py | 38 +++++++++++++++++++ users/models/identity.py | 20 ++++++++-- users/models/inbox_message.py | 10 ++++- 7 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 users/migrations/0003_identity_followers_etc.py diff --git a/core/models/config.py b/core/models/config.py index 6c31658..dab0059 100644 --- a/core/models/config.py +++ b/core/models/config.py @@ -213,6 +213,7 @@ class Config(models.Model): identity_min_length: int = 2 identity_max_per_user: int = 5 identity_max_age: int = 24 * 60 * 60 + inbox_message_purge_after: int = 24 * 60 * 60 restricted_usernames: str = "admin\nadmins\nadministrator\nadministrators\nsystem\nroot\nannounce\nannouncement\nannouncements" diff --git a/stator/graph.py b/stator/graph.py index 424ea49..5c71d4a 100644 --- a/stator/graph.py +++ b/stator/graph.py @@ -87,10 +87,14 @@ class State: try_interval: Optional[float] = None, handler_name: Optional[str] = None, externally_progressed: bool = False, + attempt_immediately: bool = True, + force_initial: bool = False, ): self.try_interval = try_interval self.handler_name = handler_name self.externally_progressed = externally_progressed + self.attempt_immediately = attempt_immediately + self.force_initial = force_initial self.parents: Set["State"] = set() self.children: Set["State"] = set() @@ -121,7 +125,7 @@ class State: @property def initial(self): - return not self.parents + return self.force_initial or (not self.parents) @property def terminal(self): diff --git a/stator/models.py b/stator/models.py index bbff395..5257ac9 100644 --- a/stator/models.py +++ b/stator/models.py @@ -74,6 +74,10 @@ class StatorModel(models.Model): def state_graph(cls) -> Type[StateGraph]: return cls._meta.get_field("state").graph + @property + def state_age(self) -> int: + return (timezone.now() - self.state_changed).total_seconds() + @classmethod async def atransition_schedule_due(cls, now=None) -> models.QuerySet: """ @@ -184,13 +188,23 @@ class StatorModel(models.Model): state = state.name if state not in self.state_graph.states: raise ValueError(f"Invalid state {state}") - self.__class__.objects.filter(pk=self.pk).update( - state=state, - state_changed=timezone.now(), - state_attempted=None, - state_locked_until=None, - state_ready=True, - ) + # See if it's ready immediately (if not, delay until first try_interval) + if self.state_graph.states[state].attempt_immediately: + self.__class__.objects.filter(pk=self.pk).update( + state=state, + state_changed=timezone.now(), + state_attempted=None, + state_locked_until=None, + state_ready=True, + ) + else: + self.__class__.objects.filter(pk=self.pk).update( + state=state, + state_changed=timezone.now(), + state_attempted=timezone.now(), + state_locked_until=None, + state_ready=False, + ) atransition_perform = sync_to_async(transition_perform) diff --git a/users/admin.py b/users/admin.py index f0d484d..235b0db 100644 --- a/users/admin.py +++ b/users/admin.py @@ -65,7 +65,7 @@ class PasswordResetAdmin(admin.ModelAdmin): @admin.register(InboxMessage) class InboxMessageAdmin(admin.ModelAdmin): - list_display = ["id", "state", "state_attempted", "message_type", "message_actor"] + list_display = ["id", "state", "state_changed", "message_type", "message_actor"] search_fields = ["message"] actions = ["reset_state"] readonly_fields = ["state_changed"] diff --git a/users/migrations/0003_identity_followers_etc.py b/users/migrations/0003_identity_followers_etc.py new file mode 100644 index 0000000..ffb6272 --- /dev/null +++ b/users/migrations/0003_identity_followers_etc.py @@ -0,0 +1,38 @@ +# Generated by Django 4.1.3 on 2022-11-27 22:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0002_identity_discoverable"), + ] + + operations = [ + migrations.AddField( + model_name="identity", + name="followers_uri", + field=models.CharField(blank=True, max_length=500, null=True), + ), + migrations.AddField( + model_name="identity", + name="following_uri", + field=models.CharField(blank=True, max_length=500, null=True), + ), + migrations.AddField( + model_name="identity", + name="metadata", + field=models.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name="identity", + name="pinned", + field=models.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name="identity", + name="shared_inbox_uri", + field=models.CharField(blank=True, max_length=500, null=True), + ), + ] diff --git a/users/models/identity.py b/users/models/identity.py index 805755a..6957526 100644 --- a/users/models/identity.py +++ b/users/models/identity.py @@ -23,19 +23,31 @@ from users.models.system_actor import SystemActor class IdentityStates(StateGraph): - outdated = State(try_interval=3600) - updated = State() + """ + There are only two states in a cycle. + Identities sit in "updated" for up to system.identity_max_age, and then + go back to "outdated" for refetching. + """ + + outdated = State(try_interval=3600, force_initial=True) + updated = State(try_interval=86400, attempt_immediately=False) outdated.transitions_to(updated) + updated.transitions_to(outdated) @classmethod async def handle_outdated(cls, identity: "Identity"): # Local identities never need fetching if identity.local: - return "updated" + return cls.updated # Run the actor fetch and progress to updated if it succeeds if await identity.fetch_actor(): - return "updated" + return cls.updated + + @classmethod + async def handle_updated(cls, instance: "Identity"): + if instance.state_age > Config.system.identity_max_age: + return cls.outdated class Identity(StatorModel): diff --git a/users/models/inbox_message.py b/users/models/inbox_message.py index 589f933..079c572 100644 --- a/users/models/inbox_message.py +++ b/users/models/inbox_message.py @@ -1,14 +1,17 @@ from asgiref.sync import sync_to_async from django.db import models +from core.models import Config from stator.models import State, StateField, StateGraph, StatorModel class InboxMessageStates(StateGraph): received = State(try_interval=300) - processed = State() + processed = State(try_interval=86400, attempt_immediately=False) + purged = State() # This is actually deletion, it will never get here received.transitions_to(processed) + processed.transitions_to(purged) @classmethod async def handle_received(cls, instance: "InboxMessage"): @@ -80,6 +83,11 @@ class InboxMessageStates(StateGraph): raise ValueError(f"Cannot handle activity of type {unknown}") return cls.processed + @classmethod + async def handle_processed(cls, instance: "InboxMessage"): + if instance.state_age > Config.system.inbox_message_purge_after: + await InboxMessage.objects.filter(pk=instance.pk).adelete() + class InboxMessage(StatorModel): """