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; }