From 68700d4199ca83df274b275ab1ea0f096cbe7e08 Mon Sep 17 00:00:00 2001 From: Felix Thielen Date: Sun, 5 Jun 2022 21:16:16 +0200 Subject: [PATCH 1/8] implement get_attachment() --- signalbot/api.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/signalbot/api.py b/signalbot/api.py index 0746b4d..e65af39 100644 --- a/signalbot/api.py +++ b/signalbot/api.py @@ -1,3 +1,5 @@ +import base64 + import aiohttp import websockets @@ -136,7 +138,28 @@ async def get_groups(self): aiohttp.ClientError, aiohttp.http_exceptions.HttpProcessingError, ): - raise GroupsError + raise GroupsError + + async def get_attachment(self, attachment_id: str) -> str: + uri = f"{self._attachment_rest_uri()}/{attachment_id}" + try: + async with aiohttp.ClientSession() as session: + resp = await session.get(uri) + resp.raise_for_status() + content = await resp.content.read() + except ( + aiohttp.ClientError, + aiohttp.http_exceptions.HttpProcessingError, + ): + raise GetAttachmentError + + base64_bytes = base64.b64encode(content) + base64_string = str(base64_bytes, encoding="utf-8") + + return base64_string + + def _attachment_rest_uri(self): + return f"http://{self.signal_service}/v1/attachments" def _receive_ws_uri(self): return f"ws://{self.signal_service}/v1/receive/{self.phone_number}" @@ -180,3 +203,7 @@ class ReactionError(Exception): class GroupsError(Exception): pass + + +class GetAttachmentError(Exception): + pass From 35220906f7ca20900af4c8f7ca7ffe8f56c04d1d Mon Sep 17 00:00:00 2001 From: Felix Thielen Date: Sun, 5 Jun 2022 21:18:13 +0200 Subject: [PATCH 2/8] implement base64_attachments: Message.parse() takes `signal: SignalAPI` instance as parameter, which allows us to call its new get_attachment() method. As a result, the Message.parse() needs to be `async` now. --- signalbot/bot.py | 2 +- signalbot/message.py | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/signalbot/bot.py b/signalbot/bot.py index ab806fd..691d52d 100644 --- a/signalbot/bot.py +++ b/signalbot/bot.py @@ -325,7 +325,7 @@ async def _produce(self, name: int) -> None: logging.info(f"[Raw Message] {raw_message}") try: - message = Message.parse(raw_message) + message = await Message.parse(self._signal, raw_message) except UnknownMessageFormatError: continue diff --git a/signalbot/message.py b/signalbot/message.py index 3aa7b36..0f37e2c 100644 --- a/signalbot/message.py +++ b/signalbot/message.py @@ -2,6 +2,9 @@ from enum import Enum +from signalbot.api import SignalAPI + + class MessageType(Enum): SYNC_MESSAGE = 1 DATA_MESSAGE = 2 @@ -56,7 +59,7 @@ def is_group(self) -> bool: return bool(self.group) @classmethod - def parse(cls, raw_message: str): + async def parse(cls, signal: SignalAPI, raw_message: str): try: raw_message = json.loads(raw_message) except Exception: @@ -82,6 +85,7 @@ def parse(cls, raw_message: str): mentions = cls._parse_mentions( raw_message["envelope"]["syncMessage"]["sentMessage"] ) + base64_attachments = None # Option 2: dataMessage elif "dataMessage" in raw_message["envelope"]: @@ -90,13 +94,13 @@ def parse(cls, raw_message: str): group = cls._parse_group_information(raw_message["envelope"]["dataMessage"]) reaction = cls._parse_reaction(raw_message["envelope"]["dataMessage"]) mentions = cls._parse_mentions(raw_message["envelope"]["dataMessage"]) + base64_attachments = await cls._parse_attachments( + signal, raw_message["envelope"]["dataMessage"] + ) else: raise UnknownMessageFormatError - # TODO: base64_attachments - base64_attachments = [] - return cls( source, timestamp, @@ -109,6 +113,17 @@ def parse(cls, raw_message: str): raw_message, ) + @classmethod + async def _parse_attachments(cls, signal: SignalAPI, data_message: dict) -> str: + + if "attachments" not in data_message: + return [] + + return [ + await signal.get_attachment(attachment["id"]) + for attachment in data_message["attachments"] + ] + @classmethod def _parse_sync_message(cls, sync_message: dict) -> str: try: From 737aacfc112bb3d8165cba4bea00d4e2cf36f425 Mon Sep 17 00:00:00 2001 From: Felix Thielen Date: Sun, 5 Jun 2022 21:23:22 +0200 Subject: [PATCH 3/8] make all `Message.parse()` calls asynchronous --- tests/test_message.py | 50 ++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/tests/test_message.py b/tests/test_message.py index ced41af..278f3b8 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -14,54 +14,56 @@ class TestMessage(unittest.TestCase): expected_group = "" # Own Message - def test_parse_source_own_message(self): - message = Message.parse(TestMessage.raw_sync_message) + async def test_parse_source_own_message(self): + message = await Message.parse(self.signal_api, TestMessage.raw_sync_message) self.assertEqual(message.timestamp, TestMessage.expected_timestamp) - def test_parse_timestamp_own_message(self): - message = Message.parse(TestMessage.raw_sync_message) + async def test_parse_timestamp_own_message(self): + message = await Message.parse(self.signal_api, TestMessage.raw_sync_message) self.assertEqual(message.source, TestMessage.expected_source) - def test_parse_type_own_message(self): - message = Message.parse(TestMessage.raw_sync_message) + async def test_parse_type_own_message(self): + message = await Message.parse(self.signal_api, TestMessage.raw_sync_message) self.assertEqual(message.type, MessageType.SYNC_MESSAGE) - def test_parse_text_own_message(self): - message = Message.parse(TestMessage.raw_sync_message) + async def test_parse_text_own_message(self): + message = await Message.parse(self.signal_api, TestMessage.raw_sync_message) self.assertEqual(message.text, TestMessage.expected_text) - def test_parse_group_own_message(self): - message = Message.parse(TestMessage.raw_sync_message) + async def test_parse_group_own_message(self): + message = await Message.parse(self.signal_api, TestMessage.raw_sync_message) self.assertEqual(message.group, TestMessage.expected_group) # Foreign Messages - def test_parse_source_foreign_message(self): - message = Message.parse(TestMessage.raw_data_message) + async def test_parse_source_foreign_message(self): + message = await Message.parse(self.signal_api, TestMessage.raw_data_message) self.assertEqual(message.timestamp, TestMessage.expected_timestamp) - def test_parse_timestamp_foreign_message(self): - message = Message.parse(TestMessage.raw_data_message) + async def test_parse_timestamp_foreign_message(self): + message = await Message.parse(self.signal_api, TestMessage.raw_data_message) self.assertEqual(message.source, TestMessage.expected_source) - def test_parse_type_foreign_message(self): - message = Message.parse(TestMessage.raw_data_message) + async def test_parse_type_foreign_message(self): + message = await Message.parse(self.signal_api, TestMessage.raw_data_message) self.assertEqual(message.type, MessageType.DATA_MESSAGE) - def test_parse_text_foreign_message(self): - message = Message.parse(TestMessage.raw_data_message) + async def test_parse_text_foreign_message(self): + message = await Message.parse(self.signal_api, TestMessage.raw_data_message) self.assertEqual(message.text, TestMessage.expected_text) - def test_parse_group_foreign_message(self): - message = Message.parse(TestMessage.raw_data_message) + async def test_parse_group_foreign_message(self): + message = await Message.parse(self.signal_api, TestMessage.raw_data_message) self.assertEqual(message.group, TestMessage.expected_group) - def test_read_reaction(self): - message = Message.parse(TestMessage.raw_reaction_message) + async def test_read_reaction(self): + message = await Message.parse(self.signal_api, TestMessage.raw_reaction_message) self.assertEqual(message.reaction, "👍") # User Chats - def test_parse_user_chat_message(self): - message = Message.parse(TestMessage.raw_user_chat_message) + async def test_parse_user_chat_message(self): + message = await Message.parse( + self.signal_api, TestMessage.raw_user_chat_message + ) self.assertEqual(message.source, TestMessage.expected_source) self.assertEqual(message.text, TestMessage.expected_text) self.assertEqual(message.timestamp, TestMessage.expected_timestamp) From cbea06f07b119ac21b899e4431c821f2992029d8 Mon Sep 17 00:00:00 2001 From: Felix Thielen Date: Sun, 5 Jun 2022 21:24:31 +0200 Subject: [PATCH 4/8] add a test case for messages with attachments --- tests/test_message.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_message.py b/tests/test_message.py index 278f3b8..40d6627 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -1,5 +1,9 @@ +import base64 import unittest +from unittest.mock import AsyncMock, patch + from signalbot import Message, MessageType +from signalbot.api import SignalAPI class TestMessage(unittest.TestCase): @@ -7,12 +11,25 @@ class TestMessage(unittest.TestCase): raw_data_message = '{"envelope":{"source":"+490123456789","sourceNumber":"+490123456789","sourceUuid":"","sourceName":"","sourceDevice":1,"timestamp":1632576001632,"dataMessage":{"timestamp":1632576001632,"message":"Uhrzeit","expiresInSeconds":0,"viewOnce":false,"mentions":[],"attachments":[],"contacts":[],"groupInfo":{"groupId":"","type":"DELIVER"}}}}' # noqa raw_reaction_message = '{"envelope":{"source":"","sourceNumber":"","sourceUuid":"","sourceName":"","sourceDevice":1,"timestamp":1632576001632,"syncMessage":{"sentMessage":{"timestamp":1632576001632,"message":null,"expiresInSeconds":0,"viewOnce":false,"reaction":{"emoji":"👍","targetAuthor":"","targetAuthorNumber":"","targetAuthorUuid":"","targetSentTimestamp":1632576001632,"isRemove":false},"mentions":[],"attachments":[],"contacts":[],"groupInfo":{"groupId":"","type":"DELIVER"},"destination":null,"destinationNumber":null,"destinationUuid":null}}}}' # noqa raw_user_chat_message = '{"envelope":{"source":"+490123456789","sourceNumber":"+490123456789","sourceUuid":"","sourceName":"","sourceDevice":1,"timestamp":1632576001632,"dataMessage":{"timestamp":1632576001632,"message":"Uhrzeit","expiresInSeconds":0,"viewOnce":false}},"account":"+49987654321","subscription":0}' # noqa + raw_attachment_message = '{"envelope":{"source":"+490123456789","sourceNumber":"+490123456789","sourceUuid":"","sourceName":"","sourceDevice":1,"timestamp":1632576001632,"dataMessage":{"timestamp":1632576001632,"message":"Uhrzeit","expiresInSeconds":0,"viewOnce":false, "attachments": [{"contentType": "image/png", "filename": "image.png", "id": "4296180834490578536","size": 12005}]}},"account":"+49987654321","subscription":0}' # noqa expected_source = "+490123456789" expected_timestamp = 1632576001632 expected_text = "Uhrzeit" expected_group = "" + signal_service = "127.0.0.1:8080" + phone_number = "+49123456789" + + group_id = "group_id1" + group_secret = "group.group_secret1" + groups = {group_id: group_secret} + + def setUp(self): + self.signal_api = SignalAPI( + TestMessage.signal_service, TestMessage.phone_number + ) + # Own Message async def test_parse_source_own_message(self): message = await Message.parse(self.signal_api, TestMessage.raw_sync_message) @@ -59,6 +76,17 @@ async def test_read_reaction(self): message = await Message.parse(self.signal_api, TestMessage.raw_reaction_message) self.assertEqual(message.reaction, "👍") + @patch("aiohttp.ClientSession.get", new_callable=AsyncMock) + async def test_attachments(self, mock): + mock.return_value = b"test" + expected_base64_bytes = base64.b64encode(mock.return_value) + expected_base64_str = str(expected_base64_bytes, encoding="utf-8") + + message = await Message.parse( + self.signal_api, TestMessage.raw_attachment_message + ) + self.assertEqual(message.base64_attachments, [expected_base64_str]) + # User Chats async def test_parse_user_chat_message(self): message = await Message.parse( From 24c402c2de5b738db7ee632b6a757d1c1390440b Mon Sep 17 00:00:00 2001 From: Era Dorta Date: Sun, 6 Oct 2024 18:56:08 +0200 Subject: [PATCH 5/8] Fix unit testing not running --- tests/test_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_message.py b/tests/test_message.py index 40d6627..416b7f0 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -6,7 +6,7 @@ from signalbot.api import SignalAPI -class TestMessage(unittest.TestCase): +class TestMessage(unittest.IsolatedAsyncioTestCase): raw_sync_message = '{"envelope":{"source":"+490123456789","sourceNumber":"+490123456789","sourceUuid":"","sourceName":"","sourceDevice":1,"timestamp":1632576001632,"syncMessage":{"sentMessage":{"timestamp":1632576001632,"message":"Uhrzeit","expiresInSeconds":0,"viewOnce":false,"mentions":[],"attachments":[],"contacts":[],"groupInfo":{"groupId":"","type":"DELIVER"},"destination":null,"destinationNumber":null,"destinationUuid":null}}}}' # noqa raw_data_message = '{"envelope":{"source":"+490123456789","sourceNumber":"+490123456789","sourceUuid":"","sourceName":"","sourceDevice":1,"timestamp":1632576001632,"dataMessage":{"timestamp":1632576001632,"message":"Uhrzeit","expiresInSeconds":0,"viewOnce":false,"mentions":[],"attachments":[],"contacts":[],"groupInfo":{"groupId":"","type":"DELIVER"}}}}' # noqa raw_reaction_message = '{"envelope":{"source":"","sourceNumber":"","sourceUuid":"","sourceName":"","sourceDevice":1,"timestamp":1632576001632,"syncMessage":{"sentMessage":{"timestamp":1632576001632,"message":null,"expiresInSeconds":0,"viewOnce":false,"reaction":{"emoji":"👍","targetAuthor":"","targetAuthorNumber":"","targetAuthorUuid":"","targetSentTimestamp":1632576001632,"isRemove":false},"mentions":[],"attachments":[],"contacts":[],"groupInfo":{"groupId":"","type":"DELIVER"},"destination":null,"destinationNumber":null,"destinationUuid":null}}}}' # noqa From acc08af19b440a4c11af37a373e6a454df361759 Mon Sep 17 00:00:00 2001 From: Era Dorta Date: Sun, 6 Oct 2024 19:48:28 +0200 Subject: [PATCH 6/8] Fix test_attachments test --- tests/test_message.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/test_message.py b/tests/test_message.py index 416b7f0..2ef41a2 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -1,9 +1,10 @@ import base64 import unittest -from unittest.mock import AsyncMock, patch - +from unittest.mock import AsyncMock, patch, Mock +import aiohttp from signalbot import Message, MessageType from signalbot.api import SignalAPI +from signalbot.utils import ChatTestCase, SendMessagesMock, ReceiveMessagesMock class TestMessage(unittest.IsolatedAsyncioTestCase): @@ -77,9 +78,16 @@ async def test_read_reaction(self): self.assertEqual(message.reaction, "👍") @patch("aiohttp.ClientSession.get", new_callable=AsyncMock) - async def test_attachments(self, mock): - mock.return_value = b"test" - expected_base64_bytes = base64.b64encode(mock.return_value) + async def test_attachments(self, mock_get): + attachment_bytes_str = b"test" + + mock_response = AsyncMock(spec=aiohttp.ClientResponse) + mock_response.raise_for_status = Mock() + mock_response.content.read = AsyncMock(return_value=attachment_bytes_str) + + mock_get.return_value = mock_response + + expected_base64_bytes = base64.b64encode(attachment_bytes_str) expected_base64_str = str(expected_base64_bytes, encoding="utf-8") message = await Message.parse( From e152306ded372870f4e8536f01cf88f08db5f334 Mon Sep 17 00:00:00 2001 From: Era Dorta Date: Sun, 6 Oct 2024 19:49:35 +0200 Subject: [PATCH 7/8] Fix formatting --- signalbot/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/signalbot/api.py b/signalbot/api.py index e65af39..b295b80 100644 --- a/signalbot/api.py +++ b/signalbot/api.py @@ -138,8 +138,8 @@ async def get_groups(self): aiohttp.ClientError, aiohttp.http_exceptions.HttpProcessingError, ): - raise GroupsError - + raise GroupsError + async def get_attachment(self, attachment_id: str) -> str: uri = f"{self._attachment_rest_uri()}/{attachment_id}" try: From ebfabb3326b31fa6c8b9820db28a66e6ea53c137 Mon Sep 17 00:00:00 2001 From: Era Dorta Date: Fri, 20 Dec 2024 15:58:46 +0100 Subject: [PATCH 8/8] Add test for the attachments URI --- tests/test_api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index b17d8d6..9ad7845 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -59,6 +59,11 @@ def test_send_uri(self): actual_uri = self.signal_api._send_rest_uri() self.assertEqual(actual_uri, expected_uri) + def test_attachment_rest_uri(self): + expected_uri = f"http://{self.signal_service}/v1/attachments" + actual_uri = self.signal_api._attachment_rest_uri() + self.assertEqual(actual_uri, expected_uri) + if __name__ == "__main__": unittest.main()