|
@@ -0,0 +1,192 @@
|
|
|
|
|
+import path from "node:path";
|
|
|
|
|
+import fs from "node:fs";
|
|
|
|
|
+import { makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } from "baileys";
|
|
|
|
|
+import qrcode from "qrcode";
|
|
|
|
|
+import { WhatsappConversa } from "../models/WhatsappConversa.model.js";
|
|
|
|
|
+import { WhatsappMensagem } from "../models/WhatsappMensagem.model.js";
|
|
|
|
|
+import { BadRequestError, NotFoundError } from "../shared/errors/index.js";
|
|
|
|
|
+
|
|
|
|
|
+const SESSION_DIR = path.resolve("data/baileys-session");
|
|
|
|
|
+
|
|
|
|
|
+let sock = null;
|
|
|
|
|
+let estado = { status: "desconectado", qrDataUrl: null, telefone: null, ultimoErro: null };
|
|
|
|
|
+
|
|
|
|
|
+function normalizarTelefone(jid) {
|
|
|
|
|
+ return jid.split("@")[0].split(":")[0];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function obterOuCriarConversa(telefone, nomeContato, jid) {
|
|
|
|
|
+ const existente = await WhatsappConversa.query().findOne({ Telefone: telefone });
|
|
|
|
|
+ if (existente) {
|
|
|
|
|
+ const patch = {};
|
|
|
|
|
+ if (nomeContato && nomeContato !== existente.NomeContato) patch.NomeContato = nomeContato;
|
|
|
|
|
+ if (jid && jid !== existente.Jid) patch.Jid = jid;
|
|
|
|
|
+ if (Object.keys(patch).length > 0) {
|
|
|
|
|
+ await WhatsappConversa.query().findById(existente.Id).patch({ ...patch, UpdatedAt: new Date() });
|
|
|
|
|
+ }
|
|
|
|
|
+ return existente.Id;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const criada = await WhatsappConversa.query().insert({
|
|
|
|
|
+ Telefone: telefone,
|
|
|
|
|
+ NomeContato: nomeContato ?? null,
|
|
|
|
|
+ Jid: jid ?? null,
|
|
|
|
|
+ Status: "aberta"
|
|
|
|
|
+ });
|
|
|
|
|
+ return criada.Id;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export async function iniciarConexao() {
|
|
|
|
|
+ if (sock) return;
|
|
|
|
|
+
|
|
|
|
|
+ fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
|
|
|
+ const { state, saveCreds } = await useMultiFileAuthState(SESSION_DIR);
|
|
|
|
|
+ const { version } = await fetchLatestBaileysVersion();
|
|
|
|
|
+
|
|
|
|
|
+ sock = makeWASocket({ auth: state, version });
|
|
|
|
|
+ estado.status = "conectando";
|
|
|
|
|
+
|
|
|
|
|
+ sock.ev.on("creds.update", saveCreds);
|
|
|
|
|
+
|
|
|
|
|
+ sock.ev.on("connection.update", async (update) => {
|
|
|
|
|
+ if (update.qr) {
|
|
|
|
|
+ estado.qrDataUrl = await qrcode.toDataURL(update.qr);
|
|
|
|
|
+ estado.status = "qr_pendente";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (update.connection === "open") {
|
|
|
|
|
+ estado = { status: "conectado", qrDataUrl: null, telefone: normalizarTelefone(sock.user.id), ultimoErro: null };
|
|
|
|
|
+ console.log(`[whatsapp] conectado como ${estado.telefone}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (update.connection === "close") {
|
|
|
|
|
+ const deslogado = update.lastDisconnect?.error?.output?.statusCode === DisconnectReason.loggedOut;
|
|
|
|
|
+ console.log(`[whatsapp] conexão fechada (deslogado=${deslogado}):`, update.lastDisconnect?.error?.message);
|
|
|
|
|
+ sock = null;
|
|
|
|
|
+
|
|
|
|
|
+ if (deslogado) {
|
|
|
|
|
+ estado = { status: "desconectado", qrDataUrl: null, telefone: null, ultimoErro: null };
|
|
|
|
|
+ fs.rmSync(SESSION_DIR, { recursive: true, force: true });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ estado.status = "desconectado";
|
|
|
|
|
+ estado.ultimoErro = update.lastDisconnect?.error?.message ?? null;
|
|
|
|
|
+ // queda momentânea (rede, etc.) — tenta reconectar sozinho usando a sessão salva
|
|
|
|
|
+ iniciarConexao().catch(() => {});
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ sock.ev.on("messages.upsert", async ({ messages, type }) => {
|
|
|
|
|
+ console.log(`[whatsapp] messages.upsert type=${type} count=${messages.length}`);
|
|
|
|
|
+
|
|
|
|
|
+ for (const m of messages) {
|
|
|
|
|
+ if (m.key.fromMe) {
|
|
|
|
|
+ console.log("[whatsapp] ignorada: fromMe");
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (m.key.remoteJid?.endsWith("@g.us") || m.key.remoteJid === "status@broadcast") {
|
|
|
|
|
+ console.log(`[whatsapp] ignorada: jid=${m.key.remoteJid}`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!m.message) {
|
|
|
|
|
+ console.log(`[whatsapp] ignorada: m.message vazio (falha de decriptação ou stub) — messageStubType=${m.messageStubType}, jid=${m.key.remoteJid}`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // remoteJid pode ser um LID (identificador interno) em vez do número de telefone real;
|
|
|
|
|
+ // quando o WhatsApp informa senderPn, ele traz o JID baseado no número de fato — preferir esse
|
|
|
|
|
+ const jid = m.key.senderPn || m.key.remoteJid;
|
|
|
|
|
+ const telefone = normalizarTelefone(jid);
|
|
|
|
|
+ const texto = m.message.conversation ?? m.message.extendedTextMessage?.text ?? `[mensagem do tipo não suportada]`;
|
|
|
|
|
+ const timestamp = m.messageTimestamp ? new Date(Number(m.messageTimestamp) * 1000) : new Date();
|
|
|
|
|
+ console.log(`[whatsapp] gravando mensagem de ${telefone}: "${texto}"`);
|
|
|
|
|
+ const conversaId = await obterOuCriarConversa(telefone, m.pushName, jid);
|
|
|
|
|
+
|
|
|
|
|
+ // o Baileys pode reentregar a mesma mensagem; key.id é único e o insert é ignorado nesse caso
|
|
|
|
|
+ const inserted = await WhatsappMensagem.query()
|
|
|
|
|
+ .insert({
|
|
|
|
|
+ ConversaId: conversaId,
|
|
|
|
|
+ Direcao: "entrada",
|
|
|
|
|
+ Corpo: texto,
|
|
|
|
|
+ WhatsappMessageId: m.key.id ?? null,
|
|
|
|
|
+ Timestamp: timestamp
|
|
|
|
|
+ })
|
|
|
|
|
+ .onConflict("WhatsappMessageId")
|
|
|
|
|
+ .ignore();
|
|
|
|
|
+
|
|
|
|
|
+ if (!inserted?.Id) {
|
|
|
|
|
+ console.log(`[whatsapp] ignorada: WhatsappMessageId duplicado (${m.key.id})`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await WhatsappConversa.query().findById(conversaId).patch({ UltimaMensagemEm: timestamp, UpdatedAt: new Date() });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function obterStatus() {
|
|
|
|
|
+ return estado;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export async function desconectar() {
|
|
|
|
|
+ if (sock) {
|
|
|
|
|
+ await sock.logout().catch(() => {});
|
|
|
|
|
+ sock = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ fs.rmSync(SESSION_DIR, { recursive: true, force: true });
|
|
|
|
|
+ estado = { status: "desconectado", qrDataUrl: null, telefone: null, ultimoErro: null };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export async function enviarMensagem(jid, texto) {
|
|
|
|
|
+ if (!sock || estado.status !== "conectado") {
|
|
|
|
|
+ throw new BadRequestError("whatsapp_nao_conectado");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const res = await sock.sendMessage(jid, { text: texto });
|
|
|
|
|
+ return res?.key?.id ?? null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export async function iniciarConexaoSalva() {
|
|
|
|
|
+ if (fs.existsSync(SESSION_DIR) && fs.readdirSync(SESSION_DIR).length > 0) {
|
|
|
|
|
+ await iniciarConexao();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export async function listarConversas() {
|
|
|
|
|
+ return WhatsappConversa.query().orderBy("UltimaMensagemEm", "desc").orderBy("UpdatedAt", "desc");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export async function listarMensagens(conversaId, after) {
|
|
|
|
|
+ const query = WhatsappMensagem.query().where("ConversaId", conversaId).orderBy("Timestamp", "asc").orderBy("Id", "asc");
|
|
|
|
|
+ if (after) query.where("Timestamp", ">", new Date(after));
|
|
|
|
|
+ return query;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export async function responderConversa(conversaId, texto) {
|
|
|
|
|
+ const conversa = await WhatsappConversa.query().findById(conversaId);
|
|
|
|
|
+ if (!conversa) throw new NotFoundError("conversa_not_found");
|
|
|
|
|
+
|
|
|
|
|
+ const jid = conversa.Jid || `${conversa.Telefone}@s.whatsapp.net`;
|
|
|
|
|
+ const whatsappMessageId = await enviarMensagem(jid, texto);
|
|
|
|
|
+ const timestamp = new Date();
|
|
|
|
|
+
|
|
|
|
|
+ const mensagem = await WhatsappMensagem.query().insert({
|
|
|
|
|
+ ConversaId: conversaId,
|
|
|
|
|
+ Direcao: "saida",
|
|
|
|
|
+ Corpo: texto,
|
|
|
|
|
+ WhatsappMessageId: whatsappMessageId,
|
|
|
|
|
+ Timestamp: timestamp
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ await WhatsappConversa.query().findById(conversaId).patch({ UltimaMensagemEm: timestamp, UpdatedAt: new Date() });
|
|
|
|
|
+
|
|
|
|
|
+ return mensagem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export async function excluirConversa(conversaId) {
|
|
|
|
|
+ const conversa = await WhatsappConversa.query().findById(conversaId);
|
|
|
|
|
+ if (!conversa) throw new NotFoundError("conversa_not_found");
|
|
|
|
|
+
|
|
|
|
|
+ // whatsapp_mensagens tem ON DELETE CASCADE em ConversaId
|
|
|
|
|
+ await WhatsappConversa.query().deleteById(conversaId);
|
|
|
|
|
+}
|