diff --git a/federation/entities/activitypub/entities.py b/federation/entities/activitypub/entities.py index cb04f50..6b4cda3 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 +from typing import Dict, List, Set from federation.entities.activitypub.constants import ( CONTEXTS_DEFAULT, CONTEXT_MANUALLY_APPROVES_FOLLOWERS, CONTEXT_SENSITIVE, CONTEXT_HASHTAG, @@ -109,6 +109,20 @@ class ActivitypubNoteMixin(AttachImagesMixin, CleanContentMixin, ActivitypubEnti tags.append(_tag) return tags + def extract_mentions(self) -> Set: + """ + Extract mentions from the source object. + """ + if not isinstance(self._source_object, dict): + return set() + _mentions = set() + source = self._source_object.get('object') if isinstance(self._source_object.get('object'), dict) else \ + self._source_object + for tag in source.get('tag', []): + if tag.get('type') == "Mention" and tag.get('href'): + _mentions.add(tag.get('href')) + return _mentions + def to_as2(self) -> Dict: as2 = { "@context": CONTEXTS_DEFAULT + [ diff --git a/federation/entities/diaspora/mixins.py b/federation/entities/diaspora/mixins.py index fcf41cc..36e98c5 100644 --- a/federation/entities/diaspora/mixins.py +++ b/federation/entities/diaspora/mixins.py @@ -1,4 +1,5 @@ import re +from typing import Set from Crypto.PublicKey import RSA from lxml import etree @@ -14,7 +15,7 @@ class DiasporaEntityMixin(BaseEntity): # Normally outbound document is generated from entity. Store one here if at some point we already have a doc outbound_doc = None - def extract_mentions(self): + def extract_mentions(self) -> Set: """ Extract mentions from an entity with ``raw_content``. diff --git a/federation/entities/mixins.py b/federation/entities/mixins.py index 9dc8267..51b9cac 100644 --- a/federation/entities/mixins.py +++ b/federation/entities/mixins.py @@ -1,7 +1,7 @@ import datetime import importlib import warnings -from typing import List, Set +from typing import List, Set, Union, Dict from commonmark import commonmark @@ -16,7 +16,7 @@ class BaseEntity: _receivers: List = None _source_protocol: str = "" # Contains the original object from payload as a string - _source_object: str = None + _source_object: Union[str, Dict] = None _sender_key: str = "" # ActivityType activity: ActivityType = None @@ -50,7 +50,7 @@ class BaseEntity: klass = getattr(entities, f"{protocol.title()}{self.__class__.__name__}") return klass.from_base(self) - def extract_mentions(self): + def extract_mentions(self) -> Set: return set() def validate(self): @@ -178,10 +178,12 @@ class CreatedAtMixin(BaseEntity): class RawContentMixin(BaseEntity): _media_type: str = "text/markdown" + _mentions: List = None _rendered_content: str = "" raw_content: str = "" def __init__(self, *args, **kwargs): + self._mentions = [] super().__init__(*args, **kwargs) self._required += ["raw_content"] diff --git a/federation/tests/entities/activitypub/test_mappers.py b/federation/tests/entities/activitypub/test_mappers.py index 6cbbb20..544eb91 100644 --- a/federation/tests/entities/activitypub/test_mappers.py +++ b/federation/tests/entities/activitypub/test_mappers.py @@ -11,7 +11,7 @@ from federation.tests.fixtures.payloads import ( ACTIVITYPUB_FOLLOW, ACTIVITYPUB_PROFILE, ACTIVITYPUB_PROFILE_INVALID, ACTIVITYPUB_UNDO_FOLLOW, ACTIVITYPUB_POST, ACTIVITYPUB_COMMENT, ACTIVITYPUB_RETRACTION, ACTIVITYPUB_SHARE, ACTIVITYPUB_RETRACTION_SHARE, ACTIVITYPUB_POST_IMAGES, ACTIVITYPUB_POST_WITH_SOURCE_MARKDOWN, ACTIVITYPUB_POST_WITH_TAGS, - ACTIVITYPUB_POST_WITH_SOURCE_BBCODE) + ACTIVITYPUB_POST_WITH_SOURCE_BBCODE, ACTIVITYPUB_POST_WITH_MENTIONS) from federation.types import UserType, ReceiverVariant @@ -83,6 +83,15 @@ class TestActivitypubEntityMappersReceive: assert isinstance(post, Post) assert post.raw_content == 'boom #test' + def test_message_to_objects_simple_post__with_mentions(self): + entities = message_to_objects(ACTIVITYPUB_POST_WITH_MENTIONS, "https://mastodon.social/users/jaywink") + assert len(entities) == 1 + post = entities[0] + assert isinstance(post, ActivitypubPost) + assert isinstance(post, Post) + assert len(post._mentions) == 1 + assert list(post._mentions)[0] == "https://dev3.jasonrobinson.me/u/jaywink/" + def test_message_to_objects_simple_post__with_source__bbcode(self): entities = message_to_objects(ACTIVITYPUB_POST_WITH_SOURCE_BBCODE, "https://diaspodon.fr/users/jaywink") assert len(entities) == 1 diff --git a/federation/tests/fixtures/payloads/activitypub.py b/federation/tests/fixtures/payloads/activitypub.py index 9adfa05..409dbdf 100644 --- a/federation/tests/fixtures/payloads/activitypub.py +++ b/federation/tests/fixtures/payloads/activitypub.py @@ -341,6 +341,50 @@ ACTIVITYPUB_POST_WITH_TAGS = { 'signatureValue': 'SjDACS7Z/Cb1SEC3AtxEokID5SHAYl7kpys/hhmaRbpXuFKCxfj2P9BmH8QhLnuam3sENZlrnBOcB5NlcBhIfwo/Xh242RZBmPQf+edTVYVCe1j19dihcftNCHtnqAcKwp/51dNM/OlKu2730FrwvOUXVIPtB7iVqkseO9TRzDYIDj+zBTksnR/NAYtq6SUpmefXfON0uW3N3Uq6PGfExJaS+aeqRf8cPGkZFSIUQZwOLXbIpb7BFjJ1+y1OMOAJueqvikUprAit3v6BiNWurAvSQpC7WWMFUKyA79/xtkO9kIPA/Q4C9ryqdzxZJ0jDhXiaIIQj2JZfIADdjLZHJA=='} } +ACTIVITYPUB_POST_WITH_MENTIONS = {'@context': ['https://www.w3.org/ns/activitystreams', + {'ostatus': 'http://ostatus.org#', + 'atomUri': 'ostatus:atomUri', + 'inReplyToAtomUri': 'ostatus:inReplyToAtomUri', + 'conversation': 'ostatus:conversation', + 'sensitive': 'as:sensitive'}], + 'id': 'https://mastodon.social/users/jaywink/statuses/102750454691863505/activity', + 'type': 'Create', + 'actor': 'https://mastodon.social/users/jaywink', + 'published': '2019-09-07T09:11:54Z', + 'to': ['https://www.w3.org/ns/activitystreams#Public'], + 'cc': ['https://mastodon.social/users/jaywink/followers', + 'https://dev3.jasonrobinson.me/u/jaywink/'], + 'object': {'id': 'https://mastodon.social/users/jaywink/statuses/102750454691863505', + 'type': 'Note', + 'summary': None, + 'inReplyTo': None, + 'published': '2019-09-07T09:11:54Z', + 'url': 'https://mastodon.social/@jaywink/102750454691863505', + 'attributedTo': 'https://mastodon.social/users/jaywink', + 'to': ['https://www.w3.org/ns/activitystreams#Public'], + 'cc': ['https://mastodon.social/users/jaywink/followers', + 'https://dev3.jasonrobinson.me/u/jaywink/'], + 'sensitive': False, + 'atomUri': 'https://mastodon.social/users/jaywink/statuses/102750454691863505', + 'inReplyToAtomUri': None, + 'conversation': 'tag:mastodon.social,2019-09-07:objectId=123339599:objectType=Conversation', + 'content': '

@jaywink need a mention payload - here!

', + 'contentMap': {'en': '

@jaywink need a mention payload - here!

'}, + 'attachment': [], + 'tag': [{'type': 'Mention', + 'href': 'https://dev3.jasonrobinson.me/u/jaywink/', + 'name': '@jaywink@dev3.jasonrobinson.me'}], + 'replies': {'id': 'https://mastodon.social/users/jaywink/statuses/102750454691863505/replies', + 'type': 'Collection', + 'first': {'type': 'CollectionPage', + 'next': 'https://mastodon.social/users/jaywink/statuses/102750454691863505/replies?only_other_accounts=true&page=true', + 'partOf': 'https://mastodon.social/users/jaywink/statuses/102750454691863505/replies', + 'items': []}}}, + 'signature': {'type': 'RsaSignature2017', + 'creator': 'https://mastodon.social/users/jaywink#main-key', + 'created': '2019-09-07T09:11:54Z', + 'signatureValue': 'FOO'}} + ACTIVITYPUB_POST_WITH_SOURCE_MARKDOWN = { '@context': ['https://www.w3.org/ns/activitystreams', {'ostatus': 'http://ostatus.org#',