Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 43 additions & 5 deletions examples/sdk/test-berry-compat-matrix.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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<void>((resolve) => {
client.once("connection.open", () => resolve());
Expand Down Expand Up @@ -288,4 +325,5 @@ const runLiveMatrix = async () => {
};

runLocalMatrix();
await runListRelayRegression();
await runLiveMatrix();
1 change: 1 addition & 0 deletions packages/socket/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ export class BerrySocket {

await this.sock.relayMessage(recipientJid, fullMessage.message!, {
messageId: fullMessage.key.id!,
additionalNodes: interactiveNativeFlowAdditionalNodes(),
});

return fullMessage as WAMessage;
Expand Down
133 changes: 133 additions & 0 deletions scripts/testar-lista-whatsapp.js
Original file line number Diff line number Diff line change
@@ -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;
}
30 changes: 30 additions & 0 deletions testar-lista-whatsapp.cmd
Original file line number Diff line number Diff line change
@@ -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%
Loading