diff --git a/extensions/whatsapp/src/auto-reply/monitor/inbound-helpers.test.ts b/extensions/whatsapp/src/auto-reply/monitor/inbound-helpers.test.ts index 88c88e594a4d..4638ba39f578 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/inbound-helpers.test.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/inbound-helpers.test.ts @@ -180,14 +180,18 @@ describe("whatsapp inbound context visibility", () => { }); }); - it("keeps a malformed native quote participant out of comparable identity facts", () => { - const malformedParticipant = "777:4:5@lid"; + it.each([ + ["777:4:5@lid", "777@lid"], + ["whatsapp:777@lid", "777@lid"], + ["whatsapp:whatsapp:777@hosted.lid", "777@hosted.lid"], + [" 777@s.whatsapp.net ", "+777"], + ])("keeps raw native quote participant %j out of comparable facts", (participant, allowFrom) => { const context = describeReplyContext({ extendedTextMessage: { text: "current", contextInfo: { stanzaId: "malformed-native-quote", - participant: malformedParticipant, + participant, quotedMessage: { conversation: "Private quoted text" }, }, }, @@ -203,22 +207,22 @@ describe("whatsapp inbound context visibility", () => { msg, mode: "allowlist", groupPolicy: "allowlist", - groupAllowFrom: ["777@lid"], + groupAllowFrom: [allowFrom], }), ).toBeNull(); const visibleQuote = resolveVisibleWhatsAppReplyContext({ msg, mode: "allowlist_quote", groupPolicy: "allowlist", - groupAllowFrom: ["777@lid"], + groupAllowFrom: [allowFrom], }); expect(visibleQuote?.body).toBe("Private quoted text"); expect(visibleQuote?.sender).toMatchObject({ - jid: malformedParticipant, + jid: participant, lid: null, e164: null, }); - expect(getComparableIdentityValues(visibleQuote?.sender)).not.toContain("777@lid"); + expect(getComparableIdentityValues(visibleQuote?.sender)).not.toContain(allowFrom); }); it("renders structured quoted media only at the visible preview boundary", () => { diff --git a/extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts b/extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts index 47ecbcaed4a4..42099a7b0919 100644 --- a/extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts +++ b/extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts @@ -245,6 +245,22 @@ describe("isBotMentionedFromTargets", () => { expectMentioned(msg, cfg, true); }); + it.each([ + "whatsapp:216372600647751@lid", + "whatsapp:whatsapp:216372600647751@lid", + " 216372600647751@lid ", + ])("does not treat raw native mention %j as a self mention", (mentionedJid) => { + const msg = makeMsg({ + body: "hey", + mentionedJids: [mentionedJid], + selfE164: "+15551234567", + selfJid: "15551234567@s.whatsapp.net", + selfLid: "216372600647751@lid", + }); + + expectMentioned(msg, mentionCfg, false); + }); + it("honors explicit self-chat overrides without recomputing from allowFrom", () => { const cfg = { mentionRegexes: [/\bopenclaw\b/i], diff --git a/extensions/whatsapp/src/identity.test.ts b/extensions/whatsapp/src/identity.test.ts index 6c7c41c8d36d..0a0d62ddb1e4 100644 --- a/extensions/whatsapp/src/identity.test.ts +++ b/extensions/whatsapp/src/identity.test.ts @@ -43,6 +43,15 @@ describe("normalizeWhatsAppLidJid", () => { }, ); + it.each([ + "whatsapp:123@lid", + "whatsapp:whatsapp:123@hosted.lid", + " 123@s.whatsapp.net ", + " 123@hosted ", + ])("keeps raw provider JID %j non-comparable", (jid) => { + expect(resolveComparableIdentity({ jid })).toEqual({ jid, lid: null, e164: null }); + }); + it.each([ ["123:4@s.whatsapp.net", { jid: "123@s.whatsapp.net", lid: null, e164: "+123" }], ["123:4@hosted", { jid: "123@hosted", lid: null, e164: "+123" }], @@ -51,4 +60,10 @@ describe("normalizeWhatsAppLidJid", () => { ])("canonicalizes exactly one device segment in %j", (jid, expected) => { expect(resolveComparableIdentity({ jid })).toEqual(expected); }); + + it("keeps configured LID aliases on convenience parsing", () => { + expect(normalizeWhatsAppLidJid(" whatsapp:whatsapp:123:4@HOSTED.LID ")).toBe( + "123@hosted.lid", + ); + }); }); diff --git a/extensions/whatsapp/src/identity.ts b/extensions/whatsapp/src/identity.ts index a1baee4bcde4..19672575c2d5 100644 --- a/extensions/whatsapp/src/identity.ts +++ b/extensions/whatsapp/src/identity.ts @@ -1,6 +1,10 @@ import type { MediaPlaceholderTextFact } from "openclaw/plugin-sdk/channel-inbound"; // Whatsapp plugin module implements identity behavior. -import { normalizeWhatsAppPhoneInput, parseWhatsAppJid } from "./phone-input.js"; +import { + normalizeWhatsAppPhoneInput, + parseExactWhatsAppJid, + parseWhatsAppJid, +} from "./phone-input.js"; import { jidToE164 } from "./text-runtime.js"; export type WhatsAppIdentity = { @@ -64,14 +68,15 @@ type LegacyMentionsLike = { }; function normalizeDeviceScopedJid(jid: string | null | undefined): string | null { - const parsed = typeof jid === "string" ? parseWhatsAppJid(jid) : null; + const parsed = typeof jid === "string" ? parseExactWhatsAppJid(jid) : null; return parsed?.kind === "pn" || parsed?.kind === "lid" ? `${parsed.digits}@${parsed.domain}` : (jid ?? null); } -function isWhatsAppLidJid(jid: string | null | undefined): boolean { - return typeof jid === "string" && parseWhatsAppJid(jid)?.kind === "lid"; +function normalizeExactWhatsAppLidJid(jid: string | null | undefined): string | null { + const parsed = typeof jid === "string" ? parseExactWhatsAppJid(jid) : null; + return parsed?.kind === "lid" ? `${parsed.digits}@${parsed.domain}` : null; } export function normalizeWhatsAppLidJid(jid: string | null | undefined): string | null { @@ -85,8 +90,8 @@ export function resolveComparableIdentity( ): WhatsAppIdentity { const rawJid = normalizeDeviceScopedJid(identity?.jid); const rawLid = normalizeDeviceScopedJid(identity?.lid); - const lid = normalizeWhatsAppLidJid(rawLid ?? rawJid); - const jid = rawJid && !isWhatsAppLidJid(rawJid) ? rawJid : null; + const lid = normalizeExactWhatsAppLidJid(rawLid ?? rawJid); + const jid = rawJid && !normalizeExactWhatsAppLidJid(rawJid) ? rawJid : null; const e164 = identity?.e164 != null ? normalizeWhatsAppPhoneInput(identity.e164) diff --git a/extensions/whatsapp/src/phone-input.ts b/extensions/whatsapp/src/phone-input.ts index c648cc097eb1..044b8ba9e39f 100644 --- a/extensions/whatsapp/src/phone-input.ts +++ b/extensions/whatsapp/src/phone-input.ts @@ -36,29 +36,31 @@ export function stripWhatsAppTargetPrefixes(value: string): string { return candidate; } -export function parseWhatsAppJid(value: string): ParsedWhatsAppJid | null { +export function parseExactWhatsAppJid(value: string): ParsedWhatsAppJid | null { if (hasUnsafeWhatsAppTargetCharacters(value)) { return null; } - const stripped = stripWhatsAppTargetPrefixes(value); - const hasGroupPrefix = /^group:/i.test(stripped); - const candidate = trimWhatsAppAsciiSpaces(stripped.replace(/^group:/i, "")); for (const [kind, pattern] of WHATSAPP_JID_PATTERNS) { - const match = candidate.match(pattern); + const match = value.match(pattern); const digits = match?.[1]; const domain = match?.[3]?.toLowerCase(); if (!digits || !domain) { continue; } - if (hasGroupPrefix && kind !== "group") { - return null; - } const device = match?.[2] ? `:${match[2]}` : ""; return { kind, jid: `${digits}${device}@${domain}`, digits, domain }; } return null; } +export function parseWhatsAppJid(value: string): ParsedWhatsAppJid | null { + const stripped = stripWhatsAppTargetPrefixes(value); + const hasGroupPrefix = /^group:/i.test(stripped); + const candidate = trimWhatsAppAsciiSpaces(stripped.replace(/^group:/i, "")); + const parsed = parseExactWhatsAppJid(candidate); + return hasGroupPrefix && parsed?.kind !== "group" ? null : parsed; +} + export function normalizeWhatsAppPhoneInput(value: string): string | null { if (hasUnsafeWhatsAppTargetCharacters(value)) { return null;