From db3fc7c53c8cbdaa6e094a1cbdeb2a0915416c3a Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Tue, 20 Dec 2022 10:17:52 +0000 Subject: [PATCH] Fetch actors with posts when needed Fixes #190, #205 --- activities/models/post.py | 14 ++++++++++++-- activities/services/__init__.py | 1 + activities/{ => services}/search.py | 2 +- activities/views/search.py | 4 ++-- api/views/search.py | 4 ++-- stator/exceptions.py | 5 +++++ stator/models.py | 3 +++ 7 files changed, 26 insertions(+), 7 deletions(-) rename activities/{ => services}/search.py (99%) create mode 100644 stator/exceptions.py diff --git a/activities/models/post.py b/activities/models/post.py index dd18d87..51e1013 100644 --- a/activities/models/post.py +++ b/activities/models/post.py @@ -23,6 +23,7 @@ from activities.models.post_types import ( from activities.templatetags.emoji_tags import imageify_emojis from core.html import sanitize_post, strip_html from core.ld import canonicalise, format_ld_date, get_list, parse_ld_date +from stator.exceptions import TryAgainLater from stator.models import State, StateField, StateGraph, StatorModel from users.models.identity import Identity, IdentityStates from users.models.system_actor import SystemActor @@ -686,7 +687,7 @@ class Post(StatorModel): ### ActivityPub (inbound) ### @classmethod - def by_ap(cls, data, create=False, update=False) -> "Post": + def by_ap(cls, data, create=False, update=False, fetch_author=False) -> "Post": """ Retrieves a Post instance by its ActivityPub JSON object. @@ -704,8 +705,16 @@ class Post(StatorModel): if create: # Resolve the author author = Identity.by_actor_uri(data["attributedTo"], create=create) + # If the author is not fetched yet, try again later + if author.domain is None: + if fetch_author: + async_to_sync(author.fetch_actor)() + if author.domain is None: + raise TryAgainLater() + else: + raise TryAgainLater() # If the post is from a blocked domain, stop and drop - if author.domain and author.domain.blocked: + if author.domain.blocked: raise cls.DoesNotExist("Post is from a blocked domain") post = cls.objects.create( object_uri=data["id"], @@ -800,6 +809,7 @@ class Post(StatorModel): canonicalise(response.json(), include_security=True), create=True, update=True, + fetch_author=True, ) # We may need to fetch the author too if post.author.state == IdentityStates.outdated: diff --git a/activities/services/__init__.py b/activities/services/__init__.py index 6e973c5..f5afa1f 100644 --- a/activities/services/__init__.py +++ b/activities/services/__init__.py @@ -1 +1,2 @@ from .post import PostService # noqa +from .search import SearchService # noqa diff --git a/activities/search.py b/activities/services/search.py similarity index 99% rename from activities/search.py rename to activities/services/search.py index d1f6dc1..a681180 100644 --- a/activities/search.py +++ b/activities/services/search.py @@ -7,7 +7,7 @@ from users.models import Domain, Identity, IdentityStates from users.models.system_actor import SystemActor -class Searcher: +class SearchService: """ Captures the logic needed to search - reused in the UI and API """ diff --git a/activities/views/search.py b/activities/views/search.py index 93c0012..4c709e0 100644 --- a/activities/views/search.py +++ b/activities/views/search.py @@ -1,7 +1,7 @@ from django import forms from django.views.generic import FormView -from activities.search import Searcher +from activities.services import SearchService class Search(FormView): @@ -15,7 +15,7 @@ class Search(FormView): ) def form_valid(self, form): - searcher = Searcher(form.cleaned_data["query"], self.request.identity) + searcher = SearchService(form.cleaned_data["query"], self.request.identity) # Render results context = self.get_context_data(form=form) context["results"] = searcher.search_all() diff --git a/api/views/search.py b/api/views/search.py index bd44cd7..6d91cbe 100644 --- a/api/views/search.py +++ b/api/views/search.py @@ -3,7 +3,7 @@ from typing import Literal from ninja import Field from activities.models import PostInteraction -from activities.search import Searcher +from activities.services.search import SearchService from api import schemas from api.decorators import identity_required from api.views.base import api_router @@ -32,7 +32,7 @@ def search( if max_id or since_id or min_id or offset: return result # Run search - searcher = Searcher(q, request.identity) + searcher = SearchService(q, request.identity) search_result = searcher.search_all() if type is None or type == "accounts": result["accounts"] = [i.to_mastodon_json() for i in search_result["identities"]] diff --git a/stator/exceptions.py b/stator/exceptions.py new file mode 100644 index 0000000..6bb2d06 --- /dev/null +++ b/stator/exceptions.py @@ -0,0 +1,5 @@ +class TryAgainLater(BaseException): + """ + Special exception that Stator will catch without error, + leaving a state to have another attempt soon. + """ diff --git a/stator/models.py b/stator/models.py index 04bbd79..350421d 100644 --- a/stator/models.py +++ b/stator/models.py @@ -8,6 +8,7 @@ from django.utils import timezone from django.utils.functional import classproperty from core import exceptions +from stator.exceptions import TryAgainLater from stator.graph import State, StateGraph @@ -169,6 +170,8 @@ class StatorModel(models.Model): return None try: next_state = await current_state.handler(self) # type: ignore + except TryAgainLater: + pass except BaseException as e: await exceptions.acapture_exception(e) traceback.print_exc()