diff --git a/federation/entities/activitypub/models.py b/federation/entities/activitypub/models.py index 1cda53e..711b979 100644 --- a/federation/entities/activitypub/models.py +++ b/federation/entities/activitypub/models.py @@ -35,10 +35,10 @@ from federation.utils.text import with_slash, validate_handle logger = logging.getLogger("federation") -def get_profile_or_entity(fid): - obj = get_profile(fid=fid) - if not obj: - obj = retrieve_and_parse_document(fid) +def get_profile_or_entity(**kwargs): + obj = get_profile(**kwargs) + if not obj and kwargs.get('fid'): + obj = retrieve_and_parse_document(kwargs['fid']) return obj @@ -586,6 +586,7 @@ class Person(Object, base.Profile): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self._required += ['url'] self._allowed_children += (Note, PropertyValue, IdentityProof) # Set finger to username@host if not provided by the platform @@ -866,18 +867,22 @@ class Note(Object, RawContentMixin): def _find_and_mark_mentions(self): mentions = [mention for mention in self.tag_objects if isinstance(mention, Mention)] - hrefs = [] + # There seems to be consensus on using the profile url for + # the link and the profile id for the Mention object href property, + # but some platforms will set mention.href to the profile url, so + # we check both. for mention in mentions: - hrefs.append(mention.href) - # add Mastodon's form - parsed = urlparse(mention.href) - username = mention.name.lstrip('@').split('@')[0] - hrefs.append(f'{parsed.scheme}://{parsed.netloc}/@{username}') - for href in hrefs: - links = self._soup.find_all(href=href) - for link in links: - profile = get_profile_or_entity(fid=link['href']) - if profile: + hrefs = [] + profile = get_profile_or_entity(fid=mention.href, remote_url=mention.href) + if profile and not profile.url: + # This should be removed when we are confident that the remote_url property + # has been populated for most profiles on the client app side. + profile = retrieve_and_parse_profile(profile.id) + if profile: + hrefs.extend([profile.id, profile.url]) + for href in hrefs: + links = self._soup.find_all(href=href) + for link in links: link['data-mention'] = profile.finger self._mentions.add(profile.finger) @@ -1317,7 +1322,7 @@ def extract_receivers(entity): profile = None # don't care about receivers for payloads without an actor_id if getattr(entity, 'actor_id'): - profile = get_profile_or_entity(entity.actor_id) + profile = get_profile_or_entity(fid=entity.actor_id) if not isinstance(profile, base.Profile): return receivers diff --git a/federation/tests/entities/activitypub/test_mappers.py b/federation/tests/entities/activitypub/test_mappers.py index 9a2c042..ba6bbbb 100644 --- a/federation/tests/entities/activitypub/test_mappers.py +++ b/federation/tests/entities/activitypub/test_mappers.py @@ -91,7 +91,8 @@ class TestActivitypubEntityMappersReceive: assert post.raw_content == '' assert post.rendered_content == '

boom

' - @patch("federation.entities.activitypub.models.get_profile_or_entity", return_value=Person(finger="jaywink@dev3.jasonrobinson.me")) + @patch("federation.entities.activitypub.models.get_profile_or_entity", + return_value=Person(finger="jaywink@dev3.jasonrobinson.me",url="https://dev3.jasonrobinson.me/u/jaywink/")) def test_message_to_objects_simple_post__with_mentions(self, mock_get): entities = message_to_objects(ACTIVITYPUB_POST_WITH_MENTIONS, "https://mastodon.social/users/jaywink") assert len(entities) == 1 @@ -102,7 +103,8 @@ class TestActivitypubEntityMappersReceive: assert list(post._mentions)[0] == "jaywink@dev3.jasonrobinson.me" - @patch("federation.entities.activitypub.models.get_profile_or_entity", return_value=Person(finger="jaywink@dev.jasonrobinson.me")) + @patch("federation.entities.activitypub.models.get_profile_or_entity", + return_value=Person(finger="jaywink@dev.jasonrobinson.me",url="https://dev.jasonrobinson.me/u/jaywink/")) def test_message_to_objects_simple_post__with_source__bbcode(self, mock_get): entities = message_to_objects(ACTIVITYPUB_POST_WITH_SOURCE_BBCODE, "https://diaspodon.fr/users/jaywink") assert len(entities) == 1 @@ -113,7 +115,8 @@ class TestActivitypubEntityMappersReceive: '@jaywink boom

' assert post.raw_content == '' - @patch("federation.entities.activitypub.models.get_profile_or_entity", return_value=Person(finger="jaywink@dev.jasonrobinson.me")) + @patch("federation.entities.activitypub.models.get_profile_or_entity", + return_value=Person(finger="jaywink@dev.jasonrobinson.me",url="https://dev.robinson.me/u/jaywink/")) def test_message_to_objects_simple_post__with_source__markdown(self, mock_get): entities = message_to_objects(ACTIVITYPUB_POST_WITH_SOURCE_MARKDOWN, "https://diaspodon.fr/users/jaywink") assert len(entities) == 1 @@ -147,7 +150,8 @@ class TestActivitypubEntityMappersReceive: assert photo.guid == "" assert photo.handle == "" - @patch("federation.entities.activitypub.models.get_profile_or_entity", return_value=Person(finger="jaywink@dev.jasonrobinson.me")) + @patch("federation.entities.activitypub.models.get_profile_or_entity", + return_value=Person(finger="jaywink@dev.jasonrobinson.me", url="https://dev.jasonrobinson.me/u/jaywink/")) def test_message_to_objects_comment(self, mock_get): entities = message_to_objects(ACTIVITYPUB_COMMENT, "https://diaspodon.fr/users/jaywink") assert len(entities) == 1 diff --git a/federation/tests/fixtures/entities.py b/federation/tests/fixtures/entities.py index c0d1a07..e555a97 100644 --- a/federation/tests/fixtures/entities.py +++ b/federation/tests/fixtures/entities.py @@ -256,7 +256,8 @@ def profile(): inboxes={ "private": "https://example.com/bob/private", "public": "https://example.com/public", - }, public_key=PUBKEY, to=["https://www.w3.org/ns/activitystreams#Public"] + }, public_key=PUBKEY, to=["https://www.w3.org/ns/activitystreams#Public"], + url="https://example.com/alice" ) diff --git a/federation/tests/utils/test_activitypub.py b/federation/tests/utils/test_activitypub.py index 2572b42..46e7d46 100644 --- a/federation/tests/utils/test_activitypub.py +++ b/federation/tests/utils/test_activitypub.py @@ -60,7 +60,7 @@ class TestRetrieveAndParseDocument: entity = retrieve_and_parse_document("https://example.com/foobar") assert isinstance(entity, Follow) - @patch("federation.entities.activitypub.models.extract_receivers", return_value=[]) + @patch("federation.entities.activitypub.models.get_profile_or_entity", return_value=None) @patch("federation.utils.activitypub.fetch_document", autospec=True, return_value=( json.dumps(ACTIVITYPUB_POST_OBJECT), None, None), ) @@ -80,7 +80,7 @@ class TestRetrieveAndParseDocument: "/foobar.jpg" @patch("federation.entities.activitypub.models.verify_ld_signature", return_value=None) - @patch("federation.entities.activitypub.models.extract_receivers", return_value=[]) + @patch("federation.entities.activitypub.models.get_profile_or_entity", return_value=None) @patch("federation.utils.activitypub.fetch_document", autospec=True, return_value=( json.dumps(ACTIVITYPUB_POST), None, None), )