diff --git a/contributors/emails/kelsia014@gmail.com b/contributors/emails/kelsia014@gmail.com new file mode 100644 index 000000000000..09887302545b --- /dev/null +++ b/contributors/emails/kelsia014@gmail.com @@ -0,0 +1 @@ +kelsia14 diff --git a/plugins/platforms/photon/adapter.py b/plugins/platforms/photon/adapter.py index 8daf743e2bf0..245aab8b2a8e 100644 --- a/plugins/platforms/photon/adapter.py +++ b/plugins/platforms/photon/adapter.py @@ -726,6 +726,19 @@ class PhotonAdapter(BasePlatformAdapter): ) ctype = content.get("type") + if ctype == "text": + raw_text = content.get("text") or "" + # iMessage emits U+FFFC OBJECT REPLACEMENT CHARACTER as a transient + # placeholder for some media bubbles (notably voice notes). Photon + # can then deliver the real attachment/voice event immediately + # afterwards with a different message id. If we dispatch the + # placeholder as a standalone text turn, the subsequent media event + # arrives while that turn is active and the gateway sends a bogus + # "Interrupting current task" busy ack. Drop placeholder-only text + # at the platform boundary; the real media event carries the bytes. + if raw_text.strip() == "\ufffc": + logger.debug("[photon] ignoring iMessage object-placeholder text event") + return if ctype == "reaction": # Route only tapbacks on messages WE sent — those are implicitly # addressed to the bot (feishu precedent: synthetic text event). diff --git a/tests/plugins/platforms/photon/test_inbound.py b/tests/plugins/platforms/photon/test_inbound.py index 3531e6f56882..66269ed82844 100644 --- a/tests/plugins/platforms/photon/test_inbound.py +++ b/tests/plugins/platforms/photon/test_inbound.py @@ -67,6 +67,24 @@ async def test_dispatch_text_dm(monkeypatch: pytest.MonkeyPatch) -> None: assert src.user_id == "+15551234567" +@pytest.mark.asyncio +async def test_dispatch_ignores_imessage_object_placeholder_text( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Voice notes may arrive as U+FFFC placeholder text followed by media. + + Dispatching the placeholder starts a bogus text turn; the real voice event + then lands during that turn and triggers the gateway's busy interrupt ack. + Drop placeholder-only text so only the actual voice/attachment is processed. + """ + adapter = _make_adapter(monkeypatch) + captured = _capture(adapter, monkeypatch) + + await adapter._dispatch_inbound(_dm_event("\ufffc", msg_id="placeholder")) + + assert captured == [] + + @pytest.mark.asyncio async def test_dispatch_group_type(monkeypatch: pytest.MonkeyPatch) -> None: adapter = _make_adapter(monkeypatch)