Skip to content

Commit

Permalink
AP: add outbound DM support
Browse files Browse the repository at this point in the history
for #1024, #966, etc
  • Loading branch information
snarfed committed Aug 9, 2024
1 parent 23aa24e commit cd0a4bc
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 13 deletions.
18 changes: 11 additions & 7 deletions activitypub.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,25 +777,29 @@ def postprocess_as2(activity, orig_obj=None, wrap=True):
# https://www.w3.org/TR/activitystreams-vocabulary/#audienceTargeting
# https://w3c.github.io/activitypub/#delivery
# https://docs.joinmastodon.org/spec/activitypub/#Mention
obj_or_activity.setdefault('cc', [])
cc = obj_or_activity.setdefault('cc', [])

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'
and not ActivityPub.is_blocklisted(href)):
add(obj_or_activity['cc'], href)
add(cc, href)

if orig_obj and type in as2.TYPE_TO_VERB:
for field in 'actor', 'attributedTo', 'to', 'cc':
for recip in as1.get_objects(orig_obj, field):
add(obj_or_activity['cc'], util.get_url(recip) or recip.get('id'))
add(cc, util.get_url(recip) or recip.get('id'))

# 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
# ideally I'd use as1.is_dm here, but it's for AS1, not AS2
to = activity.setdefault('to', [])
add(to, as2.PUBLIC_AUDIENCE)
is_dm = not cc and len(to) == 1
if not is_dm:
# 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)

# hashtags. Mastodon requires:
# * type: Hashtag
Expand Down
3 changes: 2 additions & 1 deletion protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -1139,7 +1139,8 @@ def bot_dm(bot_cls, to_user, text):
logger.info(f'Sending DM from {bot.key.id()} to {to_user.key.id()}: {text[:100]}')

id = f'{bot.profile_id()}#welcome-dm-{to_user.key.id()}-{util.now().isoformat()}'
target = Target(protocol=to_user.LABEL, uri=to_user.target_for(to_user.obj))
target_uri = to_user.target_for(to_user.obj, shared=False)
target = Target(protocol=to_user.LABEL, uri=target_uri)
obj_key = Object(id=id, source_protocol='web', undelivered=[target], our_as1={
'objectType': 'note',
'id': id,
Expand Down
42 changes: 39 additions & 3 deletions tests/test_activitypub.py
Original file line number Diff line number Diff line change
Expand Up @@ -2376,6 +2376,16 @@ def test_postprocess_as2_mentions_into_cc(self):
self.assertEqual(['https://masto.foo/@other'],
postprocess_as2(obj)['cc'])

def test_postprocess_as2_dm(self):
dm = {
'objectType': 'note',
'author': 'web.brid.gy',
'content': '<p>hello world</p>',
'contentMap': {'en': '<p>hello world</p>'},
'to': ['http://inst/user'],
}
self.assertEqual(dm, 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 = [
Expand Down Expand Up @@ -2790,10 +2800,8 @@ def test_send_blocklisted(self, mock_post):
'https://fed.brid.gy/ap/sharedInbox'))
mock_post.assert_not_called()

@patch('requests.post')
@patch('requests.post', return_value=requests_response())
def test_send_convert_ids(self, mock_post):
mock_post.return_value = requests_response()

like = Object(our_as1={
'id': 'fake:like',
'objectType': 'activity',
Expand All @@ -2814,3 +2822,31 @@ def test_send_convert_ids(self, mock_post):
'actor': 'https://fa.brid.gy/ap/fake:user',
'to': [as2.PUBLIC_AUDIENCE],
}, json_loads(kwargs['data']))

@patch('requests.post', return_value=requests_response())
def test_send_dm(self, mock_post):
bot = self.make_user('web.brid.gy', cls=Web)
user = self.make_user(ACTOR['id'], cls=ActivityPub, obj_as2=ACTOR)

dm = Object(id='https://internal.brid.gy/dm', source_protocol='web', our_as1={
'objectType': 'note',
'author': 'web.brid.gy',
'content': 'hello world',
'to': [ACTOR['id']],
})
dm.put()
self.assertTrue(ActivityPub.send(dm, ACTOR['inbox'], from_user=bot))

self.assertEqual(1, len(mock_post.call_args_list))
args, kwargs = mock_post.call_args_list[0]
self.assertEqual((ACTOR['inbox'],), args)
self.assertEqual({
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Note',
'id': 'http://localhost/r/https://internal.brid.gy/dm',
'attributedTo': 'https://web.brid.gy/web.brid.gy',
'content': '<p>hello world</p>',
'contentMap': {'en': '<p>hello world</p>'},
'content_is_html': True,
'to': [ACTOR['id']],
}, json_loads(kwargs['data']))
17 changes: 15 additions & 2 deletions tests/test_integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,9 +393,22 @@ def test_activitypub_follow_bsky_bot_user_enables_protocol(self, mock_get, mock_
list(records.keys()))
self.assertEqual(['self'], list(records['app.bsky.actor.profile'].keys()))

# bot user follows back
# bot user DM
args, kwargs = mock_post.call_args_list[1]
self.assert_equals(('http://inst/inbox',), args)
self.assert_equals({
'type': 'Note',
'id': 'https://bsky.brid.gy/r/https://bsky.brid.gy/#welcome-dm-https://inst/alice-2022-01-02T03:04:05+00:00',
'actor': 'https://bsky.brid.gy/bsky.brid.gy',
'content': '<p>hello world</p>',
'contentMap': {'en': '<p>hello world</p>'},
'content_is_html': True,
'to': ['https://inst/alice'],
}, json_loads(kwargs['data']), ignore=['to', '@context'])

# bot user follows back
args, kwargs = mock_post.call_args_list[2]
self.assert_equals(('http://inst/inbox',), args)
self.assert_equals({
'type': 'Follow',
'id': 'https://bsky.brid.gy/r/https://bsky.brid.gy/#follow-back-https://inst/alice-2022-01-02T03:04:05+00:00',
Expand All @@ -404,7 +417,7 @@ def test_activitypub_follow_bsky_bot_user_enables_protocol(self, mock_get, mock_
}, json_loads(kwargs['data']), ignore=['to', '@context'])

# accept user's follow
args, kwargs = mock_post.call_args_list[2]
args, kwargs = mock_post.call_args_list[3]
self.assert_equals(('http://inst/inbox',), args)
self.assert_equals({
'type': 'Accept',
Expand Down
2 changes: 2 additions & 0 deletions tests/test_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -2464,8 +2464,10 @@ def test_follow_and_block_protocol_user_sets_enabled_protocols(self):
self.assertEqual(['eefake:user'], Fake.created_for)
self.assertTrue(user.is_enabled(Fake))

dm_id = 'https://fa.brid.gy/#welcome-dm-eefake:user-2022-01-02T03:04:05+00:00'
follow_back_id = 'https://fa.brid.gy/#follow-back-eefake:user-2022-01-02T03:04:05+00:00'
self.assertEqual([
(dm_id, 'eefake:user:target'),
# fa.brid.gy follows back
(follow_back_id, 'eefake:user:target'),
('fa.brid.gy/followers#accept-eefake:follow', 'eefake:user:target'),
Expand Down
2 changes: 2 additions & 0 deletions tests/testutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ def create_for(cls, user):
cls.created_for.append(id)
add(user.copies, Target(uri=ids.translate_user_id(id=id, from_=user, to=cls),
protocol=cls.LABEL))
user.put()

if user.obj_key:
profile_copy_id = ids.translate_object_id(
id=user.profile_id(), from_=user, to=cls)
Expand Down

0 comments on commit cd0a4bc

Please sign in to comment.