309 lines
13 KiB
JavaScript
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()}`;
|
|
}
|