diff --git a/atproto.py b/atproto.py index ab1ae2be..a1707da7 100644 --- a/atproto.py +++ b/atproto.py @@ -834,3 +834,40 @@ def create_report(cls, input, from_user): output = client.com.atproto.moderation.createReport(input) logger.info(f'Created report on {mod_host}: {json_dumps(output)}') return True + + def send_chat(self, msg, from_user): + """Sends a chat message to this user. + + Args: + msg (dict): ``chat.bsky.convo.defs#messageInput`` + from_user (models.User) + + Returns: + bool: True if the report was sent successfully, False if the flag's + actor is not bridged into ATProto + """ + 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) + client = Client(f'https://{chat_host}', truncate=True, headers={ + 'User-Agent': USER_AGENT, + 'Authorization': f'Bearer {token}', + }) + convo = client.chat.bsky.convo.getConvoForMembers(members=[to_did]) + output = client.chat.bsky.convo.sendMessage({ + 'convoId': convo['convo']['id'], + 'message': msg, + }) + logger.info(f'Sent chat message from {from_user.handle} to {self.handle} {to_did}: {json_dumps(output)}') + return True diff --git a/router.yaml b/router.yaml index 8ee2bdac..f81647d1 100644 --- a/router.yaml +++ b/router.yaml @@ -22,6 +22,9 @@ env_variables: BGS_HOST: bsky.network MOD_SERVICE_HOST: mod.bsky.app MOD_SERVICE_DID: did:plc:ar7c4by46qjdydhdevvrndac + # https://bsky.app/profile/gargaj.umlaut.hu/post/3kxsvpqiuln26 + CHAT_HOST: api.bsky.chat + CHAT_DID: did:web:api.bsky.chat manual_scaling: instances: 1 diff --git a/tests/test_atproto.py b/tests/test_atproto.py index ef9c6c73..e4600fdd 100644 --- a/tests/test_atproto.py +++ b/tests/test_atproto.py @@ -1741,6 +1741,55 @@ def test_send_flag_createReport(self, _, mock_post): 'Authorization': ANY, }) + # sendMessage + @patch('requests.post', return_value=requests_response({ + 'id': 'chat456', + 'rev': '22222222tef2d', + 'sender': {'did': 'did:plc:user'}, + 'text': 'hello world', + })) + @patch('requests.get', side_effect=[ + requests_response({ # getConvoForMembers + 'convo': { + 'id': 'convo123', + 'rev': '22222222fuozt', + 'members': [{ + 'did': 'did:plc:alice', + 'handle': 'alice.bsky.social', + }, { + 'did': 'did:plc:user', + 'handle': 'handull', + }], + }, + }), + requests_response(DID_DOC), + ]) + def test_send_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)) + + mock_get.assert_any_call( + 'https://chat.service.local/xrpc/chat.bsky.convo.getConvoForMembers?members=did%3Aplc%3Aalice', + json=None, data=None, headers=ANY) + mock_post.assert_called_with( + 'https://chat.service.local/xrpc/chat.bsky.convo.sendMessage', + json={ + 'convoId': 'convo123', + 'message': { + '$type': 'chat.bsky.convo.defs#messageInput', + 'text': 'hello world', + }, + }, data=None, headers={ + 'Content-Type': 'application/json', + 'User-Agent': common.USER_AGENT, + 'Authorization': ANY, + }) + def test_datastore_client_get_record_datastore_object(self): self.make_user_and_repo() post = { diff --git a/tests/testutil.py b/tests/testutil.py index 185d8a40..765b7dce 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -314,6 +314,8 @@ def setUp(self): 'PLC_HOST': 'plc.local', 'MOD_SERVICE_HOST': 'mod.service.local', 'MOD_SERVICE_DID': 'did:mod-service', + 'CHAT_HOST': 'chat.service.local', + 'CHAT_DID': 'did:chat-service', }) atproto.appview.address = 'https://appview.local'