diff --git a/federation/entities/activitypub/entities.py b/federation/entities/activitypub/entities.py index 661e634..64ea310 100644 --- a/federation/entities/activitypub/entities.py +++ b/federation/entities/activitypub/entities.py @@ -1,7 +1,7 @@ import logging import re import uuid -from typing import Dict, List, Set +from typing import Dict, List from federation.entities.activitypub.constants import ( CONTEXTS_DEFAULT, CONTEXT_MANUALLY_APPROVES_FOLLOWERS, CONTEXT_SENSITIVE, CONTEXT_HASHTAG, @@ -13,7 +13,7 @@ from federation.entities.utils import get_base_attributes from federation.outbound import handle_send from federation.types import UserType from federation.utils.django import get_configuration -from federation.utils.text import with_slash +from federation.utils.text import with_slash, validate_handle logger = logging.getLogger("federation") @@ -23,6 +23,7 @@ class AttachImagesMixin(RawContentMixin): """ Attach any embedded images from raw_content. """ + super().pre_send() if self._media_type != "text/markdown": return regex = r"!\[([\w ]*)\]\((https?://[\w\d\-\./]+\.[\w]*((?<=jpg)|(?<=gif)|(?<=png)|(?<=jpeg)))\)" @@ -59,6 +60,7 @@ class CleanContentMixin(RawContentMixin): def cleaner(match): return f"#{match.groups()[0]}" + super().post_receive() self.raw_content = re.sub( r'\[#([\w\-_]+)\]\(http?s://[a-zA-Z0-9/._-]+\)', cleaner, @@ -122,6 +124,10 @@ class ActivitypubNoteMixin(AttachImagesMixin, CleanContentMixin, ActivitypubEnti if tag.get('type') == "Mention" and tag.get('href'): self._mentions.add(tag.get('href')) + def pre_send(self): + super().pre_send() + self.extract_mentions() + def to_as2(self) -> Dict: as2 = { "@context": CONTEXTS_DEFAULT + [ @@ -146,15 +152,35 @@ class ActivitypubNoteMixin(AttachImagesMixin, CleanContentMixin, ActivitypubEnti 'content': self.raw_content, 'mediaType': self._media_type, }, + "tag": [], }, "published": self.created_at.isoformat(), } + if len(self._children): as2["object"]["attachment"] = [] for child in self._children: as2["object"]["attachment"].append(child.to_as2()) - as2["object"]["tag"] = self.add_object_tags() + if len(self._mentions): + mentions = list(self._mentions) + mentions.sort() + for mention in mentions: + if mention.startswith("http"): + as2["object"]["tag"].append({ + 'type': 'Mention', + 'href': mention, + 'name': mention, + }) + elif validate_handle(mention): + # Look up via WebFinger + as2["object"]["tag"].append({ + 'type': 'Mention', + 'href': mention, # TODO need to implement fetch via webfinger for AP handles first + 'name': mention, + }) + + as2["object"]["tag"].extend(self.add_object_tags()) return as2 @@ -172,6 +198,7 @@ class ActivitypubFollow(ActivitypubEntityMixin, Follow): """ Post receive hook - send back follow ack. """ + super().post_receive() if not self.following: return diff --git a/federation/entities/mixins.py b/federation/entities/mixins.py index bd5d128..d4ba203 100644 --- a/federation/entities/mixins.py +++ b/federation/entities/mixins.py @@ -51,6 +51,18 @@ class BaseEntity: klass = getattr(entities, f"{protocol.title()}{self.__class__.__name__}") return klass.from_base(self) + def post_receive(self): + """ + Run any actions after deserializing the payload into an entity. + """ + pass + + def pre_send(self): + """ + Run any actions before serializing the entity for sending. + """ + pass + def validate(self): """Do validation. diff --git a/federation/tests/entities/activitypub/test_entities.py b/federation/tests/entities/activitypub/test_entities.py index d498fa3..1266486 100644 --- a/federation/tests/entities/activitypub/test_entities.py +++ b/federation/tests/entities/activitypub/test_entities.py @@ -127,6 +127,56 @@ class TestEntitiesConvertToAS2: 'published': '2019-04-27T00:00:00', } + def test_post_to_as2__with_mentions(self, activitypubpost_mentions): + activitypubpost_mentions.pre_send() + result = activitypubpost_mentions.to_as2() + assert result == { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + {"pyfed": "https://docs.jasonrobinson.me/ns/python-federation"}, + {'Hashtag': 'as:Hashtag'}, + 'https://w3id.org/security/v1', + {'sensitive': 'as:sensitive'}, + ], + 'type': 'Create', + 'id': 'http://127.0.0.1:8000/post/123456/#create', + 'actor': 'http://127.0.0.1:8000/profile/123456/', + 'object': { + 'id': 'http://127.0.0.1:8000/post/123456/', + 'type': 'Note', + 'attributedTo': 'http://127.0.0.1:8000/profile/123456/', + 'content': """
@{someone@localhost.local}
""", + 'published': '2019-04-27T00:00:00', + 'inReplyTo': None, + 'sensitive': False, + 'summary': None, + 'tag': [ + { + "type": "Mention", + "href": "http://127.0.0.1:8000/profile/999999", + "name": "http://127.0.0.1:8000/profile/999999", + }, + { + "type": "Mention", + "href": "jaywink@localhost.local", + "name": "jaywink@localhost.local", + }, + { + "type": "Mention", + "href": "someone@localhost.local", + "name": "someone@localhost.local", + }, + ], + 'url': '', + 'source': { + 'content': '# raw_content\n\n@{someone@localhost.local}', + 'mediaType': 'text/markdown', + }, + }, + 'published': '2019-04-27T00:00:00', + } + def test_post_to_as2__with_tags(self, activitypubpost_tags): result = activitypubpost_tags.to_as2() assert result == { diff --git a/federation/tests/fixtures/entities.py b/federation/tests/fixtures/entities.py index 85288f8..3a092f7 100644 --- a/federation/tests/fixtures/entities.py +++ b/federation/tests/fixtures/entities.py @@ -87,6 +87,23 @@ def activitypubpost_images(): ) +@pytest.fixture +def activitypubpost_mentions(): + with freeze_time("2019-04-27"): + return ActivitypubPost( + raw_content="""# raw_content\n\n@{someone@localhost.local}""", + public=True, + provider_display_name="Socialhome", + id=f"http://127.0.0.1:8000/post/123456/", + activity_id=f"http://127.0.0.1:8000/post/123456/#create", + actor_id=f"http://127.0.0.1:8000/profile/123456/", + _mentions={ + "http://127.0.0.1:8000/profile/999999", + "jaywink@localhost.local", + } + ) + + @pytest.fixture def activitypubpost_tags(): with freeze_time("2019-04-27"):