From d7642e03923d42f95f4a54463dc578f2a74a075d Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Sat, 17 Aug 2024 16:48:55 -0700 Subject: [PATCH] AP DMs: wrap in post activity, add mention tag, improve DM vs public handling --- activitypub.py | 14 ++++++++------ protocol.py | 4 ++++ tests/test_activitypub.py | 37 ++++++++++++++++++++++++++++++++++++- tests/test_integrations.py | 6 +++++- 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/activitypub.py b/activitypub.py index 9cab442..df48453 100644 --- a/activitypub.py +++ b/activitypub.py @@ -785,7 +785,9 @@ def postprocess_as2(activity, orig_obj=None, wrap=True): tags = util.get_list(activity, 'tag') + util.get_list(obj, 'tag') for tag in tags: href = tag.get('href') - if (href and tag.get('type') == 'Mention' + if (tag.get('type') == 'Mention' + and href + and href not in util.get_list(obj_or_activity, 'to') and not ActivityPub.is_blocklisted(href)): add(cc, href) @@ -794,15 +796,15 @@ def postprocess_as2(activity, orig_obj=None, wrap=True): for recip in as1.get_objects(orig_obj, field): add(cc, util.get_url(recip) or recip.get('id')) - # ideally I'd use as1.is_dm here, but it's for AS1, not AS2 - to = activity.setdefault('to', []) - is_dm = not cc and len(to) == 1 - if not is_dm: + # WARNING: activity here is AS2, but we're using as1.is_dm. right now the + # logic is effectively the same for our purposes, but watch out here if that + # ever changes. + if not as1.is_dm(activity): # to public, since Mastodon interprets to public as public, cc public as # unlisted: # https://socialhub.activitypub.rocks/t/visibility-to-cc-mapping/284 # https://wordsmith.social/falkreon/securing-activitypub - add(to, as2.PUBLIC_AUDIENCE) + add(activity.setdefault('to', []), as2.PUBLIC_AUDIENCE) # hashtags. Mastodon requires: # * type: Hashtag diff --git a/protocol.py b/protocol.py index 5f987d5..cd5ef74 100644 --- a/protocol.py +++ b/protocol.py @@ -1174,6 +1174,10 @@ class Protocol: 'id': id, 'author': bot.key.id(), 'content': text, + 'tags': [{ + 'objectType': 'mention', + 'url': to_user.key.id(), + }], 'to': [to_user.key.id()], }, 'to': [to_user.key.id()], diff --git a/tests/test_activitypub.py b/tests/test_activitypub.py index 0d36c51..91ac8e5 100644 --- a/tests/test_activitypub.py +++ b/tests/test_activitypub.py @@ -2456,7 +2456,7 @@ class ActivityPubUtilsTest(TestCase): self.assertEqual(['https://masto.foo/@other'], postprocess_as2(obj)['cc']) - def test_postprocess_as2_dm(self): + def test_postprocess_as2_dm_note(self): dm = { 'objectType': 'note', 'author': 'web.brid.gy', @@ -2466,6 +2466,41 @@ class ActivityPubUtilsTest(TestCase): } self.assertEqual(dm, postprocess_as2(copy.deepcopy(dm))) + def test_postprocess_as2_dm_note_with_mention_tag(self): + dm = { + 'objectType': 'note', + 'author': 'web.brid.gy', + 'content': '

hello world

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

hello world

'}, + 'tags': [{ + 'objectType': 'mention', + 'url': 'https://inst/user', + }], + 'to': ['http://inst/user'], + } + self.assertEqual(dm, postprocess_as2(copy.deepcopy(dm))) + + def test_postprocess_as2_dm_create(self): + dm = { + 'objectType': 'activity', + 'verb': 'post', + 'id': 'https://inst/dm#create', + 'actor': 'web.brid.gy', + 'object': { + 'objectType': 'note', + 'id': 'https://inst/dm', + 'author': 'web.brid.gy', + 'content': '

hello world

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

hello world

'}, + 'to': ['http://inst/user'], + }, + 'to': ['http://inst/user'], + } + self.assertEqual({ + **dm, + 'id': 'http://localhost/r/https://inst/dm#create', + }, postprocess_as2(copy.deepcopy(dm))) + @patch('requests.get') def test_signed_get_redirects_manually_with_new_sig_headers(self, mock_get): mock_get.side_effect = [ diff --git a/tests/test_integrations.py b/tests/test_integrations.py index 70dd13e..e95aaf7 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -409,10 +409,14 @@ class IntegrationTests(TestCase): 'content': message, 'contentMap': {'en': message}, 'content_is_html': True, + 'tag': [{ + 'type': 'Mention', + 'href': 'https://inst/alice', + }], 'to': ['https://inst/alice'], }, 'to': ['https://inst/alice'], - }, json_loads(kwargs['data']), ignore=['to', '@context']) + }, json_loads(kwargs['data']), ignore=['@context']) # bot user follows back args, kwargs = mock_post.call_args_list[2]