Files
clube67_newwhats.local/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/rich-message.js
T
VPS 4 Deploy Agent 5ec6bd6354
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)
chore(ops): migrate clube67 to newwhats.clube67.com directory
2026-05-18 03:26:41 +02:00

309 lines
13 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendRichMessage = sendRichMessage;
/**
* rich-message.ts — Construção e envio de mensagens ricas via Baileys padrão
*
* O pacote @whiskeysockets/baileys não suporta os campos `nativeButtons`,
* `nativeList` e `nativeCarousel` que são específicos do fork InfiniteAPI.
* Este módulo replica a lógica de transformação do InfiniteAPI para que
* o motor newwhats possa enviar botões, listas e carrosséis usando o
* sock.relayMessage() com o proto correto.
*
* Referência: github:rsalcara/InfiniteAPI — lib/Socket/messages-send.js
*
* IMPORTANTE: O WhatsApp Web NÃO suporta viewOnceMessage para mensagens interativas.
* Por isso usamos interactiveMessage DIRETO (sem wrapper viewOnce) + additionalNodes.
*
* O que é necessário para botões/listas/carrosséis funcionarem no WhatsApp
* (celular E web):
*
* 1. interactiveMessage direto (NÃO viewOnceMessage) — viewOnce não carrega no Web
*
* 2. Nó adicional <biz><interactive type="native_flow" v="1"><native_flow v="9" name="mixed"/></interactive></biz>
* injetado no stanza via relayMessage(additionalNodes)
* (sem isso o WhatsApp Web exibe "Não foi possível carregar")
*
* 3. Nó <bot biz_bot="1"/> para chats 1-a-1
* (necessário para interactive messages em conversas privadas)
*
* Lista (single_select) também usa o mesmo fluxo — InfiniteAPI converte
* o viewOnceMessage > nativeFlowMessage de volta para listMessage no relayMessage,
* mas testamos que a forma direta com nó biz > list também funciona.
*/
const engine_1 = require("./engine");
// ─── Converte um botão nativo para o formato buttonParamsJson do WhatsApp ─────
function formatNativeFlowButton(btn) {
switch (btn.type) {
case 'url':
return {
name: 'cta_url',
buttonParamsJson: JSON.stringify({
display_text: btn.text,
url: btn.url,
merchant_url: btn.url,
}),
};
case 'copy':
return {
name: 'cta_copy',
buttonParamsJson: JSON.stringify({
display_text: btn.text,
copy_code: btn.copyText,
}),
};
case 'reply':
return {
name: 'quick_reply',
buttonParamsJson: JSON.stringify({
display_text: btn.text,
id: btn.id,
}),
};
case 'call':
return {
name: 'cta_call',
buttonParamsJson: JSON.stringify({
display_text: btn.text,
phone_number: btn.phoneNumber,
}),
};
}
}
// ─── Nó biz > interactive > native_flow (obrigatório para renderizar no Web) ──
const BIZ_NATIVE_FLOW_NODE = {
tag: 'biz',
attrs: {},
content: [
{
tag: 'interactive',
attrs: { type: 'native_flow', v: '1' },
content: [
{
tag: 'native_flow',
attrs: { v: '9', name: 'mixed' },
},
],
},
],
};
// ─── Nó biz para listMessage legado (formato que renderiza no Web) ───────────
// InfiniteAPI usa <biz><list type="product_list" v="2"/></biz> com proto.listMessage
// (não interactiveMessage). Comentário do fork: "Modern format causes rejection
// (error 479) — Legacy format works on all platforms".
const BIZ_LIST_NODE = {
tag: 'biz',
attrs: {},
content: [
{
tag: 'list',
attrs: { type: 'product_list', v: '2' },
},
],
};
// ─── Nó bot (necessário para chats 1-a-1 com interactive messages) ────────────
const BOT_NODE = {
tag: 'bot',
attrs: { biz_bot: '1' },
};
// ─── Helper: jid é chat privado (não grupo, não status, não bot) ─────────────
function isPrivateChat(jid) {
return (!(0, engine_1.isJidGroup)(jid) &&
!(0, engine_1.isJidStatusBroadcast)(jid) &&
!jid.includes('newsletter') &&
!jid.includes('broadcast'));
}
// ─── Helper: envia proto pré-construído pelo MESMO caminho do sendMessage ────
// Diferença crítica vs `sock.relayMessage` direto:
// sendMessage() → generateWAMessage() → WAProto.Message.fromObject() → relayMessage()
// ↑ normalização proto que o WA Web exige
// Sem esse passo, o Web descarta a mensagem ("Não foi possível carregar").
// É o mesmo motivo pelo qual nossas enquetes (via sendMessage) funcionam no Web
// e os botões (via relayMessage direto) não funcionavam.
async function sendBuiltProto(sock, jid, protoMsg, msgId, additionalNodes, quoted) {
const fullMsg = (0, engine_1.generateWAMessageFromContent)(jid, protoMsg, {
userJid: sock.user?.id,
messageId: msgId,
quoted,
});
await sock.relayMessage(jid, fullMsg.message, {
messageId: fullMsg.key.id,
additionalNodes,
});
return fullMsg.key.id;
}
// ─── Helper: monta interactiveMessage DIRETO (sem viewOnceMessage wrapper) ────
// viewOnceMessage wrapper faz o WhatsApp Web exibir "Não foi possível carregar":
// o Web trata viewOnce como mensagem efêmera e recusa conteúdo interativo dentro.
// Baileys padrão não adiciona messageContextInfo para interactiveMessage
// (só faz isso para poll, com messageSecret). Usar interactiveMessage direto
// + nó <biz> no additionalNodes é o caminho correto com @whiskeysockets/baileys.
function makeInteractiveProto(interactiveMsg) {
return {
interactiveMessage: interactiveMsg,
};
}
/**
* Envia uma mensagem rica pelo socket Baileys padrão.
*
* - Buttons / CTAs: interactiveMessage direto + nó biz>interactive>native_flow + nó bot
* - Carousel: interactiveMessage direto + nó biz>interactive>native_flow
* - List: interactiveMessage direto + nó biz>interactive>native_flow + nó bot
* - Poll: sock.sendMessage (requer encriptação especial do Baileys)
* - Text: sock.sendMessage normal
*
* Retorna o messageId gerado.
*/
async function sendRichMessage(sock, jid, payload, quoted, engineType) {
const { text, footer, nativeButtons, nativeList, nativeCarousel, poll } = payload;
const engine = engineType ?? process.env.BAILEYS_ENGINE ?? 'infinite';
// Se a engine ativa for 'infinite', utilizamos o suporte nativo do fork InfiniteAPI
if (engine === 'infinite') {
// ── Poll: delegado ao sendMessage padrão (requer encriptação especial do Baileys) ──
if (poll) {
const sent = await sock.sendMessage(jid, {
poll: {
name: poll.name,
values: poll.values,
selectableCount: Math.min(Math.max(1, poll.selectableCount ?? 1), poll.values.length),
},
}, quoted ? { quoted } : undefined);
return sent?.key?.id ?? `poll-${Date.now()}`;
}
// ── Botões / CTAs nativos (InfiniteAPI) ──
if (nativeButtons) {
const sent = await sock.sendMessage(jid, {
text: text ?? '',
footer,
nativeButtons,
}, quoted ? { quoted } : undefined);
return sent?.key?.id ?? `btn-${Date.now()}`;
}
// ── Carrossel nativo (InfiniteAPI) ──
if (nativeCarousel) {
const sent = await sock.sendMessage(jid, {
text: text ?? '',
footer,
nativeCarousel,
}, quoted ? { quoted } : undefined);
return sent?.key?.id ?? `carousel-${Date.now()}`;
}
// ── Lista nativa (InfiniteAPI) ──
if (nativeList) {
const sent = await sock.sendMessage(jid, {
text: text ?? '',
footer,
nativeList,
}, quoted ? { quoted } : undefined);
return sent?.key?.id ?? `list-${Date.now()}`;
}
// ── Texto simples (InfiniteAPI) ──
const sent = await sock.sendMessage(jid, { text: text ?? '' }, quoted ? { quoted } : undefined);
return sent?.key?.id ?? `text-${Date.now()}`;
}
// FALLBACK: Lógica de montagem manual de nós binários para o Baileys oficial
// ── Poll: delegado ao sendMessage padrão (requer messageSecret) ───────────
if (poll) {
const sent = await sock.sendMessage(jid, {
poll: {
name: poll.name,
values: poll.values,
selectableCount: Math.min(Math.max(1, poll.selectableCount ?? 1), poll.values.length),
},
}, quoted ? { quoted } : undefined);
return sent?.key?.id ?? `poll-${Date.now()}`;
}
// ── Texto simples: delegado ao sendMessage padrão ─────────────────────────
if (!nativeButtons && !nativeList && !nativeCarousel) {
const sent = await sock.sendMessage(jid, { text: text ?? '' }, quoted ? { quoted } : undefined);
return sent?.key?.id ?? `text-${Date.now()}`;
}
const msgId = (0, engine_1.generateMessageIDV2)(sock.user?.id);
const isPrivate = isPrivateChat(jid);
// ── Botões / CTAs: interactiveMessage + nativeFlowMessage ────────────────────
if (nativeButtons) {
const formattedButtons = nativeButtons.map(formatNativeFlowButton);
const protoMsg = makeInteractiveProto({
body: { text: text ?? '' },
footer: footer ? { text: footer } : undefined,
header: { title: '', subtitle: '', hasMediaAttachment: false },
nativeFlowMessage: {
buttons: formattedButtons,
messageParamsJson: JSON.stringify({}),
messageVersion: 2,
},
});
const additionalNodes = [BIZ_NATIVE_FLOW_NODE];
return await sendBuiltProto(sock, jid, protoMsg, msgId, additionalNodes, quoted);
}
// ── Carrossel: viewOnceMessage > interactiveMessage > carouselMessage ─────
if (nativeCarousel) {
const cards = nativeCarousel.cards;
if (cards.length < 2)
throw new Error('Carrossel requer no mínimo 2 cards');
if (cards.length > 10)
throw new Error('Carrossel suporta no máximo 10 cards');
const carouselCards = cards.map((card) => {
const rawButtons = card.buttons ?? [];
const cardButtons = rawButtons.length > 0
? rawButtons.map((btn) => formatNativeFlowButton({
type: btn.type ?? 'reply',
id: btn.id ?? btn.text.toLowerCase().replace(/\s+/g, '_'),
text: btn.text,
}))
: [{ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'Ver mais', id: 'see_more' }) }];
return {
header: {
title: card.title ?? '',
subtitle: '',
hasMediaAttachment: !!(card.image?.url),
},
body: { text: card.body ?? '' },
footer: card.footer ? { text: card.footer } : undefined,
nativeFlowMessage: {
buttons: cardButtons,
messageParamsJson: JSON.stringify({}),
},
};
});
const protoMsg = makeInteractiveProto({
body: { text: text ?? '' },
footer: footer ? { text: footer } : undefined,
header: { title: '', subtitle: '', hasMediaAttachment: false },
carouselMessage: {
cards: carouselCards,
messageVersion: 1,
},
});
const additionalNodes = [BIZ_NATIVE_FLOW_NODE];
return await sendBuiltProto(sock, jid, protoMsg, msgId, additionalNodes, quoted);
}
// ── Lista: proto.listMessage LEGADO + nó <biz><list type="product_list" v="2"/></biz> ──
if (nativeList) {
const protoMsg = {
listMessage: {
title: '',
description: text ?? '',
buttonText: nativeList.buttonText,
footerText: footer ?? '',
listType: 2,
sections: nativeList.sections.map((s) => ({
title: s.title,
rows: s.rows.map((r) => ({
rowId: r.id,
title: r.title,
description: r.description ?? '',
})),
})),
},
};
const additionalNodes = [BIZ_LIST_NODE];
if (isPrivate)
additionalNodes.push(BOT_NODE);
return await sendBuiltProto(sock, jid, protoMsg, msgId, additionalNodes, quoted);
}
// Fallback (não deve chegar aqui)
const sent = await sock.sendMessage(jid, { text: text ?? '' });
return sent?.key?.id ?? `fallback-${Date.now()}`;
}
//# sourceMappingURL=rich-message.js.map