mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 00:40:45 +08:00
fix(whatsapp): resolve LID @-mentions in sidecar and trust mentions_bot in gateway
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
dcb779a691
commit
8c665290c4
4 changed files with 99 additions and 7 deletions
|
|
@ -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 {}
|
account = store.find_user_by_channel_account(channel=inbound.channel, account_id=account_id) or {}
|
||||||
from runtime.orchestration.group_ingest import (
|
from runtime.orchestration.group_ingest import (
|
||||||
build_group_sender_context,
|
build_group_sender_context,
|
||||||
|
metadata_mentions_bot,
|
||||||
resolve_group_policy,
|
resolve_group_policy,
|
||||||
session_user_key,
|
session_user_key,
|
||||||
should_process_group_inbound,
|
should_process_group_inbound,
|
||||||
|
|
@ -712,10 +713,11 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.getLogger(__name__).info(
|
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_chat_id,
|
||||||
inbound.external_user_id,
|
inbound.external_user_id,
|
||||||
list(inbound.mentions or []),
|
list(inbound.mentions or []),
|
||||||
|
metadata_mentions_bot(inbound.metadata if isinstance(inbound.metadata, dict) else {}),
|
||||||
group_policy.require_mention,
|
group_policy.require_mention,
|
||||||
text[:120],
|
text[:120],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import dns from "node:dns/promises";
|
||||||
import makeWASocket, {
|
import makeWASocket, {
|
||||||
Browsers,
|
Browsers,
|
||||||
DisconnectReason,
|
DisconnectReason,
|
||||||
|
areJidsSameUser,
|
||||||
fetchLatestBaileysVersion,
|
fetchLatestBaileysVersion,
|
||||||
jidNormalizedUser,
|
jidNormalizedUser,
|
||||||
proto,
|
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<typeof makeWASocket> | 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 {
|
function isStatusOrBroadcastJid(jid: string): boolean {
|
||||||
const low = String(jid || "").toLowerCase();
|
const low = String(jid || "").toLowerCase();
|
||||||
return low === "status@broadcast" || low.endsWith("@broadcast");
|
return low === "status@broadcast" || low.endsWith("@broadcast");
|
||||||
|
|
@ -143,13 +191,16 @@ function buildInboundPayload(params: {
|
||||||
mentions: string[];
|
mentions: string[];
|
||||||
groupName?: string;
|
groupName?: string;
|
||||||
botJid?: string;
|
botJid?: string;
|
||||||
|
mentionsBot?: boolean;
|
||||||
}): Json {
|
}): Json {
|
||||||
const metadata: Json = {
|
const metadata: Json = {
|
||||||
source: "whatsapp_baileys",
|
source: "whatsapp_baileys",
|
||||||
raw: params.raw,
|
raw: params.raw,
|
||||||
|
mentions_bot: params.mentionsBot === true,
|
||||||
};
|
};
|
||||||
if (params.groupName) metadata.group_name = params.groupName;
|
if (params.groupName) metadata.group_name = params.groupName;
|
||||||
if (params.botJid) metadata.bot_jid = params.botJid;
|
if (params.botJid) metadata.bot_jid = params.botJid;
|
||||||
|
if (params.mentions.length) metadata.mentioned_jids = params.mentions;
|
||||||
return {
|
return {
|
||||||
channel: "whatsapp",
|
channel: "whatsapp",
|
||||||
account_id: ACCOUNT_ID,
|
account_id: ACCOUNT_ID,
|
||||||
|
|
@ -493,26 +544,37 @@ async function main(): Promise<void> {
|
||||||
if (!text) continue;
|
if (!text) continue;
|
||||||
|
|
||||||
const isGroup = remoteJid.endsWith("@g.us");
|
const isGroup = remoteJid.endsWith("@g.us");
|
||||||
const from = isGroup ? String(key.participant || "").trim() : remoteJid;
|
const participantRaw = String(key.participant || "").trim();
|
||||||
const userId = from ? jidNormalizedUser(from) : jidNormalizedUser(remoteJid);
|
const participantAlt = String((key as any).participantAlt || "").trim();
|
||||||
|
const userId = isGroup
|
||||||
|
? resolveSenderJid(key) || jidNormalizedUser(remoteJid)
|
||||||
|
: jidNormalizedUser(remoteJid);
|
||||||
const chatId = jidNormalizedUser(remoteJid);
|
const chatId = jidNormalizedUser(remoteJid);
|
||||||
const mentions = extractMentionsFromUpsert(msg);
|
const mentions = extractMentionsFromUpsert(msg);
|
||||||
const quote = extractQuoteContext(msg.message);
|
const quote = extractQuoteContext(msg.message);
|
||||||
const botJidRaw = sock?.user?.id ? String(sock.user.id).trim() : "";
|
const botJidRaw = sock?.user?.id ? String(sock.user.id).trim() : "";
|
||||||
const botJid = botJidRaw ? jidNormalizedUser(botJidRaw) : "";
|
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 groupName = isGroup ? await resolveGroupName(chatId) : "";
|
||||||
|
|
||||||
const raw = {
|
const raw = {
|
||||||
id,
|
id,
|
||||||
remoteJid,
|
remoteJid,
|
||||||
participant: key.participant || null,
|
participant: participantRaw || null,
|
||||||
|
participantAlt: participantAlt || null,
|
||||||
pushName: (msg as any).pushName || null,
|
pushName: (msg as any).pushName || null,
|
||||||
messageTimestamp: (msg as any).messageTimestamp || null,
|
messageTimestamp: (msg as any).messageTimestamp || null,
|
||||||
quotedParticipant: quote.participant || null,
|
quotedParticipant: quote.participant || null,
|
||||||
quotedStanzaId: quote.stanzaId || null,
|
quotedStanzaId: quote.stanzaId || null,
|
||||||
isReplyToBot,
|
isReplyToBot,
|
||||||
|
mentionsBot,
|
||||||
|
mentionedJids: mentions,
|
||||||
};
|
};
|
||||||
|
|
||||||
const inbound = buildInboundPayload({
|
const inbound = buildInboundPayload({
|
||||||
|
|
@ -524,17 +586,18 @@ async function main(): Promise<void> {
|
||||||
mentions,
|
mentions,
|
||||||
groupName: groupName || undefined,
|
groupName: groupName || undefined,
|
||||||
botJid: botJidRaw || botJid || undefined,
|
botJid: botJidRaw || botJid || undefined,
|
||||||
|
mentionsBot,
|
||||||
});
|
});
|
||||||
if (VERBOSE || isGroup) {
|
if (VERBOSE || isGroup) {
|
||||||
log(
|
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) {
|
} else if (VERBOSE) {
|
||||||
log(`inbound posting chat=${chatId} user=${userId} textLen=${text.length}`);
|
log(`inbound posting chat=${chatId} user=${userId} textLen=${text.length}`);
|
||||||
}
|
}
|
||||||
const out = await postInbound(inbound);
|
const out = await postInbound(inbound);
|
||||||
const replies = Array.isArray(out.replies) ? (out.replies as Json[]) : [];
|
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) {
|
for (const r of replies) {
|
||||||
const outText = String((r as any).text || "").trim();
|
const outText = String((r as any).text || "").trim();
|
||||||
if (!shouldSendOutboundText(outText) && !(Array.isArray((r as any).attachments) && (r as any).attachments.length)) {
|
if (!shouldSendOutboundText(outText) && !(Array.isArray((r as any).attachments) && (r as any).attachments.length)) {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,16 @@ def _metadata_raw(metadata: dict[str, Any] | None) -> dict[str, Any]:
|
||||||
return raw if isinstance(raw, dict) else {}
|
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:
|
def text_mentions_bot(*, text: str, bot_jid: str | None) -> bool:
|
||||||
"""Fallback when WhatsApp omits mentionedJid but user visibly @-mentions the bot."""
|
"""Fallback when WhatsApp omits mentionedJid but user visibly @-mentions the bot."""
|
||||||
bot = str(bot_jid or "").strip()
|
bot = str(bot_jid or "").strip()
|
||||||
|
|
@ -184,6 +194,8 @@ def should_process_group_inbound(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not is_group:
|
if not is_group:
|
||||||
return True
|
return True
|
||||||
|
if metadata_mentions_bot(metadata):
|
||||||
|
return True
|
||||||
if is_reply_to_bot(metadata=metadata, bot_jid=bot_jid):
|
if is_reply_to_bot(metadata=metadata, bot_jid=bot_jid):
|
||||||
return True
|
return True
|
||||||
mention_set = normalize_jids(list(mentions or []))
|
mention_set = normalize_jids(list(mentions or []))
|
||||||
|
|
@ -252,6 +264,7 @@ __all__ = [
|
||||||
"GroupPolicyConfig",
|
"GroupPolicyConfig",
|
||||||
"build_group_sender_context",
|
"build_group_sender_context",
|
||||||
"build_whatsapp_group_reply_metadata",
|
"build_whatsapp_group_reply_metadata",
|
||||||
|
"metadata_mentions_bot",
|
||||||
"normalize_jid",
|
"normalize_jid",
|
||||||
"normalize_jids",
|
"normalize_jids",
|
||||||
"infer_is_group_from_chat_id",
|
"infer_is_group_from_chat_id",
|
||||||
|
|
|
||||||
|
|
@ -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:
|
def test_should_accept_group_reply_to_bot() -> None:
|
||||||
assert (
|
assert (
|
||||||
should_process_group_inbound(
|
should_process_group_inbound(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue