From 8c665290c4385e8f20ad67a162e38cf558a766ea Mon Sep 17 00:00:00 2001 From: oliver Date: Thu, 4 Jun 2026 20:01:45 +0800 Subject: [PATCH] fix(whatsapp): resolve LID @-mentions in sidecar and trust mentions_bot in gateway Co-authored-by: Cursor --- .../application/gateway/inbound_service.py | 4 +- .../whatsapp_bridge/baileys_runner.ts | 75 +++++++++++++++++-- runtime/orchestration/group_ingest.py | 13 ++++ tests/test_group_ingest.py | 14 ++++ 4 files changed, 99 insertions(+), 7 deletions(-) diff --git a/runtime/application/gateway/inbound_service.py b/runtime/application/gateway/inbound_service.py index 2baf0445..c420fc64 100644 --- a/runtime/application/gateway/inbound_service.py +++ b/runtime/application/gateway/inbound_service.py @@ -672,6 +672,7 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]: account = store.find_user_by_channel_account(channel=inbound.channel, account_id=account_id) or {} from runtime.orchestration.group_ingest import ( build_group_sender_context, + metadata_mentions_bot, resolve_group_policy, session_user_key, should_process_group_inbound, @@ -712,10 +713,11 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]: import logging logging.getLogger(__name__).info( - "whatsapp group inbound skipped chat=%s user=%s mentions=%s require_mention=%s text=%r", + "whatsapp group inbound skipped chat=%s user=%s mentions=%s mentions_bot=%s require_mention=%s text=%r", inbound.external_chat_id, inbound.external_user_id, list(inbound.mentions or []), + metadata_mentions_bot(inbound.metadata if isinstance(inbound.metadata, dict) else {}), group_policy.require_mention, text[:120], ) diff --git a/runtime/operations/whatsapp_bridge/baileys_runner.ts b/runtime/operations/whatsapp_bridge/baileys_runner.ts index 8a2df8e2..b5a40892 100644 --- a/runtime/operations/whatsapp_bridge/baileys_runner.ts +++ b/runtime/operations/whatsapp_bridge/baileys_runner.ts @@ -6,6 +6,7 @@ import dns from "node:dns/promises"; import makeWASocket, { Browsers, DisconnectReason, + areJidsSameUser, fetchLatestBaileysVersion, jidNormalizedUser, proto, @@ -129,6 +130,53 @@ function extractQuoteContext(m: proto.IMessage | null | undefined): { participan }; } +function resolveSenderJid(key: proto.IMessageKey): string { + const participant = String(key.participant || "").trim(); + const participantAlt = String((key as any).participantAlt || "").trim(); + if (participantAlt && participant.toLowerCase().endsWith("@lid")) { + return jidNormalizedUser(participantAlt); + } + if (participant) return jidNormalizedUser(participant); + return ""; +} + +function messageMentionsBot( + sock: ReturnType | null, + mentions: string[], + botJid: string, +): boolean { + const bot = String(botJid || "").trim(); + if (!bot || !mentions.length) return false; + const lidMapping = (sock as any)?.signalRepository?.lidMapping; + for (const m of mentions) { + const mention = String(m || "").trim(); + if (!mention) continue; + try { + if (areJidsSameUser(mention, bot)) return true; + } catch { + // ignore + } + if (jidsSameUser(mention, bot)) return true; + if (lidMapping && typeof lidMapping.getPNForLID === "function" && mention.toLowerCase().endsWith("@lid")) { + try { + const pn = lidMapping.getPNForLID(mention); + if (pn && (areJidsSameUser(String(pn), bot) || jidsSameUser(String(pn), bot))) return true; + } catch { + // ignore + } + } + if (lidMapping && typeof lidMapping.getLIDForPN === "function" && bot.includes("@s.whatsapp")) { + try { + const lid = lidMapping.getLIDForPN(bot); + if (lid && (areJidsSameUser(mention, String(lid)) || jidsSameUser(mention, String(lid)))) return true; + } catch { + // ignore + } + } + } + return false; +} + function isStatusOrBroadcastJid(jid: string): boolean { const low = String(jid || "").toLowerCase(); return low === "status@broadcast" || low.endsWith("@broadcast"); @@ -143,13 +191,16 @@ function buildInboundPayload(params: { mentions: string[]; groupName?: string; botJid?: string; + mentionsBot?: boolean; }): Json { const metadata: Json = { source: "whatsapp_baileys", raw: params.raw, + mentions_bot: params.mentionsBot === true, }; if (params.groupName) metadata.group_name = params.groupName; if (params.botJid) metadata.bot_jid = params.botJid; + if (params.mentions.length) metadata.mentioned_jids = params.mentions; return { channel: "whatsapp", account_id: ACCOUNT_ID, @@ -493,26 +544,37 @@ async function main(): Promise { if (!text) continue; const isGroup = remoteJid.endsWith("@g.us"); - const from = isGroup ? String(key.participant || "").trim() : remoteJid; - const userId = from ? jidNormalizedUser(from) : jidNormalizedUser(remoteJid); + const participantRaw = String(key.participant || "").trim(); + const participantAlt = String((key as any).participantAlt || "").trim(); + const userId = isGroup + ? resolveSenderJid(key) || jidNormalizedUser(remoteJid) + : jidNormalizedUser(remoteJid); const chatId = jidNormalizedUser(remoteJid); const mentions = extractMentionsFromUpsert(msg); const quote = extractQuoteContext(msg.message); const botJidRaw = sock?.user?.id ? String(sock.user.id).trim() : ""; const botJid = botJidRaw ? jidNormalizedUser(botJidRaw) : ""; - const isReplyToBot = Boolean(botJidRaw && quote.participant && jidsSameUser(botJidRaw, quote.participant)); + const mentionsBot = messageMentionsBot(sock, mentions, botJidRaw); + const isReplyToBot = Boolean( + botJidRaw && + quote.participant && + (messageMentionsBot(sock, [quote.participant], botJidRaw) || jidsSameUser(botJidRaw, quote.participant)), + ); const groupName = isGroup ? await resolveGroupName(chatId) : ""; const raw = { id, remoteJid, - participant: key.participant || null, + participant: participantRaw || null, + participantAlt: participantAlt || null, pushName: (msg as any).pushName || null, messageTimestamp: (msg as any).messageTimestamp || null, quotedParticipant: quote.participant || null, quotedStanzaId: quote.stanzaId || null, isReplyToBot, + mentionsBot, + mentionedJids: mentions, }; const inbound = buildInboundPayload({ @@ -524,17 +586,18 @@ async function main(): Promise { mentions, groupName: groupName || undefined, botJid: botJidRaw || botJid || undefined, + mentionsBot, }); if (VERBOSE || isGroup) { log( - `inbound group=${isGroup} chat=${chatId} user=${userId} mentions=${mentions.length} replyToBot=${isReplyToBot} textLen=${text.length}`, + `inbound group=${isGroup} chat=${chatId} user=${userId} mentions=${mentions.length} mentionsBot=${mentionsBot} replyToBot=${isReplyToBot} textLen=${text.length} mention0=${mentions[0] || ""}`, ); } else if (VERBOSE) { log(`inbound posting chat=${chatId} user=${userId} textLen=${text.length}`); } const out = await postInbound(inbound); const replies = Array.isArray(out.replies) ? (out.replies as Json[]) : []; - if (VERBOSE) log(`inbound ok replies=${replies.length}`); + if (isGroup || VERBOSE) log(`inbound ok chat=${chatId} replies=${replies.length}`); for (const r of replies) { const outText = String((r as any).text || "").trim(); if (!shouldSendOutboundText(outText) && !(Array.isArray((r as any).attachments) && (r as any).attachments.length)) { diff --git a/runtime/orchestration/group_ingest.py b/runtime/orchestration/group_ingest.py index bfe4ccca..b54ecca2 100644 --- a/runtime/orchestration/group_ingest.py +++ b/runtime/orchestration/group_ingest.py @@ -51,6 +51,16 @@ def _metadata_raw(metadata: dict[str, Any] | None) -> dict[str, Any]: return raw if isinstance(raw, dict) else {} +def metadata_mentions_bot(metadata: dict[str, Any] | None) -> bool: + """Sidecar resolves LID @-mentions via Baileys; gateway trusts that signal.""" + if not isinstance(metadata, dict): + return False + if metadata.get("mentions_bot") is True or metadata.get("mentionsBot") is True: + return True + raw = _metadata_raw(metadata) + return raw.get("mentionsBot") is True or raw.get("mentions_bot") is True + + def text_mentions_bot(*, text: str, bot_jid: str | None) -> bool: """Fallback when WhatsApp omits mentionedJid but user visibly @-mentions the bot.""" bot = str(bot_jid or "").strip() @@ -184,6 +194,8 @@ def should_process_group_inbound( ) -> bool: if not is_group: return True + if metadata_mentions_bot(metadata): + return True if is_reply_to_bot(metadata=metadata, bot_jid=bot_jid): return True mention_set = normalize_jids(list(mentions or [])) @@ -252,6 +264,7 @@ __all__ = [ "GroupPolicyConfig", "build_group_sender_context", "build_whatsapp_group_reply_metadata", + "metadata_mentions_bot", "normalize_jid", "normalize_jids", "infer_is_group_from_chat_id", diff --git a/tests/test_group_ingest.py b/tests/test_group_ingest.py index 501377ea..03414ca1 100644 --- a/tests/test_group_ingest.py +++ b/tests/test_group_ingest.py @@ -82,6 +82,20 @@ def test_should_accept_group_mention_with_lid_phone_match() -> None: ) +def test_should_accept_group_when_sidecar_reports_mentions_bot() -> None: + assert ( + should_process_group_inbound( + is_group=True, + text="hi", + mentions=["unknown-lid@lid"], + bot_jid="999@s.whatsapp.net", + require_mention=True, + metadata={"mentions_bot": True, "raw": {"mentionsBot": True}}, + ) + is True + ) + + def test_should_accept_group_reply_to_bot() -> None: assert ( should_process_group_inbound(