mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-17 08:13:56 +00:00
fix(whatsapp): handle LID group mentions (#2663)
Co-authored-by: Xubin Ren <52506698+Re-bin@users.noreply.github.com>
This commit is contained in:
parent
ab9f49970d
commit
4f5f965f09
@ -26,10 +26,12 @@ export interface InboundMessage {
|
|||||||
id: string;
|
id: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
pn: string;
|
pn: string;
|
||||||
|
participant?: string;
|
||||||
content: string;
|
content: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
isGroup: boolean;
|
isGroup: boolean;
|
||||||
wasMentioned?: boolean;
|
wasMentioned?: boolean;
|
||||||
|
isReplyToBot?: boolean;
|
||||||
media?: string[];
|
media?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,28 +52,49 @@ export class WhatsAppClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private normalizeJid(jid: string | undefined | null): string {
|
private normalizeJid(jid: string | undefined | null): string {
|
||||||
return (jid || '').split(':')[0];
|
return (jid || '').trim().toLowerCase().replace(/:\d+(?=@)/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
private wasMentioned(msg: any): boolean {
|
private selfJids(): Set<string> {
|
||||||
if (!msg?.key?.remoteJid?.endsWith('@g.us')) return false;
|
return new Set(
|
||||||
|
|
||||||
const candidates = [
|
|
||||||
msg?.message?.extendedTextMessage?.contextInfo?.mentionedJid,
|
|
||||||
msg?.message?.imageMessage?.contextInfo?.mentionedJid,
|
|
||||||
msg?.message?.videoMessage?.contextInfo?.mentionedJid,
|
|
||||||
msg?.message?.documentMessage?.contextInfo?.mentionedJid,
|
|
||||||
msg?.message?.audioMessage?.contextInfo?.mentionedJid,
|
|
||||||
];
|
|
||||||
const mentioned = candidates.flatMap((items) => (Array.isArray(items) ? items : []));
|
|
||||||
if (mentioned.length === 0) return false;
|
|
||||||
|
|
||||||
const selfIds = new Set(
|
|
||||||
[this.sock?.user?.id, this.sock?.user?.lid, this.sock?.user?.jid]
|
[this.sock?.user?.id, this.sock?.user?.lid, this.sock?.user?.jid]
|
||||||
.map((jid) => this.normalizeJid(jid))
|
.map((jid) => this.normalizeJid(jid))
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
);
|
);
|
||||||
return mentioned.some((jid: string) => selfIds.has(this.normalizeJid(jid)));
|
}
|
||||||
|
|
||||||
|
private messageContextInfos(msg: any): any[] {
|
||||||
|
const unwrapped = baileysExtractMessageContent(msg?.message);
|
||||||
|
const containers = [msg?.message, unwrapped];
|
||||||
|
const infos = containers.flatMap((message) => [
|
||||||
|
message?.extendedTextMessage?.contextInfo,
|
||||||
|
message?.imageMessage?.contextInfo,
|
||||||
|
message?.videoMessage?.contextInfo,
|
||||||
|
message?.documentMessage?.contextInfo,
|
||||||
|
message?.audioMessage?.contextInfo,
|
||||||
|
]);
|
||||||
|
return infos.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
private botAddressing(msg: any): { wasMentioned: boolean; isReplyToBot: boolean } {
|
||||||
|
if (!msg?.key?.remoteJid?.endsWith('@g.us')) {
|
||||||
|
return { wasMentioned: false, isReplyToBot: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const selfIds = this.selfJids();
|
||||||
|
const contextInfos = this.messageContextInfos(msg);
|
||||||
|
|
||||||
|
const mentioned = contextInfos.flatMap((info) => (
|
||||||
|
Array.isArray(info?.mentionedJid) ? info.mentionedJid : []
|
||||||
|
));
|
||||||
|
const wasMentioned = mentioned.some((jid: string) => selfIds.has(this.normalizeJid(jid)));
|
||||||
|
|
||||||
|
const isReplyToBot = contextInfos.some((info) => {
|
||||||
|
const quotedParticipant = this.normalizeJid(info?.participant);
|
||||||
|
return Boolean(info?.stanzaId && quotedParticipant && selfIds.has(quotedParticipant));
|
||||||
|
});
|
||||||
|
|
||||||
|
return { wasMentioned, isReplyToBot };
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
@ -175,16 +198,17 @@ export class WhatsAppClient {
|
|||||||
if (!finalContent && mediaPaths.length === 0) continue;
|
if (!finalContent && mediaPaths.length === 0) continue;
|
||||||
|
|
||||||
const isGroup = msg.key.remoteJid?.endsWith('@g.us') || false;
|
const isGroup = msg.key.remoteJid?.endsWith('@g.us') || false;
|
||||||
const wasMentioned = this.wasMentioned(msg);
|
const { wasMentioned, isReplyToBot } = this.botAddressing(msg);
|
||||||
|
|
||||||
this.options.onMessage({
|
this.options.onMessage({
|
||||||
id: msg.key.id || '',
|
id: msg.key.id || '',
|
||||||
sender: msg.key.remoteJid || '',
|
sender: msg.key.remoteJid || '',
|
||||||
pn: msg.key.remoteJidAlt || '',
|
pn: msg.key.remoteJidAlt || '',
|
||||||
|
...(isGroup && msg.key.participant ? { participant: msg.key.participant } : {}),
|
||||||
content: finalContent,
|
content: finalContent,
|
||||||
timestamp: msg.messageTimestamp as number,
|
timestamp: msg.messageTimestamp as number,
|
||||||
isGroup,
|
isGroup,
|
||||||
...(isGroup ? { wasMentioned } : {}),
|
...(isGroup ? { wasMentioned: wasMentioned || isReplyToBot, isReplyToBot } : {}),
|
||||||
...(mediaPaths.length > 0 ? { media: mediaPaths } : {}),
|
...(mediaPaths.length > 0 ? { media: mediaPaths } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -216,7 +216,7 @@ class WhatsAppChannel(BaseChannel):
|
|||||||
|
|
||||||
# Extract just the phone number or lid as chat_id
|
# Extract just the phone number or lid as chat_id
|
||||||
is_group = data.get("isGroup", False)
|
is_group = data.get("isGroup", False)
|
||||||
was_mentioned = data.get("wasMentioned", False)
|
was_mentioned = bool(data.get("wasMentioned", False) or data.get("isReplyToBot", False))
|
||||||
|
|
||||||
if is_group and getattr(self.config, "group_policy", "open") == "mention":
|
if is_group and getattr(self.config, "group_policy", "open") == "mention":
|
||||||
if not was_mentioned:
|
if not was_mentioned:
|
||||||
@ -225,7 +225,8 @@ class WhatsAppChannel(BaseChannel):
|
|||||||
# Classify by JID suffix: @s.whatsapp.net = phone, @lid.whatsapp.net = LID
|
# Classify by JID suffix: @s.whatsapp.net = phone, @lid.whatsapp.net = LID
|
||||||
# The bridge's pn/sender fields don't consistently map to phone/LID across versions.
|
# The bridge's pn/sender fields don't consistently map to phone/LID across versions.
|
||||||
raw_a = pn or ""
|
raw_a = pn or ""
|
||||||
raw_b = sender or ""
|
participant = data.get("participant", "")
|
||||||
|
raw_b = participant or sender or ""
|
||||||
id_a = raw_a.split("@")[0] if "@" in raw_a else raw_a
|
id_a = raw_a.split("@")[0] if "@" in raw_a else raw_a
|
||||||
id_b = raw_b.split("@")[0] if "@" in raw_b else raw_b
|
id_b = raw_b.split("@")[0] if "@" in raw_b else raw_b
|
||||||
|
|
||||||
@ -289,6 +290,8 @@ class WhatsAppChannel(BaseChannel):
|
|||||||
"message_id": message_id,
|
"message_id": message_id,
|
||||||
"timestamp": data.get("timestamp"),
|
"timestamp": data.get("timestamp"),
|
||||||
"is_group": data.get("isGroup", False),
|
"is_group": data.get("isGroup", False),
|
||||||
|
"participant": participant or None,
|
||||||
|
"is_reply_to_bot": data.get("isReplyToBot", False),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -163,6 +163,32 @@ async def test_group_policy_mention_accepts_mentioned_group_message():
|
|||||||
assert kwargs["sender_id"] == "user"
|
assert kwargs["sender_id"] == "user"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_group_policy_mention_accepts_reply_to_bot_message():
|
||||||
|
ch = WhatsAppChannel({"enabled": True, "allowFrom": ["*"], "groupPolicy": "mention"}, MagicMock())
|
||||||
|
ch._handle_message = AsyncMock()
|
||||||
|
|
||||||
|
await ch._handle_bridge_message(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"id": "m-reply",
|
||||||
|
"sender": "12345@g.us",
|
||||||
|
"pn": "user@s.whatsapp.net",
|
||||||
|
"content": "replying to bot",
|
||||||
|
"timestamp": 1,
|
||||||
|
"isGroup": True,
|
||||||
|
"wasMentioned": False,
|
||||||
|
"isReplyToBot": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ch._handle_message.assert_awaited_once()
|
||||||
|
kwargs = ch._handle_message.await_args.kwargs
|
||||||
|
assert kwargs["metadata"]["is_reply_to_bot"] is True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_sender_id_prefers_phone_jid_over_lid():
|
async def test_sender_id_prefers_phone_jid_over_lid():
|
||||||
"""sender_id should resolve to phone number when @s.whatsapp.net JID is present."""
|
"""sender_id should resolve to phone number when @s.whatsapp.net JID is present."""
|
||||||
@ -184,6 +210,30 @@ async def test_sender_id_prefers_phone_jid_over_lid():
|
|||||||
assert kwargs["sender_id"] == "5551234"
|
assert kwargs["sender_id"] == "5551234"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_group_sender_id_uses_participant_when_phone_jid_missing():
|
||||||
|
"""Group messages should identify the participant, not the group chat JID."""
|
||||||
|
ch = WhatsAppChannel({"enabled": True, "allowFrom": ["SENDERLID"]}, MagicMock())
|
||||||
|
ch._handle_message = AsyncMock()
|
||||||
|
|
||||||
|
await ch._handle_bridge_message(
|
||||||
|
json.dumps({
|
||||||
|
"type": "message",
|
||||||
|
"id": "group-lid",
|
||||||
|
"sender": "12345@g.us",
|
||||||
|
"pn": "",
|
||||||
|
"participant": "SENDERLID@lid.whatsapp.net",
|
||||||
|
"content": "hi",
|
||||||
|
"timestamp": 1,
|
||||||
|
"isGroup": True,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
kwargs = ch._handle_message.await_args.kwargs
|
||||||
|
assert kwargs["sender_id"] == "SENDERLID"
|
||||||
|
assert kwargs["metadata"]["participant"] == "SENDERLID@lid.whatsapp.net"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_lid_to_phone_cache_resolves_lid_only_messages():
|
async def test_lid_to_phone_cache_resolves_lid_only_messages():
|
||||||
"""When only LID is present, a cached LID→phone mapping should be used."""
|
"""When only LID is present, a cached LID→phone mapping should be used."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user