fix(whatsapp): preserve transport envelope ownership (#116989)

Co-authored-by: Peter Steinberger <steipete@macos.shared>
This commit is contained in:
Peter Steinberger
2026-07-31 12:19:17 -07:00
committed by GitHub
parent 629d3c2144
commit fbb489fedf
4 changed files with 109 additions and 36 deletions
@@ -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]);
});
+4 -26
View File
@@ -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<string[] | null | undefined> = [
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(
+35 -6
View File
@@ -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");
+5 -4
View File
@@ -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;