Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions gateway/platforms/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ async def _handle_envelope(self, envelope: dict) -> None:
logger.warning("Signal: attachment too large (%d bytes), skipping", att_size)
continue
try:
cached_path, ext = await self._fetch_attachment(att_id)
cached_path, ext = await self._fetch_attachment(att_id, att)
if cached_path:
# Use contentType from Signal if available, else map from extension
content_type = att.get("contentType") or _ext_to_mime(ext)
Expand Down Expand Up @@ -600,8 +600,6 @@ async def _handle_envelope(self, envelope: dict) -> None:
if media_types:
if any(mt.startswith("audio/") for mt in media_types):
msg_type = MessageType.VOICE
elif any(mt.startswith("image/") for mt in media_types):
msg_type = MessageType.PHOTO
elif any(mt.startswith("video/") for mt in media_types):
msg_type = MessageType.VIDEO
else:
Expand Down Expand Up @@ -703,7 +701,7 @@ async def _resolve_recipient(self, chat_id: str) -> str:
# Attachment Handling
# ------------------------------------------------------------------

async def _fetch_attachment(self, attachment_id: str) -> tuple:
async def _fetch_attachment(self, attachment_id: str, attachment_meta: Optional[dict] = None) -> tuple:
"""Fetch an attachment via JSON-RPC and cache it. Returns (path, ext)."""
result = await self._rpc("getAttachment", {
"account": self.account,
Expand All @@ -729,7 +727,12 @@ async def _fetch_attachment(self, attachment_id: str) -> tuple:
elif _is_audio_ext(ext):
path = cache_audio_from_bytes(raw_data, ext)
else:
path = cache_document_from_bytes(raw_data, ext)
filename = None
if isinstance(attachment_meta, dict):
raw_filename = attachment_meta.get("filename") or attachment_meta.get("name")
if raw_filename:
filename = Path(str(raw_filename)).name
path = cache_document_from_bytes(raw_data, filename or ext)

return path, ext

Expand Down
70 changes: 70 additions & 0 deletions tests/gateway/test_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,76 @@ async def test_fetch_attachment_handles_dict_response(self, monkeypatch):
assert path == "/tmp/test.pdf"
assert ext == ".pdf"

@pytest.mark.asyncio
async def test_fetch_attachment_preserves_signal_filename_for_documents(self, monkeypatch):
adapter = _make_signal_adapter(monkeypatch)

pdf_data = b"%PDF-1.4" + b"\x00" * 100
b64_data = base64.b64encode(pdf_data).decode()
adapter._rpc, _ = _stub_rpc({"data": b64_data})

with patch("gateway.platforms.signal.cache_document_from_bytes", return_value="/tmp/document.pdf") as cache_doc:
path, ext = await adapter._fetch_attachment("doc-456", {"filename": "document.pdf"})

assert path == "/tmp/document.pdf"
assert ext == ".pdf"
cache_doc.assert_called_once_with(pdf_data, "document.pdf")

@pytest.mark.asyncio
async def test_fetch_attachment_strips_signal_filename_path_components(self, monkeypatch):
adapter = _make_signal_adapter(monkeypatch)

pdf_data = b"%PDF-1.4" + b"\x00" * 100
b64_data = base64.b64encode(pdf_data).decode()
adapter._rpc, _ = _stub_rpc({"data": b64_data})

with patch("gateway.platforms.signal.cache_document_from_bytes", return_value="/tmp/passwd") as cache_doc:
await adapter._fetch_attachment("doc-456", {"filename": "../../../passwd"})

cache_doc.assert_called_once_with(pdf_data, "passwd")

@pytest.mark.asyncio
async def test_handle_envelope_marks_non_image_audio_attachment_as_document(self, monkeypatch):
from gateway.platforms.base import MessageType

adapter = _make_signal_adapter(monkeypatch)
captured = {}

async def fake_handle(event):
captured["event"] = event

adapter.handle_message = fake_handle

async def fake_fetch(attachment_id, attachment_meta=None):
assert attachment_id == "att-pdf"
assert attachment_meta["filename"] == "document.pdf"
return "/tmp/document.pdf", ".pdf"

adapter._fetch_attachment = fake_fetch

await adapter._handle_envelope({
"envelope": {
"sourceNumber": "+155****1111",
"sourceUuid": "uuid-sender",
"sourceName": "Tester",
"timestamp": 1000000000,
"dataMessage": {
"message": "see attached",
"attachments": [{
"id": "att-pdf",
"filename": "document.pdf",
"contentType": "application/pdf",
"size": 128,
}],
},
}
})

event = captured["event"]
assert event.message_type == MessageType.DOCUMENT
assert event.media_urls == ["/tmp/document.pdf"]
assert event.media_types == ["application/pdf"]


# ---------------------------------------------------------------------------
# Session Source
Expand Down