Skip to content

Commit

Permalink
opt in/out prompt: accept yes/no DMs to bot users to enable/disable p…
Browse files Browse the repository at this point in the history
…rotocols

for #880
  • Loading branch information
snarfed committed Apr 21, 2024
1 parent 0c37d94 commit 1686a2b
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 1 deletion.
13 changes: 13 additions & 0 deletions activitypub.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
host_url,
LOCAL_DOMAINS,
PRIMARY_DOMAIN,
PROTOCOL_DOMAINS,
redirect_wrap,
subdomain_wrap,
unwrap,
Expand All @@ -56,6 +57,8 @@

FEDI_URL_RE = re.compile(r'https://[^/]+/(@|users/)([^/@]+)(@[^/@]+)?(/(?:statuses/)?[0-9]+)?')

_BOT_ACTOR_IDS = None


def instance_actor():
global _INSTANCE_ACTOR
Expand All @@ -65,6 +68,15 @@ def instance_actor():
return _INSTANCE_ACTOR


def bot_actor_ids():
global _BOT_ACTOR_IDS
if _BOT_ACTOR_IDS is None:
from activitypub import ActivityPub
_BOT_ACTOR_IDS = [translate_user_id(id=domain, from_=Web, to=ActivityPub)
for domain in PROTOCOL_DOMAINS]
return _BOT_ACTOR_IDS


class ActivityPub(User, Protocol):
"""ActivityPub protocol class.
Expand Down Expand Up @@ -926,6 +938,7 @@ def inbox(protocol=None, id=None):
# follows, or other activity types, since Mastodon doesn't currently mark
# those as explicitly public. Use as2's is_public instead of as1's because
# as1's interprets unlisted as true.
# TODO: move this to Protocol
if type == 'Create' and not as2.is_public(activity, unlisted=False):
logger.info('Dropping non-public activity')
return 'OK'
Expand Down
14 changes: 14 additions & 0 deletions protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,20 @@ def receive(from_cls, obj, authed_as=None, internal=False):
from_user.disable_protocol(proto)
return 'OK', 200

elif obj.type == 'post':
to_cc = (util.get_list(inner_obj_as1, 'to')
+ util.get_list(inner_obj_as1, 'cc'))
if len(to_cc) == 1 and to_cc[0] in PROTOCOL_DOMAINS:
content = inner_obj_as1.get('content').strip().lower()
logger.info(f'DM to bot user {to_cc}: {content}')
proto = Protocol.for_bridgy_subdomain(to_cc[0])
assert proto
if content in ('yes', 'ok'):
from_user.enable_protocol(proto)
elif content == 'no':
from_user.disable_protocol(proto)
return 'OK', 200

# fetch actor if necessary
if actor and actor.keys() == set(['id']):
logger.info('Fetching actor so we have name, profile photo, etc')
Expand Down
3 changes: 3 additions & 0 deletions tests/test_activitypub.py
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,9 @@ def test_inbox_private(self, *mocks):
def test_inbox_unlisted(self, *mocks):
self._test_inbox_with_to_ignored(['@unlisted'], *mocks)

def test_inbox_dm(self, *mocks):
self._test_inbox_with_to_ignored(['http://localhost/web/user.com'], *mocks)

def _test_inbox_with_to_ignored(self, to, mock_head, mock_get, mock_post):
Follower.get_or_create(to=self.make_user(ACTOR['id'], cls=ActivityPub),
from_=self.user)
Expand Down
41 changes: 40 additions & 1 deletion tests/test_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -1796,7 +1796,7 @@ def test_follow_and_block_protocol_user_sets_enabled_protocols(self):
user = self.make_user('eefake:user', cls=ExplicitEnableFake)
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, user))

# protocol isn't enabled yet, block should be a noop
# fake protocol isn't enabled yet, block should be a noop
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(block))
user = user.key.get()
self.assertEqual([], user.enabled_protocols)
Expand Down Expand Up @@ -1826,6 +1826,45 @@ def test_follow_and_block_protocol_user_sets_enabled_protocols(self):
self.assertEqual([], user.enabled_protocols)
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, user))

def test_dm_no_yes_sets_enabled_protocols(self):
dm = {
'objectType': 'note',
'id': 'eefake:dm',
'actor': 'eefake:user',
'to': ['fa.brid.gy'],
'content': 'no',
}

user = self.make_user('eefake:user', cls=ExplicitEnableFake)
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, user))

# fake protocol isn't enabled yet, no DM should be a noop
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(dm))
user = user.key.get()
self.assertEqual([], user.enabled_protocols)

# yes DM should add to enabled_protocols
dm['id'] += '2'
dm['content'] = 'yes'
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(dm))
user = user.key.get()
self.assertEqual(['fake'], user.enabled_protocols)
self.assertTrue(ExplicitEnableFake.is_enabled_to(Fake, user))

# another yes DM should be a noop
dm['id'] += '3'
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(dm))
user = user.key.get()
self.assertEqual(['fake'], user.enabled_protocols)

# block should remove from enabled_protocols
dm['id'] += '4'
dm['content'] = ' \n NO '
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(dm))
user = user.key.get()
self.assertEqual([], user.enabled_protocols)
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, user))

def test_receive_task_handler(self):
note = {
'id': 'fake:post',
Expand Down

0 comments on commit 1686a2b

Please sign in to comment.