From 37c781a2df62fa0034b397310023e5e2de73b4c0 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Thu, 8 Aug 2024 11:42:28 -0700 Subject: [PATCH] ATProto.send: handle DMs, translate and send as chat messages for #1024, #966, etc --- atproto.py | 40 +++++++++++++++++++++---------------- tests/test_atproto.py | 46 +++++++++++++++++++++++++------------------ 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/atproto.py b/atproto.py index d7abb6c..ca1ec9a 100644 --- a/atproto.py +++ b/atproto.py @@ -461,6 +461,10 @@ class ATProto(User, Protocol): Doesn't deliver anywhere externally! Relays will receive this record through ``subscribeRepos`` and then deliver it to AppView(s), which will notify recipients as necessary. + + Exceptions: + * ``flag``s are translated to ``createReport`` to the mod service + * DMs are translated to ``sendMessage`` to the chat service """ if util.domain_from_link(url) not in DOMAINS: logger.info(f'Target PDS {url} is not us') @@ -527,6 +531,7 @@ class ATProto(User, Protocol): # * delete actor => tombstone repo # * flag => send report to mod service # * stop-following => delete follow record (prepared above) + # * dm => chat message verb = obj.as1.get('verb') if verb == 'delete': atp_base_id = (base_id if ATProto.owns_id(base_id) @@ -537,7 +542,11 @@ class ATProto(User, Protocol): arroba.server.storage.tombstone_repo(repo) return True - elif verb == 'flag': + if not record: + # _convert already logged + return False + + if verb == 'flag': logger.info(f'flag => createReport with {record}') return to_cls.create_report(record, from_user=user) @@ -546,11 +555,13 @@ class ATProto(User, Protocol): assert base_obj and base_obj.type == 'follow', base_obj verb = 'delete' - # write commit - if not record: - # _convert already logged - return False + elif as1.is_dm(obj.as1): + # is_dm checked that `to` has one elem + to_id = as1.get_ids(base_obj_as1, 'to')[0] + assert to_id.startswith('did:'), to_id + return ATProto.send_chat(record, from_repo=repo, to_did=to_id) + # write commit type = record['$type'] lex_type = LEXICONS[type]['type'] assert lex_type == 'record', f"Can't store {type} object of type {lex_type}" @@ -835,12 +846,14 @@ class ATProto(User, Protocol): logger.info(f'Created report on {mod_host}: {json_dumps(output)}') return True - def send_chat(self, msg, from_user): + @classmethod + def send_chat(cls, msg, from_repo, to_did): """Sends a chat message to this user. Args: msg (dict): ``chat.bsky.convo.defs#messageInput`` - from_user (models.User) + from_repo (arroba.repo.Repo) + to_did (str) Returns: bool: True if the report was sent successfully, False if the flag's @@ -848,18 +861,11 @@ class ATProto(User, Protocol): """ assert msg['$type'] == 'chat.bsky.convo.defs#messageInput' - to_did = self.key.id() - from_did = from_user.get_copy(ATProto) - if not from_did or not from_user.is_enabled(ATProto): - return False - - repo = arroba.server.storage.load_repo(from_did) - chat_host = os.environ['CHAT_HOST'] token = service_jwt(host=chat_host, aud=os.environ['CHAT_DID'], - repo_did=from_did, - privkey=repo.signing_key) + repo_did=from_repo.did, + privkey=from_repo.signing_key) client = Client(f'https://{chat_host}', truncate=True, headers={ 'User-Agent': USER_AGENT, 'Authorization': f'Bearer {token}', @@ -876,5 +882,5 @@ class ATProto(User, Protocol): util.interpret_http_exception(e) return False - logger.info(f'Sent chat message from {from_user.handle} to {self.handle} {to_did}: {json_dumps(sent)}') + logger.info(f'Sent chat message from {from_repo.handle} to {to_did}: {json_dumps(sent)}') return True diff --git a/tests/test_atproto.py b/tests/test_atproto.py index bcca45c..16a4be7 100644 --- a/tests/test_atproto.py +++ b/tests/test_atproto.py @@ -1741,8 +1741,7 @@ Sed tortor neque, aliquet quis posuere aliquam […] 'Authorization': ANY, }) - # sendMessage - @patch('requests.post', return_value=requests_response({ + @patch('requests.post', return_value=requests_response({ # sendMessage 'id': 'chat456', 'rev': '22222222tef2d', 'sender': {'did': 'did:plc:user'}, @@ -1764,18 +1763,25 @@ Sed tortor neque, aliquet quis posuere aliquam […] }), requests_response(DID_DOC), ]) - def test_send_chat(self, mock_get, mock_post): + def test_send_dm_chat(self, mock_get, mock_post): user = self.make_user_and_repo() - alice = ATProto(id='did:plc:alice') - self.assertTrue(alice.send_chat({ - '$type': 'chat.bsky.convo.defs#messageInput', - 'text': 'hello world', - }, from_user=user)) + dm = Object(id='fake:dm', source_protocol='fake', our_as1={ + 'objectType': 'note', + 'actor': user.key.id(), + 'content': 'hello world', + 'to': ['did:plc:alice'], + }) + self.assertTrue(ATProto.send(dm, 'https://bsky.brid.gy/')) + headers = { + 'Content-Type': 'application/json', + 'User-Agent': common.USER_AGENT, + 'Authorization': ANY, + } mock_get.assert_any_call( 'https://chat.service.local/xrpc/chat.bsky.convo.getConvoForMembers?members=did%3Aplc%3Aalice', - json=None, data=None, headers=ANY) + json=None, data=None, headers=headers) mock_post.assert_called_with( 'https://chat.service.local/xrpc/chat.bsky.convo.sendMessage', json={ @@ -1783,12 +1789,12 @@ Sed tortor neque, aliquet quis posuere aliquam […] 'message': { '$type': 'chat.bsky.convo.defs#messageInput', 'text': 'hello world', + # unused + 'createdAt': '2022-01-02T03:04:05.000Z', + 'bridgyOriginalText': 'hello world', + 'bridgyOriginalUrl': 'fake:dm', }, - }, data=None, headers={ - 'Content-Type': 'application/json', - 'User-Agent': common.USER_AGENT, - 'Authorization': ANY, - }) + }, data=None, headers=headers) # getConvoForMembers @patch('requests.get', return_value=requests_response({ @@ -1797,12 +1803,14 @@ Sed tortor neque, aliquet quis posuere aliquam […] }, status=400)) def test_send_chat_recipient_disabled(self, mock_get): user = self.make_user_and_repo() - alice = ATProto(id='did:plc:alice') - self.assertFalse(alice.send_chat({ - '$type': 'chat.bsky.convo.defs#messageInput', - 'text': 'hello world', - }, from_user=user)) + dm = Object(id='fake:dm', source_protocol='fake', our_as1={ + 'objectType': 'note', + 'actor': user.key.id(), + 'content': 'hello world', + 'to': ['did:plc:alice'], + }) + self.assertFalse(ATProto.send(dm, 'https://bsky.brid.gy/')) mock_get.assert_any_call( 'https://chat.service.local/xrpc/chat.bsky.convo.getConvoForMembers?members=did%3Aplc%3Aalice',