From 236c04dd71a42cdb0847117baa8eb6947811f7d5 Mon Sep 17 00:00:00 2001 From: HCL Date: Wed, 29 Apr 2026 05:46:07 +0800 Subject: [PATCH] fix(whatsapp): also recognize button/list/interactive responses as content (#73797) Greptile review on the original PR flagged that the four extractors used in `hasInboundUserContent` (`extractText`, `extractMediaPlaceholder`, `extractContactContext`, `extractLocationData`) do not surface `buttonsResponseMessage`, `listResponseMessage`, `templateButtonReplyMessage`, or `interactiveResponseMessage`. The gate would have silently dropped real user button/list selections as if they were receipts. Add a `hasInteractiveResponseContent(message)` helper that checks for the four reply-message keys directly, and call it from `hasInboundUserContent` both at the root and after walking `buildMessageChain` (so wrapped ephemeral/viewOnce envelopes still surface their interactive responses). 5 new test cases cover all four reply types plus the wrapped-ephemeral path. Sign-Off: hclsys --- .../whatsapp/src/inbound/extract.test.ts | 59 +++++++++++++++++++ extensions/whatsapp/src/inbound/extract.ts | 33 ++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/extensions/whatsapp/src/inbound/extract.test.ts b/extensions/whatsapp/src/inbound/extract.test.ts index 244ac4f74af..9b11bd1340b 100644 --- a/extensions/whatsapp/src/inbound/extract.test.ts +++ b/extensions/whatsapp/src/inbound/extract.test.ts @@ -169,6 +169,65 @@ describe("hasInboundUserContent", () => { ).toBe(true); }); + it("returns true for buttons response (user button click)", () => { + expect( + hasInboundUserContent({ + buttonsResponseMessage: { + selectedButtonId: "yes", + selectedDisplayText: "Yes", + }, + } as proto.IMessage), + ).toBe(true); + }); + + it("returns true for list response (user list selection)", () => { + expect( + hasInboundUserContent({ + listResponseMessage: { + title: "Option A", + singleSelectReply: { selectedRowId: "a" }, + } as unknown as proto.Message.IListResponseMessage, + } as proto.IMessage), + ).toBe(true); + }); + + it("returns true for template button reply", () => { + expect( + hasInboundUserContent({ + templateButtonReplyMessage: { + selectedId: "btn-1", + selectedDisplayText: "Click", + } as unknown as proto.Message.ITemplateButtonReplyMessage, + } as proto.IMessage), + ).toBe(true); + }); + + it("returns true for interactive response", () => { + expect( + hasInboundUserContent({ + interactiveResponseMessage: { + body: { text: "x" }, + nativeFlowResponseMessage: { name: "n", paramsJson: "{}" }, + } as unknown as proto.Message.IInteractiveResponseMessage, + } as proto.IMessage), + ).toBe(true); + }); + + it("returns true for buttons response wrapped in ephemeralMessage (regression for #73797 + greptile review)", () => { + expect( + hasInboundUserContent({ + ephemeralMessage: { + message: { + buttonsResponseMessage: { + selectedButtonId: "ok", + selectedDisplayText: "OK", + }, + }, + }, + } as proto.IMessage), + ).toBe(true); + }); + it("returns false for undefined message (regression for #73797)", () => { expect(hasInboundUserContent(undefined)).toBe(false); }); diff --git a/extensions/whatsapp/src/inbound/extract.ts b/extensions/whatsapp/src/inbound/extract.ts index 84dcc53007b..70071efe588 100644 --- a/extensions/whatsapp/src/inbound/extract.ts +++ b/extensions/whatsapp/src/inbound/extract.ts @@ -439,11 +439,28 @@ export function describeReplyContext( }; } +function hasInteractiveResponseContent(message: proto.IMessage | undefined): boolean { + if (!message) { + return false; + } + // Button/list/template/interactive selections that the existing four + // extractors do not cover. Treat any presence of these keys as user + // content — Baileys never delivers these as receipts or protocol + // envelopes, only as explicit user choices. + return Boolean( + message.buttonsResponseMessage || + message.listResponseMessage || + message.templateButtonReplyMessage || + message.interactiveResponseMessage, + ); +} + /** * Fast O(1) check that a Baileys message carries user-visible inbound content - * (text, media, contact, location). Returns false for protocol/receipt/typing - * notifications that arrive on the same `messages.upsert` stream as real - * messages but should not trigger pairing access-control side effects. + * (text, media, contact, location, button/list selection). Returns false for + * protocol/receipt/typing notifications that arrive on the same + * `messages.upsert` stream as real messages but should not trigger pairing + * access-control side effects. */ export function hasInboundUserContent(rawMessage: proto.IMessage | undefined): boolean { if (!rawMessage) { @@ -461,5 +478,15 @@ export function hasInboundUserContent(rawMessage: proto.IMessage | undefined): b if (extractLocationData(rawMessage)) { return true; } + if (hasInteractiveResponseContent(rawMessage)) { + return true; + } + // Walk wrappers (ephemeral, viewOnce, etc.) — interactive responses + // can arrive nested. + for (const candidate of buildMessageChain(rawMessage)) { + if (candidate !== rawMessage && hasInteractiveResponseContent(candidate)) { + return true; + } + } return false; }