mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
fix(whatsapp): detect group @-mentions via contextInfo.mentionedJid
Before this change, the inbound construction site hard-coded `isMention: !isGroup ? true : undefined`, which meant group messages that explicitly @-mentioned the bot never set the field. The router then never woke the agent on a mention-only trigger. Detection lives in a new pure helper `isBotMentionedInGroup` which scans `contextInfo.mentionedJid` across the four message types that can host mentions (extendedTextMessage + image/video/document captions), matching against both the bot's phone JID and LID since modern WhatsApp clients increasingly emit the LID for phone-number mentions. A second helper `computeIsMention` wraps the DM/group ternary so both branches of the fix are unit-testable. Tests in src/channels/whatsapp.test.ts cover phone-JID detection, LID-only detection, image-caption mentions, the negative cases, and the call-site isMention semantics for DMs vs groups vs no-mention. Fixes #2560
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Regression coverage for #2560 — group @-mentions of the bot must set
|
||||
* `InboundMessage.isMention`. Before the fix, the inbound construction
|
||||
* site hard-coded `isMention: !isGroup ? true : undefined`, which dropped
|
||||
* every group mention on the floor and prevented the router from waking
|
||||
* the agent on a mention-only trigger.
|
||||
*
|
||||
* The detection logic lives in the exported pure helper `isBotMentionedInGroup`;
|
||||
* the inbound site calls it with `normalized`, `botPhoneJid`, `botLidUser`.
|
||||
* `isMention` is then computed as:
|
||||
*
|
||||
* isMention: !isGroup ? true : botMentionedInGroup ? true : undefined
|
||||
*
|
||||
* Both the helper and the call-site ternary are covered below so a future
|
||||
* refactor that breaks either part fails this suite.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { computeIsMention, isBotMentionedInGroup } from './whatsapp.js';
|
||||
|
||||
const BOT_PHONE_JID = '15550009999@s.whatsapp.net';
|
||||
const BOT_LID_USER = '987654321';
|
||||
|
||||
describe('isBotMentionedInGroup (#2560)', () => {
|
||||
it('detects the bot phone JID in extendedTextMessage.contextInfo.mentionedJid', () => {
|
||||
const normalized = {
|
||||
extendedTextMessage: {
|
||||
text: 'hey @15550009999 take a look',
|
||||
contextInfo: { mentionedJid: [BOT_PHONE_JID] },
|
||||
},
|
||||
};
|
||||
expect(isBotMentionedInGroup(normalized, BOT_PHONE_JID, BOT_LID_USER)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the bot is not in mentionedJid', () => {
|
||||
const normalized = {
|
||||
extendedTextMessage: {
|
||||
text: 'hey @15551112222 take a look',
|
||||
contextInfo: { mentionedJid: ['15551112222@s.whatsapp.net'] },
|
||||
},
|
||||
};
|
||||
expect(isBotMentionedInGroup(normalized, BOT_PHONE_JID, BOT_LID_USER)).toBe(false);
|
||||
});
|
||||
|
||||
it('detects an LID-only mention when no phone JID is in the list', () => {
|
||||
// Modern WhatsApp clients increasingly emit the LID even when the
|
||||
// human typed a phone-number mention; the phone JID may not appear.
|
||||
const normalized = {
|
||||
extendedTextMessage: {
|
||||
contextInfo: { mentionedJid: [`${BOT_LID_USER}@lid`] },
|
||||
},
|
||||
};
|
||||
expect(isBotMentionedInGroup(normalized, BOT_PHONE_JID, BOT_LID_USER)).toBe(true);
|
||||
});
|
||||
|
||||
it('detects a mention in an image caption', () => {
|
||||
const normalized = {
|
||||
imageMessage: {
|
||||
caption: 'check this @15550009999',
|
||||
contextInfo: { mentionedJid: [BOT_PHONE_JID] },
|
||||
},
|
||||
};
|
||||
expect(isBotMentionedInGroup(normalized, BOT_PHONE_JID, BOT_LID_USER)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false on an empty / missing mentionedJid array', () => {
|
||||
expect(isBotMentionedInGroup({}, BOT_PHONE_JID, BOT_LID_USER)).toBe(false);
|
||||
expect(
|
||||
isBotMentionedInGroup(
|
||||
{ extendedTextMessage: { contextInfo: { mentionedJid: [] } } },
|
||||
BOT_PHONE_JID,
|
||||
BOT_LID_USER,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when neither bot identifier is known', () => {
|
||||
const normalized = {
|
||||
extendedTextMessage: {
|
||||
contextInfo: { mentionedJid: [BOT_PHONE_JID, `${BOT_LID_USER}@lid`] },
|
||||
},
|
||||
};
|
||||
expect(isBotMentionedInGroup(normalized, undefined, undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('InboundMessage.isMention semantics (#2560)', () => {
|
||||
it('is undefined for a group message with no bot mention', () => {
|
||||
expect(computeIsMention(true, false)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('is true for a group message where the bot is mentioned', () => {
|
||||
expect(computeIsMention(true, true)).toBe(true);
|
||||
});
|
||||
|
||||
it('is true for a DM regardless of mention state', () => {
|
||||
// DMs are unconditionally mentions — the helper isn't consulted there.
|
||||
expect(computeIsMention(false, false)).toBe(true);
|
||||
expect(computeIsMention(false, true)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -157,6 +157,64 @@ function formatWhatsApp(text: string): string {
|
||||
return segments.map(({ content, isProtected }) => (isProtected ? content : transformForWhatsApp(content))).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Subset of a normalized Baileys message content carrying the message
|
||||
* types that can host a `contextInfo.mentionedJid` array. Kept as a
|
||||
* structural type so the helper (and its tests) don't pull in the full
|
||||
* `proto.IMessage` shape just to construct fixtures.
|
||||
*/
|
||||
type MentionContextSource = {
|
||||
extendedTextMessage?: { contextInfo?: { mentionedJid?: string[] | null } | null } | null;
|
||||
imageMessage?: { contextInfo?: { mentionedJid?: string[] | null } | null } | null;
|
||||
videoMessage?: { contextInfo?: { mentionedJid?: string[] | null } | null } | null;
|
||||
documentMessage?: { contextInfo?: { mentionedJid?: string[] | null } | null } | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect an explicit @-mention of the bot in a WhatsApp group message.
|
||||
* WhatsApp carries mentions in `contextInfo.mentionedJid` on the text +
|
||||
* caption-bearing message types. Matches against both the bot's phone
|
||||
* JID and LID — most modern clients emit the LID even when the human
|
||||
* typed a phone-number mention.
|
||||
*
|
||||
* Exported for unit testing. The inbound construction site calls this
|
||||
* to set `InboundMessage.isMention` for group messages (#2560). DMs are
|
||||
* unconditionally mentions and don't go through this helper.
|
||||
*/
|
||||
export function isBotMentionedInGroup(
|
||||
normalized: MentionContextSource,
|
||||
botPhoneJid: string | undefined,
|
||||
botLidUser: string | undefined,
|
||||
): boolean {
|
||||
if (!botPhoneJid && !botLidUser) return false;
|
||||
const mentionedJids: string[] = [
|
||||
...(normalized.extendedTextMessage?.contextInfo?.mentionedJid ?? []),
|
||||
...(normalized.imageMessage?.contextInfo?.mentionedJid ?? []),
|
||||
...(normalized.videoMessage?.contextInfo?.mentionedJid ?? []),
|
||||
...(normalized.documentMessage?.contextInfo?.mentionedJid ?? []),
|
||||
];
|
||||
const botLidJid = botLidUser ? `${botLidUser}@lid` : undefined;
|
||||
return mentionedJids.some((jid) => {
|
||||
if (!jid) return false;
|
||||
const bare = jid.split(':')[0];
|
||||
return bare === botPhoneJid || bare === botLidJid;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute `InboundMessage.isMention` for a WhatsApp message:
|
||||
* - DMs are always mentions (router auto-engages on the bot's behalf).
|
||||
* - Group messages are mentions only when the bot is explicitly tagged.
|
||||
*
|
||||
* Returns `true | undefined` rather than `true | false` because the
|
||||
* `InboundMessage` field is `isMention?: boolean` and downstream code
|
||||
* treats `undefined` differently than an explicit `false` (#2560).
|
||||
*/
|
||||
export function computeIsMention(isGroup: boolean, botMentionedInGroup: boolean): true | undefined {
|
||||
if (!isGroup) return true;
|
||||
return botMentionedInGroup ? true : undefined;
|
||||
}
|
||||
|
||||
/** Map file extension to Baileys media message type. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function buildMediaMessage(data: Buffer, filename: string, ext: string, caption?: string): any {
|
||||
@@ -613,14 +671,21 @@ registerChannelAdapter('whatsapp', {
|
||||
}
|
||||
}
|
||||
|
||||
// Detect explicit @-mentions of the bot in groups. Detail in
|
||||
// isBotMentionedInGroup(); short version is contextInfo.mentionedJid
|
||||
// on text + caption-bearing messages, matched against the bot's
|
||||
// phone JID and LID (#2560).
|
||||
const botMentionedInGroup = isGroup && isBotMentionedInGroup(normalized, botPhoneJid, botLidUser);
|
||||
|
||||
const inbound: InboundMessage = {
|
||||
id: msg.key.id || `wa-${Date.now()}`,
|
||||
kind: 'chat',
|
||||
// DMs are addressed to the bot by definition. Mark them as
|
||||
// platform-confirmed mentions so the router auto-creates an
|
||||
// approval-required messaging_group when the chat is unknown,
|
||||
// instead of silently dropping.
|
||||
isMention: !isGroup ? true : undefined,
|
||||
// instead of silently dropping. In groups, only an explicit
|
||||
// @-mention counts.
|
||||
isMention: computeIsMention(isGroup, botMentionedInGroup),
|
||||
isGroup,
|
||||
content: {
|
||||
text: content,
|
||||
|
||||
Reference in New Issue
Block a user