diff --git a/CHANGELOG.md b/CHANGELOG.md index 61a0677..b0fdbeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this public repository will be documented here. +## Unreleased + +- fixed WhatsApp `single_select` lists being relayed without the required native-flow metadata + ## 0.1.10 - reshaped the repository into a public SDK layout inspired by Baileys diff --git a/examples/sdk/test-berry-compat-matrix.ts b/examples/sdk/test-berry-compat-matrix.ts index 0ccbf15..e57852c 100644 --- a/examples/sdk/test-berry-compat-matrix.ts +++ b/examples/sdk/test-berry-compat-matrix.ts @@ -1,9 +1,12 @@ import { BerryProtocol } from "../../packages/core/src/index.ts"; import { BerryOTP } from "../../packages/berry-otp/src/index.ts"; +import { BerryEventBus } from "../../packages/events/src/index.ts"; +import { BerrySocket } from "../../packages/socket/src/index.ts"; import { buttonsPayloadToNativeFlowInteractiveContent, carouselPayloadToMessageContent, - listToLegacyListMessageContent, + interactiveNativeFlowAdditionalNodes, + listToInteractiveMessageContent, normalizeIncomingMessage, } from "../../packages/wa-message/src/index.ts"; @@ -43,7 +46,7 @@ const runLocalMatrix = () => { assert(interactiveButtons[1]?.name === "cta_copy", "second button should be cta_copy"); assert(interactiveButtons[2]?.name === "cta_url", "third button should be cta_url"); - const listPayload = listToLegacyListMessageContent({ + const listPayload = listToInteractiveMessageContent({ title: "Menu", text: "Escolha uma categoria", footer: "BerryProtocol", @@ -54,9 +57,18 @@ const runLocalMatrix = () => { ], }); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const listMessage = (listPayload as any).listMessage; - assert(listMessage?.sections?.length === 2, "legacy list should preserve two sections"); - assert(listMessage?.buttonText === "Abrir lista", "legacy list should preserve button text"); + const listButton = (listPayload as any).viewOnceMessage?.message?.interactiveMessage + ?.nativeFlowMessage?.buttons?.[0]; + const listParams = JSON.parse(listButton?.buttonParamsJson ?? "{}"); + assert(listButton?.name === "single_select", "list should use the current single_select native flow"); + assert(listParams.sections?.length === 2, "interactive list should preserve two sections"); + assert(listParams.title === "Abrir lista", "interactive list should preserve button text"); + + const listRelayNodes = interactiveNativeFlowAdditionalNodes(); + const nativeFlowNode = (listRelayNodes[0]?.content as any[])?.[0]?.content?.[0]; + assert(listRelayNodes[0]?.tag === "biz", "interactive list relay should include the biz node"); + assert(nativeFlowNode?.tag === "native_flow", "interactive list relay should include native_flow metadata"); + assert(nativeFlowNode?.attrs?.name === "mixed", "interactive list should use the mixed native flow"); const carouselPayload = carouselPayloadToMessageContent({ text: "Confira nossas pizzas", @@ -115,6 +127,31 @@ const runLocalMatrix = () => { console.log("Local compatibility matrix passed."); }; +const runListRelayRegression = async () => { + let relayOptions: { additionalNodes?: Array<{ tag?: string }> } | undefined; + const socket = new BerrySocket({ sessionId: "list-relay-regression" }, new BerryEventBus()); + + // Inject a minimal connected transport so this regression stays local and never contacts WhatsApp. + (socket as any).sock = { + user: { id: "5511999999999:1@s.whatsapp.net" }, + relayMessage: async (_jid: string, _message: unknown, options: typeof relayOptions) => { + relayOptions = options; + }, + }; + + await socket.sendListMessage("5511888888888@s.whatsapp.net", { + text: "Escolha uma opcao", + buttonText: "Abrir lista", + sections: [section("Opcoes", [{ id: "one", title: "Opcao 1" }])], + }); + + assert( + relayOptions?.additionalNodes?.some((node) => node.tag === "biz"), + "sendListMessage must relay native-flow metadata", + ); + console.log("List relay regression passed."); +}; + const waitForOpen = (client: BerryProtocol) => new Promise((resolve) => { client.once("connection.open", () => resolve()); @@ -288,4 +325,5 @@ const runLiveMatrix = async () => { }; runLocalMatrix(); +await runListRelayRegression(); await runLiveMatrix(); diff --git a/packages/socket/src/index.ts b/packages/socket/src/index.ts index dfe1b5c..486f764 100644 --- a/packages/socket/src/index.ts +++ b/packages/socket/src/index.ts @@ -376,6 +376,7 @@ export class BerrySocket { await this.sock.relayMessage(recipientJid, fullMessage.message!, { messageId: fullMessage.key.id!, + additionalNodes: interactiveNativeFlowAdditionalNodes(), }); return fullMessage as WAMessage; diff --git a/scripts/testar-lista-whatsapp.js b/scripts/testar-lista-whatsapp.js new file mode 100644 index 0000000..9d2821d --- /dev/null +++ b/scripts/testar-lista-whatsapp.js @@ -0,0 +1,133 @@ +import pino from "pino"; +import qrcode from "qrcode-terminal"; + +import { BerryClient } from "../packages/core/dist/index.js"; + +const SESSION_ID = "teste-lista-whatsapp"; +const PRIVATE_CHAT_SUFFIXES = ["@s.whatsapp.net", "@lid"]; +const handledMessages = new Set(); + +const TEST_LIST = { + title: "Lista atualizada", + text: "Escolha uma das opções abaixo para confirmar que a lista está funcionando.", + footer: "Teste BerryProtocol", + buttonText: "Abrir opções", + sections: [ + { + title: "Opções de teste", + rows: [ + { + id: "teste_opcao_1", + title: "Opção 1", + description: "Primeira opção da lista atualizada", + }, + { + id: "teste_opcao_2", + title: "Opção 2", + description: "Segunda opção da lista atualizada", + }, + { + id: "teste_opcao_3", + title: "Opção 3", + description: "Terceira opção da lista atualizada", + }, + ], + }, + ], +}; + +const client = new BerryClient({ + sessionId: SESSION_ID, + printQrInTerminal: false, + logger: pino({ level: "silent" }), +}); + +const isPrivateChat = (jid) => PRIVATE_CHAT_SUFFIXES.some((suffix) => jid.endsWith(suffix)); + +const waitForConnection = () => + new Promise((resolve, reject) => { + const onOpen = () => { + cleanup(); + resolve(); + }; + + const onError = ({ error }) => { + cleanup(); + reject(new Error(error)); + }; + + const cleanup = () => { + client.off("connection.open", onOpen); + client.off("auth.error", onError); + }; + + client.on("connection.open", onOpen); + client.on("auth.error", onError); + }); + +client.on("auth.qr", ({ value }) => { + console.clear(); + qrcode.generate(value, { small: true }); + console.log("\nEscaneie o QR code e aguarde a conexão."); +}); + +client.on("message.received", async (message) => { + if (message.type !== "text" || handledMessages.has(message.id)) { + return; + } + + const chatId = message.chatId ?? message.from; + if (!chatId || !isPrivateChat(chatId)) { + return; + } + + handledMessages.add(message.id); + + try { + console.log(`\nMensagem recebida de ${chatId}. Enviando a lista...`); + const sent = await client.sendList(chatId, TEST_LIST); + console.log(`Lista enviada com sucesso. ID: ${sent.id}`); + } catch (error) { + handledMessages.delete(message.id); + console.error( + "Falha ao enviar a lista:", + error instanceof Error ? error.message : String(error), + ); + } +}); + +client.on("connection.close", ({ reason }) => { + console.log(`\nConexão encerrada: ${reason ?? "motivo desconhecido"}.`); +}); + +let shuttingDown = false; +const shutdown = async () => { + if (shuttingDown) { + return; + } + + shuttingDown = true; + console.log("\nEncerrando o teste..."); + await client.disconnect().catch(() => undefined); + process.exit(0); +}; + +process.once("SIGINT", shutdown); +process.once("SIGTERM", shutdown); + +try { + const connected = waitForConnection(); + await client.connectWithQr(); + await connected; + + console.clear(); + console.log("WhatsApp conectado."); + console.log("Envie uma mensagem de texto para esta conta a partir de outro número."); + console.log("A lista atualizada será enviada automaticamente. Pressione Ctrl+C para sair."); +} catch (error) { + console.error( + "Não foi possível conectar ao WhatsApp:", + error instanceof Error ? error.message : String(error), + ); + process.exitCode = 1; +} diff --git a/testar-lista-whatsapp.cmd b/testar-lista-whatsapp.cmd new file mode 100644 index 0000000..2d407f6 --- /dev/null +++ b/testar-lista-whatsapp.cmd @@ -0,0 +1,30 @@ +@echo off +setlocal +chcp 65001 >nul +cd /d "%~dp0" +title BerryProtocol - Teste de lista do WhatsApp + +if not exist "node_modules\.bin\tsc.cmd" ( + echo Dependencias nao encontradas. Execute npm install antes deste teste. + pause + exit /b 1 +) + +call "node_modules\.bin\tsc.cmd" -b packages/events packages/transport packages/protocol packages/messages packages/wa-message packages/media packages/store packages/auth packages/socket packages/core >nul +if errorlevel 1 ( + echo. + echo Nao foi possivel compilar o BerryProtocol. + pause + exit /b 1 +) + +node "scripts\testar-lista-whatsapp.js" +set "BERRY_EXIT_CODE=%ERRORLEVEL%" + +if not "%BERRY_EXIT_CODE%"=="0" ( + echo. + echo O teste terminou com erro. + pause +) + +exit /b %BERRY_EXIT_CODE%