Skip to content

Commit

Permalink
add block DM command
Browse files Browse the repository at this point in the history
for #1406
  • Loading branch information
snarfed committed Feb 1, 2025
1 parent 8147210 commit a033433
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 69 deletions.
138 changes: 86 additions & 52 deletions dms.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"""Protocol-independent code for sending and receiving DMs aka chat messages."""
"""Protocol-independent code for sending and receiving DMs aka chat messages.
TODO: make a command framework, abstract out arg parsing and handling, etc
"""
from datetime import timedelta
import logging

Expand All @@ -19,6 +22,7 @@
REQUESTS_LIMIT_USER = 10

COMMANDS = (
'block',
'did',
'help',
'no',
Expand Down Expand Up @@ -126,7 +130,7 @@ def receive(*, from_user, obj):
logger.debug(f' first token is bot mention, removing')
tokens = tokens[1:]

if not tokens:
if not tokens or len(tokens) > 2:
return r'¯\_(ツ)_/¯', 204

if tokens[0].lstrip('/') in COMMANDS:
Expand Down Expand Up @@ -184,61 +188,91 @@ def reply(text, type=None):
return reply(str(e))
return reply(f"Your username in {to_proto.PHRASE} has been set to {from_user.user_link(proto=to_proto, name=False, handle=True)}. It should appear soon!")

if cmd == 'block' and arg:
handle = arg
if not to_proto.owns_handle(handle) and handle.startswith('@'):
logging.info(f"doesn't look like a handle, trying without leading @")
handle = handle.removeprefix('@')

to_user = load_user(to_proto, arg)
if not to_user:
return reply(f"Couldn't find {to_proto.PHRASE} user {handle}")

block_id = f'{from_user.key.id()}#bridgy-fed-block-{util.now().isoformat()}'
obj = Object(id=block_id, our_as1={
'id': block_id,
'objectType': 'activity',
'verb': 'block',
'actor': from_user.key.id(),
'object': to_user.key.id(),
})
obj.put()
from_user.deliver(obj, from_user=from_user)
return reply(f"""OK, you're now blocking {to_user.user_link()} in {to_proto.PHRASE}.""")

# are they requesting a user?
if not cmd:
if not to_proto.owns_handle(arg) and arg.startswith('@'):
handle = arg
if not to_proto.owns_handle(handle) and handle.startswith('@'):
logging.info(f"doesn't look like a handle, trying without leading @")
arg = arg.removeprefix('@')

if to_proto.owns_handle(arg) is not False:
handle = arg
from_proto = from_user.__class__

try:
ids.translate_handle(handle=handle, from_=to_proto, to=from_user,
enhanced=False)
except ValueError as e:
logger.warning(e)
return reply(f"Sorry, Bridgy Fed doesn't yet support bridging handle {handle} from {to_proto.PHRASE} to {from_proto.PHRASE}.")

to_id = to_proto.handle_to_id(handle)
if not to_id:
return reply(f"Couldn't find {to_proto.PHRASE} user {handle}")

to_user = to_proto.get_or_create(to_id)
if not to_user:
return reply(f"Couldn't find {to_proto.PHRASE} user {handle}")

if not to_user.obj:
# doesn't exist
return reply(f"Couldn't find {to_proto.PHRASE} user {handle}")

elif to_user.is_enabled(from_proto):
# already bridged
return reply(f'{to_user.user_link(proto=from_proto)} is already bridged into {from_proto.PHRASE}.')

elif (models.DM(protocol=from_proto.LABEL, type='request_bridging')
in to_user.sent_dms):
# already requested
return reply(f"We've already sent {to_user.user_link()} a DM. Fingers crossed!")

# check and update rate limits
attempts_key = f'dm-user-requests-{from_user.LABEL}-{from_user.key.id()}'
# incr leaves existing expiration as is, doesn't change it
# https://stackoverflow.com/a/4084043/186123
attempts = memcache.memcache.incr(attempts_key, 1)
if not attempts:
memcache.memcache.add(
attempts_key, 1,
expire=int(REQUESTS_LIMIT_EXPIRE.total_seconds()))
elif attempts > REQUESTS_LIMIT_USER:
return reply(f"Sorry, you've hit your limit of {REQUESTS_LIMIT_USER} requests per day. Try again tomorrow!")

# send the DM request!
maybe_send(from_proto=from_proto, to_user=to_user, type='request_bridging', text=f"""\
handle = handle.removeprefix('@')

to_user = load_user(to_proto, handle)
if not to_user:
return reply(f"Couldn't find {to_proto.PHRASE} user {handle}")

from_proto = from_user.__class__
try:
ids.translate_handle(handle=handle, from_=to_proto, to=from_user,
enhanced=False)
except ValueError as e:
logger.warning(e)
return reply(f"Sorry, Bridgy Fed doesn't yet support bridging handle {handle} from {to_proto.PHRASE} to {from_proto.PHRASE}.")

if to_user.is_enabled(from_proto):
# already bridged
return reply(f'{to_user.user_link(proto=from_proto)} is already bridged into {from_proto.PHRASE}.')

elif (models.DM(protocol=from_proto.LABEL, type='request_bridging')
in to_user.sent_dms):
# already requested
return reply(f"We've already sent {to_user.user_link()} a DM. Fingers crossed!")

# check and update rate limits
attempts_key = f'dm-user-requests-{from_user.LABEL}-{from_user.key.id()}'
# incr leaves existing expiration as is, doesn't change it
# https://stackoverflow.com/a/4084043/186123
attempts = memcache.memcache.incr(attempts_key, 1)
if not attempts:
memcache.memcache.add(
attempts_key, 1,
expire=int(REQUESTS_LIMIT_EXPIRE.total_seconds()))
elif attempts > REQUESTS_LIMIT_USER:
return reply(f"Sorry, you've hit your limit of {REQUESTS_LIMIT_USER} requests per day. Try again tomorrow!")

# send the DM request!
maybe_send(from_proto=from_proto, to_user=to_user, type='request_bridging', text=f"""\
<p>Hi! {from_user.user_link(proto=to_proto, proto_fallback=True)} is using Bridgy Fed to bridge their account from {from_proto.PHRASE} into {to_proto.PHRASE}, and they'd like to follow you. You can bridge your account into {from_proto.PHRASE} by following this account. <a href="https://fed.brid.gy/docs">See the docs</a> for more information.
<p>If you do nothing, your account won't be bridged, and users on {from_proto.PHRASE} won't be able to see or interact with you.
<p>Bridgy Fed will only send you this message once.""")
return reply(f"Got it! We'll send {to_user.user_link()} a message and say that you hope they'll enable the bridge. Fingers crossed!")
return reply(f"Got it! We'll send {to_user.user_link()} a message and say that you hope they'll enable the bridge. Fingers crossed!")

error(f"Couldn't understand DM: {tokens}", status=304)


def load_user(proto, handle):
"""
Args:
proto (protocol.Protocol)
handle (str)
Returns:
models.User or None
"""
if proto.owns_handle(handle) is False:
return None

if id := proto.handle_to_id(handle):
if user := proto.get_or_create(id):
if user.obj:
return user
50 changes: 33 additions & 17 deletions tests/test_dms.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
**DM_BASE,
'content': ' other:handle:bob ',
}
ALICE_CONFIRMATION_CONTENT = """Got it! We'll send <a class="h-card u-author" rel="me" href="web:other:bob" title="other:handle:bob">other:handle:bob</a> a message and say that you hope they'll enable the bridge. Fingers crossed!"""
ALICE_REQUEST_CONFIRMATION_CONTENT = """Got it! We'll send <a class="h-card u-author" rel="me" href="web:other:bob" title="other:handle:bob">other:handle:bob</a> a message and say that you hope they'll enable the bridge. Fingers crossed!"""
ALICE_REQUEST_CONTENT = """\
<p>Hi! <a class="h-card u-author" rel="me" href="web:other:efake:alice" title="efake:handle:alice &middot; other:handle:efake:handle:alice"><span style="unicode-bidi: isolate">efake:handle:alice</span> &middot; other:handle:efake:handle:alice</a> is using Bridgy Fed to bridge their account from efake-phrase into other-phrase, and they'd like to follow you. You can bridge your account into efake-phrase by following this account. <a href="https://fed.brid.gy/docs">See the docs</a> for more information.
<p>If you do nothing, your account won't be bridged, and users on efake-phrase won't be able to see or interact with you.
Expand All @@ -36,6 +36,12 @@
}
ALICE_USERNAME_CONFIRMATION_CONTENT = 'Your username in other-phrase has been set to <a class="h-card u-author" rel="me" href="web:other:efake:alice" title="other:handle:efake:handle:alice">other:handle:efake:handle:alice</a>. It should appear soon!'

DM_EFAKE_ALICE_BLOCK_BOB = {
**DM_BASE,
'content': 'block other:handle:bob',
}
ALICE_BLOCK_CONFIRMATION_CONTENT = """OK, you're now blocking <a class="h-card u-author" rel="me" href="web:other:bob" title="other:handle:bob">other:handle:bob</a> in other-phrase."""


class DmsTest(TestCase):
def make_alice_bob(self):
Expand Down Expand Up @@ -137,7 +143,7 @@ def test_receive_empty_strip_mention_of_bot(self):
'content': '<a href="https://other.brid.gy/other.brid.gy">@other.brid.gy</a> ',
})
self.assertEqual((\\_(ツ)_/¯', 204), receive(from_user=alice, obj=obj))
self.assert_replied(OtherFake, alice, '?', ALICE_CONFIRMATION_CONTENT)
self.assert_replied(OtherFake, alice, '?', ALICE_REQUEST_CONFIRMATION_CONTENT)
self.assert_sent(ExplicitFake, bob, 'request_bridging',
ALICE_REQUEST_CONTENT)

Expand All @@ -148,12 +154,9 @@ def test_receive_unknown_text(self):

obj = Object(our_as1={
**DM_BASE,
'content': 'foo bar',
'content': 'foo bar baz',
})
with self.assertRaises(NotModified) as e:
receive(from_user=alice, obj=obj)

self.assertIn("Couldn't understand DM: ['foo', 'bar']", str(e.exception))
self.assertEqual((\\_(ツ)_/¯', 204), receive(from_user=alice, obj=obj))
self.assertEqual([], OtherFake.sent)
self.assertEqual([], Fake.sent)

Expand Down Expand Up @@ -221,7 +224,7 @@ def test_receive_prompt_sends_request_dm(self):
obj = Object(our_as1=DM_EFAKE_ALICE_REQUESTS_OTHER_BOB)
self.assertEqual(('OK', 200), receive(from_user=alice, obj=obj))

self.assert_replied(OtherFake, alice, '?', ALICE_CONFIRMATION_CONTENT)
self.assert_replied(OtherFake, alice, '?', ALICE_REQUEST_CONFIRMATION_CONTENT)
self.assert_sent(ExplicitFake, bob, 'request_bridging',
ALICE_REQUEST_CONTENT)

Expand All @@ -233,7 +236,7 @@ def test_receive_prompt_strips_leading_at_sign(self):
'content': '@other:handle:bob',
})
self.assertEqual(('OK', 200), receive(from_user=alice, obj=obj))
self.assert_replied(OtherFake, alice, '?', ALICE_CONFIRMATION_CONTENT)
self.assert_replied(OtherFake, alice, '?', ALICE_REQUEST_CONFIRMATION_CONTENT)
self.assert_sent(ExplicitFake, bob, 'request_bridging',
ALICE_REQUEST_CONTENT)

Expand All @@ -245,7 +248,7 @@ def test_receive_prompt_html_link(self):
'content': '<a href="http://bob">@other:handle:bob</a>',
})
self.assertEqual(('OK', 200), receive(from_user=alice, obj=obj))
self.assert_replied(OtherFake, alice, '?', ALICE_CONFIRMATION_CONTENT)
self.assert_replied(OtherFake, alice, '?', ALICE_REQUEST_CONFIRMATION_CONTENT)
self.assert_sent(ExplicitFake, bob, 'request_bridging',
ALICE_REQUEST_CONTENT)

Expand All @@ -257,7 +260,7 @@ def test_receive_prompt_strip_mention_of_bot(self):
'content': '<a href="https://other.brid.gy/other.brid.gy">@other.brid.gy</a> other:handle:bob',
})
self.assertEqual(('OK', 200), receive(from_user=alice, obj=obj))
self.assert_replied(OtherFake, alice, '?', ALICE_CONFIRMATION_CONTENT)
self.assert_replied(OtherFake, alice, '?', ALICE_REQUEST_CONFIRMATION_CONTENT)
self.assert_sent(ExplicitFake, bob, 'request_bridging',
ALICE_REQUEST_CONTENT)

Expand All @@ -270,7 +273,7 @@ def test_receive_prompt_fetch_user(self):

obj = Object(our_as1=DM_EFAKE_ALICE_REQUESTS_OTHER_BOB)
self.assertEqual(('OK', 200), receive(from_user=alice, obj=obj))
self.assert_replied(OtherFake, alice, '?', ALICE_CONFIRMATION_CONTENT)
self.assert_replied(OtherFake, alice, '?', ALICE_REQUEST_CONFIRMATION_CONTENT)
self.assert_sent(ExplicitFake, OtherFake(id='other:bob'),
'request_bridging', ALICE_REQUEST_CONTENT)
self.assertEqual(['other:bob'], OtherFake.fetched)
Expand Down Expand Up @@ -354,18 +357,16 @@ def test_receive_prompt_request_rate_limit(self):

def test_receive_prompt_wrong_protocol(self):
self.make_user(id='other.brid.gy', cls=Web)
user = self.make_user('fake:user', cls=Fake, obj_as1={'x': 'y'})

obj = Object(our_as1={
**DM_BASE,
'content': 'fake:eve',
})
with self.assertRaises(NotModified) as e:
receive(from_user=Fake(id='fake:user'), obj=obj)

self.assertIn("Couldn't understand DM: ['fake:eve']", str(e.exception))
self.assertEqual(('OK', 200), receive(from_user=user, obj=obj))
self.assertEqual([], ExplicitFake.sent)
self.assertEqual([], OtherFake.sent)
self.assertEqual([], Fake.sent)
self.assert_replied(Fake, user, '?', "Couldn't find user fake:eve on other-phrase")

@mock.patch('ids.translate_handle', side_effect=ValueError('nope'))
def test_receive_prompt_not_supported_in_target_protocol(self, _):
Expand Down Expand Up @@ -455,3 +456,18 @@ def test_receive_did_atproto(self):
self.assertEqual(('OK', 200), receive(from_user=alice, obj=obj))
self.assert_replied(ATProto, alice, '?',
'Your DID is <code>did:abc:123</code>')

def test_receive_block(self):
alice, bob = self.make_alice_bob()

obj = Object(our_as1=DM_EFAKE_ALICE_BLOCK_BOB)
self.assertEqual(('OK', 200), receive(from_user=alice, obj=obj))

self.assert_replied(OtherFake, alice, '?', ALICE_BLOCK_CONFIRMATION_CONTENT)
self.assertEqual([('other:bob:target', {
'objectType': 'activity',
'verb': 'block',
'id': 'efake:alice#bridgy-fed-block-2022-01-02T03:04:05+00:00',
'actor': 'efake:alice',
'object': 'other:bob',
})], OtherFake.sent)

0 comments on commit a033433

Please sign in to comment.