From fbb489fedf833484945bee128723d8ac8583b7a4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 31 Jul 2026 12:19:17 -0700 Subject: [PATCH] fix(whatsapp): preserve transport envelope ownership (#116989) Co-authored-by: Peter Steinberger --- .../whatsapp/src/inbound/extract.test.ts | 65 +++++++++++++++++++ extensions/whatsapp/src/inbound/extract.ts | 30 ++------- extensions/whatsapp/src/send.test.ts | 41 ++++++++++-- extensions/whatsapp/src/send.ts | 9 +-- 4 files changed, 109 insertions(+), 36 deletions(-) diff --git a/extensions/whatsapp/src/inbound/extract.test.ts b/extensions/whatsapp/src/inbound/extract.test.ts index 45bec4ce7c9c..5618c25fb000 100644 --- a/extensions/whatsapp/src/inbound/extract.test.ts +++ b/extensions/whatsapp/src/inbound/extract.test.ts @@ -117,6 +117,71 @@ describe("extractMentionedJids", () => { }, }, }, + { + name: "shared contacts", + message: { + contactMessage: { + displayName: "Alice", + contextInfo: { mentionedJid: [botJid] }, + }, + }, + }, + { + name: "shared contact collections", + message: { + contactsArrayMessage: { + contacts: [{ displayName: "Alice" }], + contextInfo: { mentionedJid: [botJid] }, + }, + }, + }, + { + name: "location pins", + message: { + locationMessage: { + degreesLatitude: 1, + degreesLongitude: 2, + contextInfo: { mentionedJid: [botJid] }, + }, + }, + }, + { + name: "live locations", + message: { + liveLocationMessage: { + degreesLatitude: 1, + degreesLongitude: 2, + contextInfo: { mentionedJid: [botJid] }, + }, + }, + }, + { + name: "interactive button prompts", + message: { + buttonsMessage: { + contentText: "Choose one", + contextInfo: { mentionedJid: [botJid] }, + }, + }, + }, + { + name: "interactive lists", + message: { + listMessage: { + title: "Choose one", + contextInfo: { mentionedJid: [botJid] }, + }, + }, + }, + { + name: "native interactive prompts", + message: { + interactiveMessage: { + body: { text: "Choose one" }, + contextInfo: { mentionedJid: [botJid] }, + }, + }, + }, ])("preserves direct bot mentions from $name", ({ message }) => { expect(extractMentionedJids(message as proto.IMessage)).toEqual([botJid]); }); diff --git a/extensions/whatsapp/src/inbound/extract.ts b/extensions/whatsapp/src/inbound/extract.ts index c9d55473d892..cbc6ec979c50 100644 --- a/extensions/whatsapp/src/inbound/extract.ts +++ b/extensions/whatsapp/src/inbound/extract.ts @@ -113,34 +113,12 @@ export function extractContextInfo( } export function extractMentionedJids(rawMessage: proto.IMessage | undefined): string[] | undefined { - const message = unwrapMessage(rawMessage); - if (!message) { + // Context ownership already follows Baileys envelopes without entering quoted messages. + const mentionedJids = extractContextInfo(rawMessage)?.mentionedJid?.filter(Boolean); + if (!mentionedJids?.length) { return undefined; } - - const candidates: Array = [ - message.extendedTextMessage?.contextInfo?.mentionedJid, - message.imageMessage?.contextInfo?.mentionedJid, - message.videoMessage?.contextInfo?.mentionedJid, - message.ptvMessage?.contextInfo?.mentionedJid, - message.documentMessage?.contextInfo?.mentionedJid, - message.audioMessage?.contextInfo?.mentionedJid, - message.stickerMessage?.contextInfo?.mentionedJid, - message.buttonsResponseMessage?.contextInfo?.mentionedJid, - message.listResponseMessage?.contextInfo?.mentionedJid, - message.templateButtonReplyMessage?.contextInfo?.mentionedJid, - message.interactiveResponseMessage?.contextInfo?.mentionedJid, - message.pollCreationMessage?.contextInfo?.mentionedJid, - message.pollCreationMessageV2?.contextInfo?.mentionedJid, - message.pollCreationMessageV3?.contextInfo?.mentionedJid, - message.pollCreationMessageV5?.contextInfo?.mentionedJid, - ]; - - const flattened = candidates.flatMap((arr) => arr ?? []).filter(Boolean); - if (flattened.length === 0) { - return undefined; - } - return uniqueStrings(flattened); + return uniqueStrings(mentionedJids); } function extractNativeFlowResponseText( diff --git a/extensions/whatsapp/src/send.test.ts b/extensions/whatsapp/src/send.test.ts index 00102bb500d5..15a1cbd6d764 100644 --- a/extensions/whatsapp/src/send.test.ts +++ b/extensions/whatsapp/src/send.test.ts @@ -656,22 +656,25 @@ describe("web outbound", () => { }); }); - it("keeps explicit document kind for prehydrated image payloads", async () => { - const buf = Buffer.from("image-as-document"); + it.each([ + { contentType: "image/png", fileName: "photo.png" }, + { contentType: "video/mp4", fileName: "clip.mp4" }, + ])("keeps explicit document delivery for prehydrated $contentType payloads", async (media) => { + const buf = Buffer.from("visual-as-document"); await sendMessageWhatsApp("+1555", "doc", { verbose: false, cfg: WHATSAPP_TEST_CFG, mediaPayload: { buffer: buf, - contentType: "image/png", + ...media, kind: "document", - fileName: "photo.png", }, }); - expect(sendMessage).toHaveBeenLastCalledWith("+1555", "doc", buf, "image/png", { - fileName: "photo.png", + expect(sendMessage).toHaveBeenLastCalledWith("+1555", "doc", buf, media.contentType, { + asDocument: true, + fileName: media.fileName, }); }); @@ -836,6 +839,32 @@ describe("web outbound", () => { }); }); + it("returns the actual outbound poll key when Baileys resolves a LID target", async () => { + sendPoll.mockResolvedValueOnce({ + kind: "poll", + messageId: "poll-lid", + keys: [ + { + id: "poll-lid", + remoteJid: "123456789@lid", + fromMe: true, + }, + ], + providerAccepted: true, + }); + + await expect( + sendPollWhatsApp( + "+1555", + { question: "Lunch?", options: ["Pizza", "Sushi"] }, + { verbose: false, cfg: WHATSAPP_TEST_CFG }, + ), + ).resolves.toEqual({ + messageId: "poll-lid", + toJid: "123456789@lid", + }); + }); + it("checks send readiness before sending direct polls", async () => { const assertSendReady = vi.fn(async () => { throw new Error("WhatsApp reachout timelock is active"); diff --git a/extensions/whatsapp/src/send.ts b/extensions/whatsapp/src/send.ts index 5ec2906e13bc..c6088dd78ae1 100644 --- a/extensions/whatsapp/src/send.ts +++ b/extensions/whatsapp/src/send.ts @@ -49,9 +49,10 @@ function buildWhatsAppMediaSendState(params: { forceDocument?: boolean; }): WhatsAppMediaSendState { const { media, caption } = params; - const forceDocumentDelivery = Boolean( - params.forceDocument && supportsForcedDocumentDelivery(media.kind), - ); + const forceDocumentDelivery = + Boolean(params.forceDocument && supportsForcedDocumentDelivery(media.kind)) || + (media.kind === "document" && + (media.mimetype.startsWith("image/") || media.mimetype.startsWith("video/"))); let text = caption ?? ""; let documentFileName = media.kind === "document" ? media.fileName : undefined; let visibleTextAfterVoice: string | undefined; @@ -405,7 +406,7 @@ export async function sendPollWhatsApp( const durationMs = Date.now() - startedAt; outboundLog.info(`Sent poll ${messageId} -> ${redactedJid} (${durationMs}ms)`); logger.info({ jid: redactedJid, messageId }, "sent poll"); - return { messageId, toJid: jid }; + return { messageId, toJid: resolveActualSentRemoteJid(result, jid) }; } catch (err) { logger.error({ err: String(err), to: redactedTo }, "failed to send poll via web session"); throw err;