diff --git a/runtime/operations/whatsapp_bridge/baileys_runner.ts b/runtime/operations/whatsapp_bridge/baileys_runner.ts index bffecee4..c50a9de2 100644 --- a/runtime/operations/whatsapp_bridge/baileys_runner.ts +++ b/runtime/operations/whatsapp_bridge/baileys_runner.ts @@ -6,7 +6,6 @@ import dns from "node:dns/promises"; import makeWASocket, { Browsers, DisconnectReason, - areJidsSameUser, fetchLatestBaileysVersion, jidNormalizedUser, proto, @@ -146,23 +145,27 @@ function jidBaseLocal(jid: string): string { return s.split("@")[0]?.split(":")[0] || ""; } -function jidsReferSameUser(a: string, b: string): boolean { - const aa = String(a || "").trim(); - const bb = String(b || "").trim(); - if (!aa || !bb) return false; - try { - if (areJidsSameUser(aa, bb)) return true; - } catch { - // ignore +function mentionMatchesBotIdentity(mention: string, botId: string): boolean { + const m = String(mention || "").trim(); + const b = String(botId || "").trim(); + if (!m || !b) return false; + if (jidNormalizedUser(m) === jidNormalizedUser(b)) return true; + const mLid = m.toLowerCase().endsWith("@lid"); + const bLid = b.toLowerCase().endsWith("@lid"); + if (mLid || bLid) { + const ml = jidBaseLocal(m); + const bl = jidBaseLocal(b); + if (ml && bl && ml === bl && ml.replace(/\D/g, "").length >= 6) return true; + return false; } - if (jidsSameUser(aa, bb)) return true; - const la = jidBaseLocal(aa); - const lb = jidBaseLocal(bb); - if (la && lb && la === lb) { - const digits = la.replace(/\D/g, ""); - if (digits.length >= 6) return true; - } - return false; + const mp = jidPhone(m); + const bp = jidPhone(b); + return ( + mp.length >= 6 && + mp === bp && + m.toLowerCase().includes("@s.whatsapp") && + b.toLowerCase().includes("@s.whatsapp") + ); } function collectBotIdentityJids( @@ -218,24 +221,11 @@ function messageMentionsBot( ): boolean { const ids = botIds?.length ? botIds : collectBotIdentityJids(sock, botPn, authCreds); if (!ids.length || !mentions.length) return false; - const lidMapping = (sock as any)?.signalRepository?.lidMapping; for (const m of mentions) { const mention = String(m || "").trim(); if (!mention) continue; for (const botId of ids) { - if (jidsReferSameUser(mention, botId)) return true; - } - if (lidMapping && typeof lidMapping.getPNForLID === "function" && mention.toLowerCase().endsWith("@lid")) { - try { - const pn = lidMapping.getPNForLID(mention); - if (pn) { - for (const botId of ids) { - if (jidsReferSameUser(String(pn), botId)) return true; - } - } - } catch { - // ignore - } + if (mentionMatchesBotIdentity(mention, botId)) return true; } } return false; @@ -639,8 +629,8 @@ async function main(): Promise { const isReplyToBot = Boolean( botJidRaw && quote.participant && - (messageMentionsBot(sock, [quote.participant], botJidRaw, botIdentityJids) || - jidsSameUser(botJidRaw, quote.participant)), + !mentions.length && + botIdentityJids.some((botId) => mentionMatchesBotIdentity(quote.participant, botId)), ); const groupName = isGroup ? await resolveGroupName(chatId) : ""; diff --git a/runtime/orchestration/group_ingest.py b/runtime/orchestration/group_ingest.py index 75708404..423c9272 100644 --- a/runtime/orchestration/group_ingest.py +++ b/runtime/orchestration/group_ingest.py @@ -88,7 +88,7 @@ def _metadata_raw(metadata: dict[str, Any] | None) -> dict[str, Any]: def metadata_mentions_bot(metadata: dict[str, Any] | None) -> bool: - """Sidecar resolves LID @-mentions via Baileys; gateway trusts that signal.""" + """Sidecar hint when WhatsApp omits mentionedJid (display-name @).""" if not isinstance(metadata, dict): return False if metadata.get("mentions_bot") is True or metadata.get("mentionsBot") is True: @@ -97,6 +97,25 @@ def metadata_mentions_bot(metadata: dict[str, Any] | None) -> bool: return raw.get("mentionsBot") is True or raw.get("mentions_bot") is True +def mentions_include_bot( + *, + mentions: list[str], + bot_jid: str | None, + metadata: dict[str, Any] | None = None, +) -> bool: + identities = _bot_identity_jids(bot_jid=bot_jid, metadata=metadata) + if not identities: + return False + for raw in mentions or []: + mention = str(raw or "").strip() + if not mention: + continue + for identity in identities: + if jids_same_user(mention, identity): + return True + return False + + 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() @@ -230,44 +249,28 @@ def should_process_group_inbound( ) -> bool: if not is_group: return True + + mentions_list = [str(m).strip() for m in (mentions or []) if str(m).strip()] + trigger_list = [str(t) for t in (triggers or []) if str(t)] + body = str(text or "") + has_trigger = bool(trigger_list and any(t in body for t in trigger_list)) + bot_mentioned = mentions_include_bot( + mentions=mentions_list, + bot_jid=bot_jid, + metadata=metadata, + ) or text_mentions_bot(text=text, bot_jid=bot_jid) + + # Multi-@: reply only when the bot is among mentionedJid; @ others alone stays silent. + if mentions_list: + if bot_mentioned: + return True + return has_trigger + if metadata_mentions_bot(metadata): return True if is_reply_to_bot(metadata=metadata, bot_jid=bot_jid): return True - for identity in _bot_identity_jids(bot_jid=bot_jid, metadata=metadata): - bot = normalize_jid(identity) - if not bot: - continue - mention_set = normalize_jids(list(mentions or [])) - if bot in mention_set: - return True - if "@" in bot: - bot_user = bot.split("@", 1)[0] - for m in mention_set: - if "@" in m and m.split("@", 1)[0] == bot_user: - return True - for m in list(mentions or []): - if jids_same_user(str(m or ""), identity): - return True - mention_set = normalize_jids(list(mentions or [])) - bot = normalize_jid(str(bot_jid or "")) - if bot and bot in mention_set: - return True - # Baileys may omit bot from mentionedJid when user uses display-name @; compare user part. - if bot and "@" in bot: - bot_user = bot.split("@", 1)[0] - for m in mention_set: - if "@" in m and m.split("@", 1)[0] == bot_user: - return True - if bot: - for m in list(mentions or []): - if jids_same_user(str(m or ""), bot): - return True - if text_mentions_bot(text=text, bot_jid=bot_jid): - return True - trigger_list = [str(t) for t in (triggers or []) if str(t)] - body = str(text or "") - if trigger_list and any(t in body for t in trigger_list): + if has_trigger: return True return not require_mention @@ -315,6 +318,7 @@ __all__ = [ "GroupPolicyConfig", "build_group_sender_context", "build_whatsapp_group_reply_metadata", + "mentions_include_bot", "metadata_mentions_bot", "normalize_jid", "normalize_jids", diff --git a/tests/test_group_ingest.py b/tests/test_group_ingest.py index 974b9194..0a09812e 100644 --- a/tests/test_group_ingest.py +++ b/tests/test_group_ingest.py @@ -74,8 +74,8 @@ def test_should_accept_group_mention_with_lid_phone_match() -> None: should_process_group_inbound( is_group=True, text="@bot hello", - mentions=["999@lid"], - bot_jid="999@s.whatsapp.net", + mentions=["6281284654304@lid"], + bot_jid="6281284654304@s.whatsapp.net", require_mention=True, ) is True @@ -102,12 +102,12 @@ def test_jids_same_user_lid_device_suffix() -> None: assert jids_same_user("176944565977182@lid", "176944565977182:2@lid") is True -def test_should_accept_group_when_sidecar_reports_mentions_bot() -> None: +def test_should_accept_group_when_sidecar_reports_mentions_bot_without_jids() -> None: assert ( should_process_group_inbound( is_group=True, - text="hi", - mentions=["unknown-lid@lid"], + text="@bot hi", + mentions=[], bot_jid="999@s.whatsapp.net", require_mention=True, metadata={"mentions_bot": True, "raw": {"mentionsBot": True}}, @@ -116,6 +116,78 @@ def test_should_accept_group_when_sidecar_reports_mentions_bot() -> None: ) +def test_should_reject_group_when_multiple_others_mentioned_without_bot() -> None: + assert ( + should_process_group_inbound( + is_group=True, + text="@alice @bob 开会", + mentions=[ + "111111111111@lid", + "222222222222@lid", + ], + bot_jid="6281284654304@s.whatsapp.net", + require_mention=True, + metadata={"bot_lid": "176944565977182:2@lid"}, + ) + is False + ) + + +def test_should_accept_group_when_bot_among_multiple_mentions() -> None: + assert ( + should_process_group_inbound( + is_group=True, + text="@alice @bot 帮忙", + mentions=[ + "111111111111@lid", + "176944565977182@lid", + ], + bot_jid="6281284654304@s.whatsapp.net", + require_mention=True, + metadata={"bot_lid": "176944565977182:2@lid"}, + ) + is True + ) + + +def test_should_reject_group_when_only_other_user_mentioned() -> None: + assert ( + should_process_group_inbound( + is_group=True, + text="@alice hello", + mentions=["333465375410398@lid"], + bot_jid="6281284654304@s.whatsapp.net", + require_mention=True, + metadata={ + "mentions_bot": True, + "bot_lid": "176944565977182:2@lid", + "raw": {"mentionsBot": True, "isReplyToBot": True}, + }, + ) + is False + ) + + +def test_should_reject_group_when_other_mentioned_even_if_reply_to_bot() -> None: + assert ( + should_process_group_inbound( + is_group=True, + text="@alice follow up", + mentions=["333465375410398@lid"], + bot_jid="6281284654304@s.whatsapp.net", + require_mention=True, + metadata={ + "bot_lid": "176944565977182:2@lid", + "raw": { + "quotedParticipant": "6281284654304@s.whatsapp.net", + "isReplyToBot": True, + }, + }, + ) + is False + ) + + def test_should_accept_group_reply_to_bot() -> None: assert ( should_process_group_inbound(