From e28bd05ef1de5e89464d200517785dfd9f9be5a6 Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Tue, 30 Jun 2026 05:02:31 +0800 Subject: [PATCH] fix(simplex): accept non-audio file transfers via /freceive The deferred-file guard only triggered for audio extensions, silently dropping images, PDFs, and other documents that hadn't finished downloading via XFTP. Remove the audio-only restriction so all pending file transfers are accepted and deferred until rcvFileComplete. Fixes #55180 --- plugins/platforms/simplex/adapter.py | 9 ++-- tests/gateway/test_simplex_plugin.py | 76 ++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/plugins/platforms/simplex/adapter.py b/plugins/platforms/simplex/adapter.py index ae4c6be34b64..931f2deefbbb 100644 --- a/plugins/platforms/simplex/adapter.py +++ b/plugins/platforms/simplex/adapter.py @@ -575,11 +575,12 @@ async def _handle_chat_item(self, chat_item: dict) -> None: if not ext and file_name: ext = Path(file_name).suffix.lower() - # Voice notes typically arrive before the file finishes - # downloading. Defer the message until rcvFileComplete fires. - if not file_path and _is_audio_ext(ext) and file_id is not None: + # File not yet downloaded — defer the message until + # rcvFileComplete fires. Applies to all file types (images, + # documents, audio) transferred via XFTP, not just voice notes. + if not file_path and file_id is not None: logger.info( - "SimpleX: voice file %d not yet received, accepting transfer", + "SimpleX: file %d not yet received, accepting transfer", file_id, ) self._pending_file_transfers[file_id] = chat_item diff --git a/tests/gateway/test_simplex_plugin.py b/tests/gateway/test_simplex_plugin.py index 66242438f8ef..7dc6eddc709b 100644 --- a/tests/gateway/test_simplex_plugin.py +++ b/tests/gateway/test_simplex_plugin.py @@ -467,3 +467,79 @@ async def _capture(event): assert dispatched, "_handle_chat_item did not dispatch any event" assert dispatched[0].message_type == MessageType.PHOTO + + +def _make_pending_file_chat_item(file_name: str, file_id: int = 42) -> dict: + """Minimal direct-chat rcvMsgContent item carrying a file NOT yet downloaded. + + The ``fileSource`` has no ``filePath`` key, simulating an XFTP transfer + that is still in progress (the daemon hasn't written the file to disk). + """ + return { + "chatInfo": { + "type": "direct", + "contact": {"contactId": 42, "localDisplayName": "tester"}, + }, + "chatItem": { + "chatDir": {"type": "directRcv"}, + "meta": {"itemTs": "2026-01-01T00:00:00Z"}, + "content": { + "type": "rcvMsgContent", + "msgContent": {"type": "file", "text": "here you go"}, + }, + "file": { + "fileId": file_id, + "fileName": file_name, + "fileSource": {}, # no filePath → transfer not complete + }, + }, + } + + +@pytest.mark.asyncio +async def test_pending_image_file_accepts_transfer(): + """An image that hasn't finished downloading must be accepted via + /freceive and deferred until rcvFileComplete — not silently dropped.""" + from gateway.config import PlatformConfig + + cfg = PlatformConfig(enabled=True, extra={"ws_url": "ws://localhost:5225"}) + adapter = SimplexAdapter(cfg) + sent_commands: list = [] + adapter._send_fire_and_forget = lambda cmd: ( + sent_commands.append(cmd) or __import__("asyncio").sleep(0) + ) + + chat_item = _make_pending_file_chat_item("photo.jpg", file_id=99) + await adapter._handle_chat_item(chat_item) + + # The adapter must accept the transfer … + assert any("/freceive 99" in c for c in sent_commands), ( + f"Expected /freceive 99, got: {sent_commands}" + ) + # … and defer the chat item until rcvFileComplete. + assert 99 in adapter._pending_file_transfers + # No message dispatched yet (deferred). + assert not hasattr(adapter, "_dispatched") or not getattr(adapter, "_dispatched", None) + + +@pytest.mark.asyncio +async def test_pending_document_file_accepts_transfer(): + """A PDF/document that hasn't finished downloading must also be accepted + via /freceive — the fix for #55180 removes the audio-only guard.""" + from gateway.config import PlatformConfig + + cfg = PlatformConfig(enabled=True, extra={"ws_url": "ws://localhost:5225"}) + adapter = SimplexAdapter(cfg) + sent_commands: list = [] + adapter._send_fire_and_forget = lambda cmd: ( + sent_commands.append(cmd) or __import__("asyncio").sleep(0) + ) + + chat_item = _make_pending_file_chat_item("report.pdf", file_id=77) + await adapter._handle_chat_item(chat_item) + + assert any("/freceive 77" in c for c in sent_commands), ( + f"Expected /freceive 77, got: {sent_commands}" + ) + assert 77 in adapter._pending_file_transfers +