From 8a3cded09cfa870678baf76dcf310bb6e5f8ad4d Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:46:26 -0700 Subject: [PATCH] fix(whatsapp): normalize device-qualified ids in every id comparison, bridge and Python #89322 fixed the bridge-local normalizeWhatsAppId, but bridge.js has since moved its id handling to bridge_helpers.js::normalizeWhatsAppId, which still turned `:@lid` into the malformed `@@lid` for mentionedJid / quoted participant / reaction keys, and the Python side (gateway/platforms/whatsapp_common.py::_normalize_whatsapp_id) did the same ':'->'@' swap on botIds. Drop the local duplicate in bridge.js, import the helper, and strip the `:` suffix on both layers so the bot's own ids compare equal to the bare ids WhatsApp sends for mentions and quotes. One invariant test: device-qualified botIds match a bare mentionedId and a bare quotedParticipant; a plain group message still does not trigger. --- contributors/emails/finance@fasttvltd.com | 1 + gateway/platforms/whatsapp_common.py | 6 ++---- scripts/whatsapp-bridge/bridge.js | 10 +--------- scripts/whatsapp-bridge/bridge_helpers.js | 6 +++++- tests/gateway/test_whatsapp_group_gating.py | 15 +++++++++++++++ 5 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 contributors/emails/finance@fasttvltd.com diff --git a/contributors/emails/finance@fasttvltd.com b/contributors/emails/finance@fasttvltd.com new file mode 100644 index 0000000000..5b80741692 --- /dev/null +++ b/contributors/emails/finance@fasttvltd.com @@ -0,0 +1 @@ +eranbes diff --git a/gateway/platforms/whatsapp_common.py b/gateway/platforms/whatsapp_common.py index 16bcd00993..5e5645f20d 100644 --- a/gateway/platforms/whatsapp_common.py +++ b/gateway/platforms/whatsapp_common.py @@ -138,10 +138,8 @@ class WhatsAppBehaviorMixin(OwnAccessPolicyMixin): def _normalize_whatsapp_id(value: Optional[str]) -> str: if not value: return "" - normalized = str(value).strip() - if ":" in normalized and "@" in normalized: - normalized = normalized.replace(":", "@", 1) - return normalized + # Device-qualified ids (`:@lid`) must equal their bare form. + return re.sub(r":\d+(?=@)", "", str(value).strip()) @staticmethod def _is_broadcast_chat(chat_id: str) -> bool: diff --git a/scripts/whatsapp-bridge/bridge.js b/scripts/whatsapp-bridge/bridge.js index 2101017c42..c91cd11281 100644 --- a/scripts/whatsapp-bridge/bridge.js +++ b/scripts/whatsapp-bridge/bridge.js @@ -46,6 +46,7 @@ import { inboundReadReceiptKeys, inferMediaType, mediaPayloadForFile, + normalizeWhatsAppId, pollCreationMessageFromPayload, pollUpdateForAggregation, } from './bridge_helpers.js'; @@ -205,15 +206,6 @@ function trackSentMessageId(sent) { rememberSentId(sent?.key?.id); } -function normalizeWhatsAppId(value) { - if (!value) return ''; - // Strip the : suffix (e.g. 447999674698:14@s.whatsapp.net -> ...@s.whatsapp.net) - // so bot/mention/quoted ids compare consistently. The old .replace(':','@') produced a - // malformed '...@14@lid', which never matched the bot's own id (breaking @mention and - // reply-to-bot detection in groups). - return String(value).replace(/:\d+@/, '@').replace(/:\d+$/, ''); -} - function redactWhatsAppId(value) { const raw = String(value || '').trim(); if (!raw) return ''; diff --git a/scripts/whatsapp-bridge/bridge_helpers.js b/scripts/whatsapp-bridge/bridge_helpers.js index b50c70e654..33a42152c6 100644 --- a/scripts/whatsapp-bridge/bridge_helpers.js +++ b/scripts/whatsapp-bridge/bridge_helpers.js @@ -15,7 +15,11 @@ export const MIME_MAP = { export function normalizeWhatsAppId(value) { if (!value) return ''; - return String(value).replace(':', '@'); + // Baileys reports the bot's own ids device-qualified (`:@lid`), while + // inbound mentionedJid / contextInfo.participant are not. Drop the suffix so both + // forms compare equal; the old `':' -> '@'` swap produced `@@lid`, + // which never matched and silently broke @mention / reply-to-bot gating in groups. + return String(value).replace(/:\d+(?=@)/, '').replace(/:\d+$/, ''); } function unwrapMessageEnvelopes(content) { diff --git a/tests/gateway/test_whatsapp_group_gating.py b/tests/gateway/test_whatsapp_group_gating.py index 96c5fd8c10..47e905cac3 100644 --- a/tests/gateway/test_whatsapp_group_gating.py +++ b/tests/gateway/test_whatsapp_group_gating.py @@ -244,3 +244,18 @@ def test_broadcast_filter_runs_before_allowlist(): assert adapter._should_process_message(msg) is False + + +def test_device_qualified_bot_ids_match_bare_mention_and_quote_ids(): + """Baileys reports the bot's own ids as ``:@lid`` while inbound + mentionedJid / quoted participant ids are bare — both must normalize equal.""" + adapter = _make_adapter(require_mention=True, group_policy="open") + device_qualified = ["447999674698:14@s.whatsapp.net", "116342762025117:14@lid"] + + assert adapter._should_process_message( + _group_message("hi there", botIds=device_qualified, mentionedIds=["116342762025117@lid"]) + ) is True + assert adapter._should_process_message( + _group_message("and this?", botIds=device_qualified, quotedParticipant="447999674698@s.whatsapp.net") + ) is True + assert adapter._should_process_message(_group_message("hello everyone", botIds=device_qualified)) is False