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 4638ba39f578..901d36959a3f 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/inbound-helpers.test.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/inbound-helpers.test.ts @@ -185,6 +185,12 @@ describe("whatsapp inbound context visibility", () => { ["whatsapp:777@lid", "777@lid"], ["whatsapp:whatsapp:777@hosted.lid", "777@hosted.lid"], [" 777@s.whatsapp.net ", "+777"], + ["777@S.WHATSAPP.NET", "+777"], + ["777@HOSTED", "+777"], + ["777@LID", "777@lid"], + ["777@HOSTED.LID", "777@hosted.lid"], + ["123-456@G.US", "123-456@g.us"], + ["777@NEWSLETTER", "777@newsletter"], ])("keeps raw native quote participant %j out of comparable facts", (participant, allowFrom) => { const context = describeReplyContext({ extendedTextMessage: { 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 42099a7b0919..607005697fe0 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 @@ -246,16 +246,21 @@ describe("isBotMentionedFromTargets", () => { }); it.each([ - "whatsapp:216372600647751@lid", - "whatsapp:whatsapp:216372600647751@lid", - " 216372600647751@lid ", - ])("does not treat raw native mention %j as a self mention", (mentionedJid) => { + ["whatsapp:216372600647751@lid", "216372600647751@lid"], + ["whatsapp:whatsapp:216372600647751@lid", "216372600647751@lid"], + [" 216372600647751@lid ", "216372600647751@lid"], + ["216372600647751@LID", "216372600647751@lid"], + ["15551234567@S.WHATSAPP.NET", "15551234567@s.whatsapp.net"], + ["216372600647751@LID", "216372600647751@LID"], + ["123-456@G.US", "123-456@G.US"], + ["216372600647751@NEWSLETTER", "216372600647751@NEWSLETTER"], + ])("does not treat raw native mention %j as self identity %j", (mentionedJid, selfJid) => { const msg = makeMsg({ body: "hey", mentionedJids: [mentionedJid], selfE164: "+15551234567", - selfJid: "15551234567@s.whatsapp.net", - selfLid: "216372600647751@lid", + selfJid, + selfLid: selfJid.includes("@lid") || selfJid.includes("@LID") ? selfJid : undefined, }); expectMentioned(msg, mentionCfg, false); diff --git a/extensions/whatsapp/src/identity.test.ts b/extensions/whatsapp/src/identity.test.ts index 0a0d62ddb1e4..1584f2e753c4 100644 --- a/extensions/whatsapp/src/identity.test.ts +++ b/extensions/whatsapp/src/identity.test.ts @@ -1,6 +1,7 @@ // Whatsapp identity tests cover strict provider identity aliases. import { describe, expect, it } from "vitest"; import { normalizeWhatsAppLidJid, resolveComparableIdentity } from "./identity.js"; +import { parseExactWhatsAppJid, parseWhatsAppJid } from "./phone-input.js"; describe("normalizeWhatsAppLidJid", () => { it.each([ @@ -66,4 +67,18 @@ describe("normalizeWhatsAppLidJid", () => { "123@hosted.lid", ); }); + + it.each([ + ["123@S.WHATSAPP.NET", "123@s.whatsapp.net"], + ["123:4@HOSTED", "123:4@hosted"], + ["123@LID", "123@lid"], + ["123:4@HOSTED.LID", "123:4@hosted.lid"], + ["123-456@G.US", "123-456@g.us"], + ["123@NEWSLETTER", "123@newsletter"], + ])("keeps raw domain case exact while convenience-normalizing %j", (raw, canonical) => { + expect(parseExactWhatsAppJid(raw)).toBeNull(); + expect(parseWhatsAppJid(raw)?.jid).toBe(canonical); + const resolved = resolveComparableIdentity({ jid: raw }); + expect(resolved).toEqual({ jid: raw, lid: null, e164: null }); + }); }); diff --git a/extensions/whatsapp/src/identity.ts b/extensions/whatsapp/src/identity.ts index 19672575c2d5..b456d93366c7 100644 --- a/extensions/whatsapp/src/identity.ts +++ b/extensions/whatsapp/src/identity.ts @@ -109,7 +109,9 @@ export function getComparableIdentityValues( identity: WhatsAppIdentity | WhatsAppSelfIdentity | null | undefined, ): string[] { const resolved = resolveComparableIdentity(identity); - return [resolved.e164, resolved.jid, resolved.lid].filter((value): value is string => + const parsedJid = resolved.jid ? parseExactWhatsAppJid(resolved.jid) : null; + const comparableJid = parsedJid?.kind === "pn" || parsedJid?.kind === "lid" ? resolved.jid : null; + return [resolved.e164, comparableJid, resolved.lid].filter((value): value is string => Boolean(value), ); } diff --git a/extensions/whatsapp/src/inbound-policy.ts b/extensions/whatsapp/src/inbound-policy.ts index 0a7fc44af1aa..341dfbe6560a 100644 --- a/extensions/whatsapp/src/inbound-policy.ts +++ b/extensions/whatsapp/src/inbound-policy.ts @@ -210,7 +210,7 @@ export async function resolveWhatsAppCommandAuthorized(params: { }); const isGroup = admission.conversation.kind === "group"; const sender = getSenderIdentity(params.msg, params.authDir); - const stableSender = isGroup ? (sender.e164 ?? sender.lid ?? sender.jid) : admission.sender.id; + const stableSender = isGroup ? (sender.e164 ?? sender.lid) : admission.sender.id; if (!stableSender) { return false; } diff --git a/extensions/whatsapp/src/inbound.media.test.ts b/extensions/whatsapp/src/inbound.media.test.ts index 0def2b23e6cb..f43672ab63b6 100644 --- a/extensions/whatsapp/src/inbound.media.test.ts +++ b/extensions/whatsapp/src/inbound.media.test.ts @@ -242,7 +242,7 @@ vi.mock("./session.js", async () => { groupFetchAllParticipating: vi.fn().mockResolvedValue({}), updateMediaMessage: vi.fn(), logger: {}, - user: { id: "me@s.whatsapp.net" }, + user: { id: "15550001111@s.whatsapp.net" }, }; return currentMockSocket; }), @@ -442,7 +442,7 @@ describe("web inbound media saves with extension", () => { contextInfo: { stanzaId: "quoted-image", participant: "222@s.whatsapp.net", - mentionedJid: ["me@s.whatsapp.net"], + mentionedJid: ["15550001111@s.whatsapp.net"], quotedMessage: { imageMessage: { mimetype: "image/jpeg" }, }, @@ -490,7 +490,7 @@ describe("web inbound media saves with extension", () => { text: "what is in your image?", contextInfo: { stanzaId: "bot-image", - participant: "me@s.whatsapp.net", + participant: "15550001111@s.whatsapp.net", quotedMessage: { imageMessage: { mimetype: "image/jpeg" } }, }, }, @@ -510,7 +510,7 @@ describe("web inbound media saves with extension", () => { const retry = encryptMediaRetryRequest( quoted!.key as never, Buffer.alloc(32, 1), - "me@s.whatsapp.net", + "15550001111@s.whatsapp.net", ); const retryNode = Array.isArray(retry.content) ? retry.content.find((node) => node.tag === "rmr") diff --git a/extensions/whatsapp/src/inbound/access-control.identity.test.ts b/extensions/whatsapp/src/inbound/access-control.identity.test.ts index 49234c0fae61..8f2942552ab8 100644 --- a/extensions/whatsapp/src/inbound/access-control.identity.test.ts +++ b/extensions/whatsapp/src/inbound/access-control.identity.test.ts @@ -151,6 +151,10 @@ describe("typed WhatsApp direct identity access", () => { ["123:4@hosted.lid", "123@hosted.lid", true], ["15550001111:4@s.whatsapp.net", "+15550001111", true], ["15550001111:4@hosted", "+15550001111", true], + ["123@LID", "123@lid", false], + ["123:4@HOSTED.LID", "123@hosted.lid", false], + ["15550001111@S.WHATSAPP.NET", "+15550001111", false], + ["15550001111:4@HOSTED", "+15550001111", false], ])( "authorizes group command sender %j against exact direct entry %j as %j", async (senderJid, allowFrom, expected) => { diff --git a/extensions/whatsapp/src/phone-input.ts b/extensions/whatsapp/src/phone-input.ts index 044b8ba9e39f..1831c1b763bd 100644 --- a/extensions/whatsapp/src/phone-input.ts +++ b/extensions/whatsapp/src/phone-input.ts @@ -1,11 +1,11 @@ // Whatsapp helper module owns dependency-free strict phone input parsing. const WHATSAPP_PHONE_INPUT_RE = /^\+?[\d ().-]+$/; const WHATSAPP_JID_PATTERNS = [ - ["pn", /^(\d+)(?::(\d+))?@(s\.whatsapp\.net|hosted)$/i], - ["pn", /^(\d+)()@(c\.us)$/i], - ["lid", /^(\d+)(?::(\d+))?@(lid|hosted\.lid)$/i], - ["group", /^([0-9]+(?:-[0-9]+)*)()@(g\.us)$/i], - ["newsletter", /^([0-9]+)()@(newsletter)$/i], + ["pn", /^(\d+)(?::(\d+))?@(s\.whatsapp\.net|hosted)$/], + ["pn", /^(\d+)()@(c\.us)$/], + ["lid", /^(\d+)(?::(\d+))?@(lid|hosted\.lid)$/], + ["group", /^([0-9]+(?:-[0-9]+)*)()@(g\.us)$/], + ["newsletter", /^([0-9]+)()@(newsletter)$/], ] as const; type ParsedWhatsAppJid = { @@ -43,7 +43,7 @@ export function parseExactWhatsAppJid(value: string): ParsedWhatsAppJid | null { for (const [kind, pattern] of WHATSAPP_JID_PATTERNS) { const match = value.match(pattern); const digits = match?.[1]; - const domain = match?.[3]?.toLowerCase(); + const domain = match?.[3]; if (!digits || !domain) { continue; } @@ -57,7 +57,7 @@ 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); + const parsed = parseExactWhatsAppJid(candidate.toLowerCase()); return hasGroupPrefix && parsed?.kind !== "group" ? null : parsed; }