Skip to content

Commit f8dc961

Browse files
authored
feat: Merge pull request #67 from Era-Dorta/base64-attachments
Enable getting attachments when receiving a message.
2 parents cdde4e3 + ebfabb3 commit f8dc961

File tree

5 files changed

+115
-30
lines changed

5 files changed

+115
-30
lines changed

signalbot/api.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import base64
2+
13
import aiohttp
24
import websockets
35
from typing import Any
@@ -139,6 +141,27 @@ async def get_groups(self):
139141
):
140142
raise GroupsError
141143

144+
async def get_attachment(self, attachment_id: str) -> str:
145+
uri = f"{self._attachment_rest_uri()}/{attachment_id}"
146+
try:
147+
async with aiohttp.ClientSession() as session:
148+
resp = await session.get(uri)
149+
resp.raise_for_status()
150+
content = await resp.content.read()
151+
except (
152+
aiohttp.ClientError,
153+
aiohttp.http_exceptions.HttpProcessingError,
154+
):
155+
raise GetAttachmentError
156+
157+
base64_bytes = base64.b64encode(content)
158+
base64_string = str(base64_bytes, encoding="utf-8")
159+
160+
return base64_string
161+
162+
def _attachment_rest_uri(self):
163+
return f"http://{self.signal_service}/v1/attachments"
164+
142165
def _receive_ws_uri(self):
143166
return f"ws://{self.signal_service}/v1/receive/{self.phone_number}"
144167

@@ -181,3 +204,7 @@ class ReactionError(Exception):
181204

182205
class GroupsError(Exception):
183206
pass
207+
208+
209+
class GetAttachmentError(Exception):
210+
pass

signalbot/bot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ async def _produce(self, name: int) -> None:
338338
logging.info(f"[Raw Message] {raw_message}")
339339

340340
try:
341-
message = Message.parse(raw_message)
341+
message = await Message.parse(self._signal, raw_message)
342342
except UnknownMessageFormatError:
343343
continue
344344

signalbot/message.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from typing import Optional
44

55

6+
from signalbot.api import SignalAPI
7+
8+
69
class MessageType(Enum):
710
SYNC_MESSAGE = 1
811
DATA_MESSAGE = 2
@@ -61,7 +64,7 @@ def is_group(self) -> bool:
6164
return bool(self.group)
6265

6366
@classmethod
64-
def parse(cls, raw_message: str):
67+
async def parse(cls, signal: SignalAPI, raw_message: str):
6568
try:
6669
raw_message = json.loads(raw_message)
6770
except Exception:
@@ -90,6 +93,7 @@ def parse(cls, raw_message: str):
9093
mentions = cls._parse_mentions(
9194
raw_message["envelope"]["syncMessage"]["sentMessage"]
9295
)
96+
base64_attachments = None
9397

9498
# Option 2: dataMessage
9599
elif "dataMessage" in raw_message["envelope"]:
@@ -98,13 +102,13 @@ def parse(cls, raw_message: str):
98102
group = cls._parse_group_information(raw_message["envelope"]["dataMessage"])
99103
reaction = cls._parse_reaction(raw_message["envelope"]["dataMessage"])
100104
mentions = cls._parse_mentions(raw_message["envelope"]["dataMessage"])
105+
base64_attachments = await cls._parse_attachments(
106+
signal, raw_message["envelope"]["dataMessage"]
107+
)
101108

102109
else:
103110
raise UnknownMessageFormatError
104111

105-
# TODO: base64_attachments
106-
base64_attachments = []
107-
108112
return cls(
109113
source,
110114
source_number,
@@ -119,6 +123,17 @@ def parse(cls, raw_message: str):
119123
raw_message,
120124
)
121125

126+
@classmethod
127+
async def _parse_attachments(cls, signal: SignalAPI, data_message: dict) -> str:
128+
129+
if "attachments" not in data_message:
130+
return []
131+
132+
return [
133+
await signal.get_attachment(attachment["id"])
134+
for attachment in data_message["attachments"]
135+
]
136+
122137
@classmethod
123138
def _parse_sync_message(cls, sync_message: dict) -> str:
124139
try:

tests/test_api.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ def test_send_uri(self):
5959
actual_uri = self.signal_api._send_rest_uri()
6060
self.assertEqual(actual_uri, expected_uri)
6161

62+
def test_attachment_rest_uri(self):
63+
expected_uri = f"http://{self.signal_service}/v1/attachments"
64+
actual_uri = self.signal_api._attachment_rest_uri()
65+
self.assertEqual(actual_uri, expected_uri)
66+
6267

6368
if __name__ == "__main__":
6469
unittest.main()

tests/test_message.py

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,105 @@
1+
import base64
12
import unittest
3+
from unittest.mock import AsyncMock, patch, Mock
4+
import aiohttp
25
from signalbot import Message, MessageType
6+
from signalbot.api import SignalAPI
7+
from signalbot.utils import ChatTestCase, SendMessagesMock, ReceiveMessagesMock
38

49

5-
class TestMessage(unittest.TestCase):
10+
class TestMessage(unittest.IsolatedAsyncioTestCase):
611
raw_sync_message = '{"envelope":{"source":"+490123456789","sourceNumber":"+490123456789","sourceUuid":"<uuid>","sourceName":"<name>","sourceDevice":1,"timestamp":1632576001632,"syncMessage":{"sentMessage":{"timestamp":1632576001632,"message":"Uhrzeit","expiresInSeconds":0,"viewOnce":false,"mentions":[],"attachments":[],"contacts":[],"groupInfo":{"groupId":"<groupid>","type":"DELIVER"},"destination":null,"destinationNumber":null,"destinationUuid":null}}}}' # noqa
712
raw_data_message = '{"envelope":{"source":"+490123456789","sourceNumber":"+490123456789","sourceUuid":"<uuid>","sourceName":"<name>","sourceDevice":1,"timestamp":1632576001632,"dataMessage":{"timestamp":1632576001632,"message":"Uhrzeit","expiresInSeconds":0,"viewOnce":false,"mentions":[],"attachments":[],"contacts":[],"groupInfo":{"groupId":"<groupid>","type":"DELIVER"}}}}' # noqa
813
raw_reaction_message = '{"envelope":{"source":"<source>","sourceNumber":"<source>","sourceUuid":"<uuid>","sourceName":"<name>","sourceDevice":1,"timestamp":1632576001632,"syncMessage":{"sentMessage":{"timestamp":1632576001632,"message":null,"expiresInSeconds":0,"viewOnce":false,"reaction":{"emoji":"👍","targetAuthor":"<target>","targetAuthorNumber":"<target>","targetAuthorUuid":"<uuid>","targetSentTimestamp":1632576001632,"isRemove":false},"mentions":[],"attachments":[],"contacts":[],"groupInfo":{"groupId":"<groupid>","type":"DELIVER"},"destination":null,"destinationNumber":null,"destinationUuid":null}}}}' # noqa
914
raw_user_chat_message = '{"envelope":{"source":"+490123456789","sourceNumber":"+490123456789","sourceUuid":"<uuid>","sourceName":"<name>","sourceDevice":1,"timestamp":1632576001632,"dataMessage":{"timestamp":1632576001632,"message":"Uhrzeit","expiresInSeconds":0,"viewOnce":false}},"account":"+49987654321","subscription":0}' # noqa
15+
raw_attachment_message = '{"envelope":{"source":"+490123456789","sourceNumber":"+490123456789","sourceUuid":"<uuid>","sourceName":"<name>","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
1016

1117
expected_source = "+490123456789"
1218
expected_timestamp = 1632576001632
1319
expected_text = "Uhrzeit"
1420
expected_group = "<groupid>"
1521

22+
signal_service = "127.0.0.1:8080"
23+
phone_number = "+49123456789"
24+
25+
group_id = "group_id1"
26+
group_secret = "group.group_secret1"
27+
groups = {group_id: group_secret}
28+
29+
def setUp(self):
30+
self.signal_api = SignalAPI(
31+
TestMessage.signal_service, TestMessage.phone_number
32+
)
33+
1634
# Own Message
17-
def test_parse_source_own_message(self):
18-
message = Message.parse(TestMessage.raw_sync_message)
35+
async def test_parse_source_own_message(self):
36+
message = await Message.parse(self.signal_api, TestMessage.raw_sync_message)
1937
self.assertEqual(message.timestamp, TestMessage.expected_timestamp)
2038

21-
def test_parse_timestamp_own_message(self):
22-
message = Message.parse(TestMessage.raw_sync_message)
39+
async def test_parse_timestamp_own_message(self):
40+
message = await Message.parse(self.signal_api, TestMessage.raw_sync_message)
2341
self.assertEqual(message.source, TestMessage.expected_source)
2442

25-
def test_parse_type_own_message(self):
26-
message = Message.parse(TestMessage.raw_sync_message)
43+
async def test_parse_type_own_message(self):
44+
message = await Message.parse(self.signal_api, TestMessage.raw_sync_message)
2745
self.assertEqual(message.type, MessageType.SYNC_MESSAGE)
2846

29-
def test_parse_text_own_message(self):
30-
message = Message.parse(TestMessage.raw_sync_message)
47+
async def test_parse_text_own_message(self):
48+
message = await Message.parse(self.signal_api, TestMessage.raw_sync_message)
3149
self.assertEqual(message.text, TestMessage.expected_text)
3250

33-
def test_parse_group_own_message(self):
34-
message = Message.parse(TestMessage.raw_sync_message)
51+
async def test_parse_group_own_message(self):
52+
message = await Message.parse(self.signal_api, TestMessage.raw_sync_message)
3553
self.assertEqual(message.group, TestMessage.expected_group)
3654

3755
# Foreign Messages
38-
def test_parse_source_foreign_message(self):
39-
message = Message.parse(TestMessage.raw_data_message)
56+
async def test_parse_source_foreign_message(self):
57+
message = await Message.parse(self.signal_api, TestMessage.raw_data_message)
4058
self.assertEqual(message.timestamp, TestMessage.expected_timestamp)
4159

42-
def test_parse_timestamp_foreign_message(self):
43-
message = Message.parse(TestMessage.raw_data_message)
60+
async def test_parse_timestamp_foreign_message(self):
61+
message = await Message.parse(self.signal_api, TestMessage.raw_data_message)
4462
self.assertEqual(message.source, TestMessage.expected_source)
4563

46-
def test_parse_type_foreign_message(self):
47-
message = Message.parse(TestMessage.raw_data_message)
64+
async def test_parse_type_foreign_message(self):
65+
message = await Message.parse(self.signal_api, TestMessage.raw_data_message)
4866
self.assertEqual(message.type, MessageType.DATA_MESSAGE)
4967

50-
def test_parse_text_foreign_message(self):
51-
message = Message.parse(TestMessage.raw_data_message)
68+
async def test_parse_text_foreign_message(self):
69+
message = await Message.parse(self.signal_api, TestMessage.raw_data_message)
5270
self.assertEqual(message.text, TestMessage.expected_text)
5371

54-
def test_parse_group_foreign_message(self):
55-
message = Message.parse(TestMessage.raw_data_message)
72+
async def test_parse_group_foreign_message(self):
73+
message = await Message.parse(self.signal_api, TestMessage.raw_data_message)
5674
self.assertEqual(message.group, TestMessage.expected_group)
5775

58-
def test_read_reaction(self):
59-
message = Message.parse(TestMessage.raw_reaction_message)
76+
async def test_read_reaction(self):
77+
message = await Message.parse(self.signal_api, TestMessage.raw_reaction_message)
6078
self.assertEqual(message.reaction, "👍")
6179

80+
@patch("aiohttp.ClientSession.get", new_callable=AsyncMock)
81+
async def test_attachments(self, mock_get):
82+
attachment_bytes_str = b"test"
83+
84+
mock_response = AsyncMock(spec=aiohttp.ClientResponse)
85+
mock_response.raise_for_status = Mock()
86+
mock_response.content.read = AsyncMock(return_value=attachment_bytes_str)
87+
88+
mock_get.return_value = mock_response
89+
90+
expected_base64_bytes = base64.b64encode(attachment_bytes_str)
91+
expected_base64_str = str(expected_base64_bytes, encoding="utf-8")
92+
93+
message = await Message.parse(
94+
self.signal_api, TestMessage.raw_attachment_message
95+
)
96+
self.assertEqual(message.base64_attachments, [expected_base64_str])
97+
6298
# User Chats
63-
def test_parse_user_chat_message(self):
64-
message = Message.parse(TestMessage.raw_user_chat_message)
99+
async def test_parse_user_chat_message(self):
100+
message = await Message.parse(
101+
self.signal_api, TestMessage.raw_user_chat_message
102+
)
65103
self.assertEqual(message.source, TestMessage.expected_source)
66104
self.assertEqual(message.text, TestMessage.expected_text)
67105
self.assertEqual(message.timestamp, TestMessage.expected_timestamp)

0 commit comments

Comments
 (0)