fix(photon): ignore iMessage media placeholders

Cherry-picked from PR #54514; dropped frozen AUTHOR_MAP hunk in scripts/release.py, contributor mapping added instead.
This commit is contained in:
kelsia14 2026-07-28 11:06:10 -07:00 committed by Teknium
parent be05741349
commit fd4f756492
3 changed files with 32 additions and 0 deletions

View file

@ -0,0 +1 @@
kelsia14

View file

@ -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).

View file

@ -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)