Files
instrucoes_gerais/clube67/newwhats.local/backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.js
T

1125 lines
63 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WhatsAppConnectionManager = void 0;
/**
* WhatsAppConnectionManager — Orquestra conexões Baileys para todas as instâncias.
*
* Esta é a classe central do módulo WhatsApp. Ela gerencia:
* 1. Ciclo de vida das conexões (connect, disconnect, reconnect)
* 2. Registro de TODOS os event listeners do Baileys
* 3. Reconexão automática com backoff exponencial
* 4. Watchdog para detectar conexões mortas silenciosamente
* 5. Sincronização de chats, contatos e histórico
* 6. Mapeamento LID → phone JID (via chats.phoneNumberShare)
* 7. Emissão de eventos Socket.IO para o frontend
*
* ARQUITETURA:
* - Uma instância de WhatsAppConnectionManager por servidor (singleton prático)
* - Cada instância WhatsApp (número conectado) tem seu próprio WASocket
* - Os Maps internos indexam por instanceId para acesso O(1)
* - Multi-tenant: tenantByInstance mapeia instância → tenant para isolamento
*
* EVENTOS BAILEYS REGISTRADOS:
* - creds.update → salva credenciais (auth state)
* - connection.update → gerencia QR, conexão, desconexão, reconexão
* - messages.upsert → delega para MessageHandler (tempo real + histórico)
* - messages.update → atualiza status (sent→delivered→read)
* - contacts.upsert/update → delega para ContactHandler
* - chats.upsert/update → sincroniza lista de chats no banco
* - chats.phoneNumberShare → captura mapeamento LID → phone JID
* - groups.upsert/update → sincroniza grupos (subject, metadata)
* - messaging-history.set → history sync (contatos, chats, mensagens em lote)
*
* @see MessageHandler — processa mensagens individuais
* @see ContactHandler — gerencia contatos e avatares
*/
const path_1 = __importDefault(require("path"));
const promises_1 = __importDefault(require("fs/promises"));
const promises_2 = __importDefault(require("dns/promises"));
const baileys_1 = require("@whiskeysockets/baileys");
const qrcode_1 = __importDefault(require("qrcode"));
const env_1 = require("../../../config/env");
const logger_1 = require("../../../config/logger");
const dragonfly_1 = require("../../../infra/cache/dragonfly");
const prisma_1 = require("../../../infra/database/prisma");
const client_1 = require("@prisma/client");
const MessageHandler_1 = require("../handlers/MessageHandler");
const ContactHandler_1 = require("../handlers/ContactHandler");
const chatbot_service_1 = require("../../chatbot/chatbot.service");
const hook_bus_1 = require("../../../core/hook-bus");
// ─── Chaves de cache no DragonflyDB (Redis-compatible) ──────────────────────
const CACHE_KEY_STATUS = (instanceId) => `instance:${instanceId}:status`;
const CACHE_KEY_QR = (instanceId) => `instance:${instanceId}:qr`;
// ─── Configuração de reconexão automática ───────────────────────────────────
// Backoff exponencial: 3s → 6s → 12s → 24s → 48s → ... → max 5min
// Nunca desiste, exceto loggedOut (usuário deslogou) ou forbidden (ban)
const RECONNECT_BASE_DELAY_MS = 3000; // delay inicial
const RECONNECT_MAX_DELAY_MS = 300000; // teto geral: 5 minutos
// Teto MENOR para erros transientes de rede (DNS, ECONN, ETIMEDOUT).
// Esses erros costumam resolver em segundos; um teto de 5min deixa a
// sessão "dormindo" num setTimeout longo enquanto a rede já voltou.
const RECONNECT_NETWORK_MAX_MS = 60000; // teto rede: 60s
const RECONNECT_MAX_ATTEMPTS = Infinity;
// ─── Network probe — acorda reconexões pendentes quando a rede volta ────────
// Enquanto uma instância está aguardando backoff, um DNS lookup leve é
// disparado em intervalo fixo. Se o lookup funciona E existe um retry
// agendado para daqui a mais de PROBE_ADVANCE_THRESHOLD_MS, cancela o timer
// e dispara retry imediato. Zero tráfego para o WhatsApp — só DNS.
const NETWORK_PROBE_INTERVAL_MS = 30000; // a cada 30s
const PROBE_ADVANCE_THRESHOLD_MS = 15000; // só adianta se faltar > 15s
const PROBE_DNS_HOST = 'web.whatsapp.com';
// ─── Watchdog: detecta conexão morta silenciosamente ────────────────────────
// Conservador por design: reconexões frequentes sinalizam "cliente instável"
// para o WhatsApp e aumentam risco de banimento do número. Por isso:
// - Timeout alto (10 min): conversas calmas são normais, não disparam
// - Intervalo baixo de verificação (3 min): menos overhead
// - Ping leve antes de matar: tenta reviver sem derrubar o socket
// - Cool-down entre kills: evita loop de reconexão se a rede oscila
const WATCHDOG_INTERVAL_MS = 180000; // verifica a cada 3 min
const WATCHDOG_TIMEOUT_MS = 600000; // 10 min sem atividade antes de agir
const WATCHDOG_PING_GRACE_MS = 30000; // aguarda 30s após ping antes de matar
const WATCHDOG_COOLDOWN_MS = 900000; // 15 min entre force-kills consecutivos
class WhatsAppConnectionManager {
constructor(io) {
// ─── Maps indexados por instanceId (UUID) ──────────────────────────────
/** Socket Baileys ativo por instância */
this.sockets = new Map();
/** Handler de contatos por instância */
this.contactHandlers = new Map();
/** Handler de mensagens por instância */
this.msgHandlers = new Map();
/** Contador de tentativas de reconexão (reseta ao conectar com sucesso) */
this.reconnectAttempts = new Map();
/** Timer de reconexão pendente (clearTimeout para cancelar) */
this.reconnectTimers = new Map();
/**
* Timestamp absoluto (ms) em que o retry pendente vai disparar.
* Usado pelo network probe para decidir se vale adiantar um retry
* que ainda está dormindo num backoff longo.
*/
this.reconnectScheduledAt = new Map();
/** Timer global do network probe (singleton) */
this.networkProbeTimer = null;
/** Timer do watchdog (clearInterval para parar) */
this.watchdogTimers = new Map();
/** Timestamp da última atividade (qualquer evento Baileys) */
this.lastActivity = new Map();
/** Timestamp do último force-kill do watchdog (cool-down anti-loop) */
this.lastWatchdogKill = new Map();
/** Timestamp do último ping do watchdog (aguarda resposta antes de matar) */
this.lastWatchdogPing = new Map();
/** Mapeamento instanceId → tenantId (necessário para reconexões) */
this.tenantByInstance = new Map();
this.io = io;
this.chatbotService = new chatbot_service_1.ChatbotService(io);
this.startNetworkProbe();
}
// ═══════════════════════════════════════════════════════════════════════════
// CONEXÃO PRINCIPAL
// ═══════════════════════════════════════════════════════════════════════════
/**
* Conecta (ou reconecta) uma instância WhatsApp.
*
* Fluxo:
* 1. Carrega auth state do disco (useMultiFileAuthState)
* 2. Cria socket Baileys com configuração padrão
* 3. Registra TODOS os event listeners
* 4. Baileys inicia handshake com WhatsApp Web
* 5. Se precisar de QR: emite evento 'qr' via Socket.IO
* 6. Se já autenticado: connection 'open' dispara sync
*
* IMPORTANTE: Este método é idempotente — se a instância já está
* conectada, retorna sem fazer nada (evita sockets duplicados).
*/
async connect(instanceId, tenantId) {
if (this.sockets.has(instanceId)) {
// Socket já existe — re-emite o status atual para sincronizar o frontend
// (acontece quando o backend reconectou em background antes do frontend abrir)
const cachedStatus = await dragonfly_1.dragonfly.get(CACHE_KEY_STATUS(instanceId));
const currentStatus = cachedStatus ?? client_1.InstanceStatus.CONNECTED;
logger_1.logger.info({ instanceId, currentStatus }, 'Instância já conectada — re-emitindo status para o frontend');
this.io.to(`tenant:${tenantId}`).emit('instance:status', {
instanceId,
status: currentStatus,
});
return;
}
// Limpa timers pendentes de reconexão anterior
this.clearReconnectTimer(instanceId);
this.tenantByInstance.set(instanceId, tenantId);
// ── Auth state: credenciais persistidas em disco ──────────────────────
// Cada instância tem seu diretório em BAILEYS_SESSIONS_PATH/{instanceId}/
// Contém: creds.json, app-state-sync-key-*, pre-key-*, etc.
const sessionDir = path_1.default.resolve(env_1.env.BAILEYS_SESSIONS_PATH, instanceId);
try {
await promises_1.default.mkdir(sessionDir, { recursive: true });
}
catch (err) {
logger_1.logger.error({ err, instanceId, sessionDir }, 'Falha ao criar diretório de sessão');
await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.DISCONNECTED);
this.io.to(`tenant:${tenantId}`).emit('instance:status', { instanceId, status: client_1.InstanceStatus.DISCONNECTED });
return;
}
const { state, saveCreds } = await (0, baileys_1.useMultiFileAuthState)(sessionDir);
// fetchLatestBaileysVersion faz requisição HTTP ao WhatsApp — pode falhar
// se não houver internet. Usa versão embutida como fallback seguro.
let version = [2, 3000, 1023504162];
try {
const fetched = await (0, baileys_1.fetchLatestBaileysVersion)();
version = fetched.version;
}
catch (err) {
logger_1.logger.warn({ instanceId }, 'fetchLatestBaileysVersion falhou — usando versão embutida como fallback');
}
logger_1.logger.info({ instanceId, version }, 'Iniciando socket Baileys');
// ── Cria socket Baileys ──────────────────────────────────────────────
// DEBUG: logger trace só para capturar XML de mensagens enviadas
const debugLogger = {
level: 'trace',
child: () => debugLogger,
trace: (obj) => {
if (obj?.xml && typeof obj.xml === 'string' && obj.xml.startsWith('<message')) {
console.log('[NEWWHATS/STANZA-XML]\n' + obj.xml);
}
},
debug: () => { }, info: () => { }, warn: () => { }, error: () => { }, fatal: () => { },
};
const sock = (0, baileys_1.makeWASocket)({
version,
auth: {
creds: state.creds,
// Cache de chaves Signal para performance (evita leituras de disco)
keys: (0, baileys_1.makeCacheableSignalKeyStore)(state.keys, logger_1.logger),
},
printQRInTerminal: false,
qrTimeout: 60000,
logger: debugLogger,
browser: ['NewWhats', 'Chrome', '124.0.0'],
syncFullHistory: true, // Solicita histórico completo ao telefone
markOnlineOnConnect: true, // Marca como "online" ao conectar
// getMessage: callback para reenvio de mensagens (retry)
// O Baileys chama quando precisa reenviar uma mensagem que falhou
getMessage: async (key) => {
if (!key.id)
return undefined;
const msg = await prisma_1.prisma.message.findUnique({
where: { instanceId_messageId: { instanceId, messageId: key.id } },
});
if (!msg?.body)
return undefined;
return { conversation: msg.body };
},
});
this.sockets.set(instanceId, sock);
this.touchActivity(instanceId);
// ═════════════════════════════════════════════════════════════════════
// REGISTRO DE EVENT LISTENERS DO BAILEYS
// ═════════════════════════════════════════════════════════════════════
// ─── Credenciais: salvar ao atualizar ────────────────────────────────
// O Baileys atualiza credenciais periodicamente (rotação de chaves Signal)
sock.ev.on('creds.update', saveCreds);
// ─── Estado de conexão ───────────────────────────────────────────────
// Eventos: qr (novo QR gerado), open (conectado), close (desconectado),
// connecting (tentando conectar)
sock.ev.on('connection.update', (update) => {
this.touchActivity(instanceId);
this.baileysLog(tenantId, instanceId, 'connection.update', {
connection: update.connection,
qr: update.qr ? '(qr gerado)' : undefined,
lastDisconnect: update.lastDisconnect
? { statusCode: update.lastDisconnect.error?.output?.statusCode }
: undefined,
});
this.handleConnectionUpdate(instanceId, tenantId, update, sock);
});
// ─── Mensagens recebidas (tempo real + histórico) ────────────────────
// type 'notify': mensagem em tempo real → processMessage completo
// type 'append': histórico sync → handleHistory (bulk, sem socket)
const msgHandler = new MessageHandler_1.MessageHandler(sock, instanceId, tenantId, this.io, this.chatbotService);
this.msgHandlers.set(instanceId, msgHandler);
sock.ws.on('CB:message', (node) => {
const msgId = node?.attrs?.id;
const peerRecipientPn = node?.attrs?.peer_recipient_pn;
if (msgId && peerRecipientPn) {
msgHandler.setPeerRecipientHint(msgId, peerRecipientPn);
}
});
sock.ev.on('messages.upsert', (m) => {
this.touchActivity(instanceId);
// Log de debug para monitoramento via página /baileys
for (const msg of m.messages ?? []) {
const text = msg.message?.conversation ??
msg.message?.extendedTextMessage?.text ??
msg.message?.imageMessage?.caption ??
msg.message?.videoMessage?.caption ??
null;
this.baileysLog(tenantId, instanceId, 'message', {
fromMe: msg.key?.fromMe,
remoteJid: msg.key?.remoteJid,
body: text,
type: Object.keys(msg.message ?? {}).find(k => k !== 'messageContextInfo') ?? 'unknown',
status: msg.status,
});
// Log de monitoramento para mensagens LID
// Ajuda a verificar se key.senderPn está presente para resolução
if (msg.key?.remoteJid?.endsWith('@lid')) {
logger_1.logger.debug({
instanceId,
remoteJid: msg.key.remoteJid,
senderPn: msg.key.senderPn,
pushName: msg.pushName,
}, '[LID] Mensagem @lid recebida — será resolvida pelo MessageHandler');
}
}
// A persistência do mapeamento LID→phone vive dentro do
// MessageHandler.processMessage (no upsert do Contact), porque
// updateMany aqui afetava 0 rows em contatos inexistentes — bug
// silencioso que descartava o mapeamento na primeira mensagem LID.
// Delega processamento completo para o MessageHandler
msgHandler.handle(m);
});
// ─── Status de mensagens (sent → delivered → read) ───────────────────
// O WhatsApp envia atualizações de status conforme a mensagem é entregue
// e lida. Mapeamos os códigos numéricos para strings legíveis.
sock.ev.on('messages.update', (updates) => {
this.touchActivity(instanceId);
for (const { key, update } of updates) {
if (!key.id || !update.status)
continue;
const statusMap = {
0: 'PENDING', 1: 'PENDING', 2: 'SENT', 3: 'DELIVERED', 4: 'READ', 5: 'READ',
};
const status = statusMap[update.status];
if (!status)
continue;
// Atualiza no banco e emite para o frontend (ticks azuis/cinza)
prisma_1.prisma.message.updateMany({
where: { instanceId, messageId: key.id },
data: { status: status },
}).then((r) => {
if (r.count === 0)
return;
this.io.to(`tenant:${tenantId}`).emit('message:status', {
messageId: key.id,
status,
});
hook_bus_1.hookBus.emit('ext:message.update', {
instanceId,
tenantId,
messageId: key.id,
status,
timestamp: Date.now(),
}).catch(() => { });
}).catch(() => { });
}
});
// ─── Contatos ────────────────────────────────────────────────────────
// contacts.upsert: lista completa de contatos (conexão inicial/sync)
// contacts.update: atualizações parciais (nome alterado, etc)
const contactHandler = new ContactHandler_1.ContactHandler(instanceId, tenantId, sock, this.io);
this.contactHandlers.set(instanceId, contactHandler);
sock.ev.on('contacts.upsert', (contacts) => {
this.touchActivity(instanceId);
this.baileysLog(tenantId, instanceId, 'contacts.upsert', { count: contacts.length });
logger_1.logger.info({ instanceId, count: contacts.length }, '[contacts.upsert] Recebido');
contactHandler.upsert(contacts);
});
sock.ev.on('contacts.update', (contacts) => {
this.touchActivity(instanceId);
this.baileysLog(tenantId, instanceId, 'contacts.update', { count: contacts.length });
contactHandler.update(contacts);
});
// ─── LID → Phone mapping (chats.phoneNumberShare) ────────────────────
// Evento emitido quando um contato envia uma mensagem de protocolo
// SHARE_PHONE_NUMBER, revelando o telefone real por trás do LID.
//
// Este é um dos mecanismos para construir o mapeamento LID → phone.
// Outros mecanismos:
// - key.senderPn nas mensagens (MessageHandler.resolveLidToPhoneJid)
// - campo `lid` nos contatos do history sync (ContactHandler.upsert)
//
// @see MessageHandler.resolveLidToPhoneJid() — consome este mapeamento
sock.ev.on('chats.phoneNumberShare', ({ lid, jid: phoneJid }) => {
this.touchActivity(instanceId);
logger_1.logger.info({ instanceId, lid, phoneJid }, '[LID] phoneNumberShare recebido');
if (lid && phoneJid) {
// Salva o mapeamento LID no contato que já existe com o phone JID
prisma_1.prisma.contact.updateMany({
where: { tenantId, instanceId, jid: phoneJid },
data: { lidJid: lid },
}).catch(() => { });
// Tenta corrigir contatos que foram criados com JID = LID
// (pode acontecer se o contato chegou antes do phoneNumberShare)
prisma_1.prisma.contact.updateMany({
where: { tenantId, instanceId, jid: lid },
data: { jid: phoneJid, phone: phoneJid.split('@')[0], lidJid: lid },
}).catch(() => { });
}
});
// ─── Chats ───────────────────────────────────────────────────────────
// chats.upsert: novos chats (ou chats que voltaram da lista)
// chats.update: atualizações (unread count, pin, archive, etc)
sock.ev.on('chats.upsert', (chats) => {
this.touchActivity(instanceId);
this.baileysLog(tenantId, instanceId, 'chats.upsert', { count: chats.length });
this.syncChats(instanceId, tenantId, chats).catch(() => { });
});
sock.ev.on('chats.update', (chats) => {
this.touchActivity(instanceId);
this.baileysLog(tenantId, instanceId, 'chats.update', { count: chats.length });
this.handleChatsUpdate(instanceId, tenantId, chats).catch(() => { });
});
// ─── Grupos ──────────────────────────────────────────────────────────
// groups.upsert: grupo criado ou entrou em novo grupo
// groups.update: subject (nome) ou metadata alterados
// Convertemos para o formato de chat e sincronizamos via syncChats()
sock.ev.on('groups.upsert', (groups) => {
this.touchActivity(instanceId);
this.baileysLog(tenantId, instanceId, 'groups.upsert', { count: groups.length });
const chatsFromGroups = groups.map((g) => ({
id: g.id,
name: g.subject,
conversationTimestamp: g.creation,
unreadCount: 0,
pinned: false,
archived: false,
}));
this.syncChats(instanceId, tenantId, chatsFromGroups).catch(() => { });
});
sock.ev.on('groups.update', (updates) => {
this.touchActivity(instanceId);
this.baileysLog(tenantId, instanceId, 'groups.update', { count: updates.length });
const chatsFromUpdates = updates
.filter((g) => g.id && g.subject)
.map((g) => ({ id: g.id, name: g.subject }));
this.syncChats(instanceId, tenantId, chatsFromUpdates).catch(() => { });
});
// ─── Histórico inicial (messaging-history.set) ───────────────────────
// Evento principal de sincronização: o telefone envia chats, contatos
// e mensagens em lotes. Pode disparar múltiplas vezes.
// Quando isLatest=true, o sync terminou → dispara reconciliação de nomes.
sock.ev.on('messaging-history.set', async ({ chats: histChats, contacts: histContacts, messages: histMessages, isLatest }) => {
this.touchActivity(instanceId);
logger_1.logger.info({
instanceId,
chats: histChats.length,
contacts: histContacts.length,
messages: histMessages.length,
isLatest,
}, '[History] messaging-history.set recebido');
this.baileysLog(tenantId, instanceId, 'messaging-history.set', {
chats: histChats.length,
contacts: histContacts.length,
messages: histMessages.length,
isLatest,
});
// Processa na ordem: contatos → chats → mensagens
// (contatos primeiro para que o vínculo Chat→Contact funcione)
if (histContacts.length > 0) {
await contactHandler.upsert(histContacts).catch(() => { });
}
if (histChats.length > 0) {
await this.syncChats(instanceId, tenantId, histChats).catch(() => { });
}
if (histMessages.length > 0) {
await msgHandler.handleHistory(histMessages).catch(() => { });
}
// Quando isLatest=true, o sync do histórico terminou completamente.
// Inicia reconciliação: busca contatos sem nome e preenche via pushName
// das mensagens recebidas (o history sync não traz pushName nos contatos).
if (isLatest) {
logger_1.logger.info({ instanceId }, '[History] Sync completo (isLatest=true) — iniciando reconciliação de nomes');
contactHandler.reconcileNamesFromMessages().catch(() => { });
}
});
// ── Guard: emite DISCONNECTED se o Baileys lançar erro síncrono ─────────
// makeWASocket pode lançar em casos raros (OpenSSL, auth corrompida, etc.)
// Não há como capturar erros async do Baileys aqui além dos event listeners,
// mas garantimos que o socket foi registrado antes de prosseguir.
if (!this.sockets.has(instanceId)) {
logger_1.logger.error({ instanceId }, 'Socket Baileys não foi registrado após inicialização — marcando como DISCONNECTED');
await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.DISCONNECTED);
this.io.to(`tenant:${tenantId}`).emit('instance:status', { instanceId, status: client_1.InstanceStatus.DISCONNECTED });
}
}
// ═══════════════════════════════════════════════════════════════════════════
// HANDLER DE ATUALIZAÇÃO DE CONEXÃO
// ═══════════════════════════════════════════════════════════════════════════
/**
* Processa eventos de mudança de estado da conexão Baileys.
*
* Estados possíveis:
* - qr: QR Code gerado → emite para o frontend escanear
* - open: conectado com sucesso → inicia sync, watchdog, etc.
* - close: desconectado → decide se reconecta ou para
* - connecting: tentando conectar → atualiza status
*
* DECISÃO DE RECONEXÃO:
* - loggedOut (401) → sessão expirada, limpa arquivos, exige novo QR
* - forbidden (403) → conta banida, para completamente
* - Qualquer outro código → reconecta com backoff exponencial
*/
async handleConnectionUpdate(instanceId, tenantId, update, sock) {
const { connection, lastDisconnect, qr } = update;
// ── QR Code emitido ───────────────────────────────────────────────────
if (qr) {
logger_1.logger.info({ instanceId }, 'QR Code gerado — aguardando escaneamento');
const qrBase64 = await qrcode_1.default.toDataURL(qr);
// Salva no cache com TTL de 60s (QR expira rapidamente)
await dragonfly_1.dragonfly.set(CACHE_KEY_QR(instanceId), qrBase64, 60);
await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.QR_PENDING);
// Emite para a sala da instância (página de conexão) E para o tenant (dashboard)
this.io.to(`instance:${instanceId}`).emit('qr', { instanceId, qrBase64 });
this.io.to(`tenant:${tenantId}`).emit('instance:status', {
instanceId,
status: client_1.InstanceStatus.QR_PENDING,
qrBase64,
});
// ext: bridge — satélites externos recebem via WS /api/ext/v1/stream
hook_bus_1.hookBus.emit('ext:session.qr', { instanceId, tenantId, qrBase64, timestamp: Date.now() }).catch(() => { });
}
// ── Conectado com sucesso ─────────────────────────────────────────────
if (connection === 'open') {
logger_1.logger.info({ instanceId }, 'WhatsApp conectado com sucesso');
// Reseta contadores de reconexão
this.reconnectAttempts.set(instanceId, 0);
this.clearReconnectTimer(instanceId);
// Inicia watchdog para detectar conexão morta
this.startWatchdog(instanceId);
await dragonfly_1.dragonfly.del(CACHE_KEY_QR(instanceId));
await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.CONNECTED);
// Salva o número de telefone da sessão no banco
// (sock.user.id vem no formato "5511999999999:1@s.whatsapp.net")
const phoneJid = sock.user?.id;
if (phoneJid) {
const phone = phoneJid.split(':')[0].split('@')[0];
await prisma_1.prisma.instance.update({ where: { id: instanceId }, data: { phone } })
.catch(() => { });
}
// Pós-conexão: sincroniza avatar da instância e busca avatares ausentes
const handler = this.contactHandlers.get(instanceId);
if (handler) {
// Avatar da própria instância (foto de perfil do número)
handler.syncInstanceAvatar(instanceId)
.then((avatar) => {
if (avatar) {
this.io.to(`tenant:${tenantId}`).emit('instance:avatar', { instanceId, avatar });
}
})
.catch(() => { });
// Busca avatares ausentes após 5s (dá tempo para o sync inicial)
setTimeout(() => handler.syncMissingAvatars().catch(() => { }), 5000);
// Renova avatares próximos de expirar após 30s (não compete com syncMissing)
setTimeout(() => handler.syncExpiredAvatars().catch(() => { }), 30000);
}
// Sync incremental: verifica gap de mensagens durante downtime
this.syncMissedMessages(instanceId, tenantId).catch(() => { });
this.io.to(`tenant:${tenantId}`).emit('instance:status', {
instanceId,
status: client_1.InstanceStatus.CONNECTED,
});
hook_bus_1.hookBus.emit('ext:session.status', { instanceId, tenantId, status: 'connected', timestamp: Date.now() }).catch(() => { });
}
// ── Desconectado ──────────────────────────────────────────────────────
if (connection === 'close') {
const rawErr = lastDisconnect?.error;
const error = rawErr?.output;
const statusCode = error?.statusCode;
const isNetworkErr = this.isTransientNetworkError(rawErr);
logger_1.logger.warn({ instanceId, statusCode, isNetworkErr }, 'Conexao encerrada');
// Limpa referências da instância desconectada
this.sockets.delete(instanceId);
this.contactHandlers.delete(instanceId);
this.msgHandlers.delete(instanceId);
this.stopWatchdog(instanceId);
// Decide se deve reconectar baseado no código de erro
const shouldReconnect = statusCode !== baileys_1.DisconnectReason.loggedOut &&
statusCode !== baileys_1.DisconnectReason.forbidden;
if (shouldReconnect) {
const attempts = (this.reconnectAttempts.get(instanceId) ?? 0) + 1;
this.reconnectAttempts.set(instanceId, attempts);
// Backoff exponencial: 3s, 6s, 12s, 24s, 48s…
// Teto menor (60s) para erros de rede transientes: DNS/ECONN se
// resolvem em segundos, não faz sentido dormir 5min. Teto geral
// (5min) fica para erros de servidor/protocolo do WhatsApp, onde
// paciência é melhor do que pressa.
const maxDelay = isNetworkErr ? RECONNECT_NETWORK_MAX_MS : RECONNECT_MAX_DELAY_MS;
const delay = Math.min(RECONNECT_BASE_DELAY_MS * Math.pow(2, attempts - 1), maxDelay);
const scheduledAt = Date.now() + delay;
const nextAt = new Date(scheduledAt).toISOString();
logger_1.logger.info({ instanceId, attempts, delayMs: delay, nextAt, isNetworkErr }, '[Reconexao] Agendando reconexao com backoff exponencial');
await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.CONNECTING);
this.io.to(`tenant:${tenantId}`).emit('instance:status', {
instanceId,
status: client_1.InstanceStatus.CONNECTING,
reconnectIn: delay,
attempt: attempts,
});
this.scheduleReconnect(instanceId, tenantId, delay);
}
else {
// loggedOut → limpa sessão e exige novo QR
if (statusCode === baileys_1.DisconnectReason.loggedOut) {
logger_1.logger.warn({ instanceId }, 'Sessao desconectada pelo usuario — limpando sessao');
await this.deleteSessionFiles(instanceId);
}
// forbidden → conta banida, apenas atualiza status
await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.DISCONNECTED);
this.io.to(`tenant:${tenantId}`).emit('instance:status', {
instanceId,
status: client_1.InstanceStatus.DISCONNECTED,
reason: baileys_1.DisconnectReason[statusCode] ?? statusCode,
});
hook_bus_1.hookBus.emit('ext:session.status', { instanceId, tenantId, status: 'disconnected', timestamp: Date.now() }).catch(() => { });
}
}
// ── Conectando ────────────────────────────────────────────────────────
if (connection === 'connecting') {
await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.CONNECTING);
this.io.to(`tenant:${tenantId}`).emit('instance:status', {
instanceId,
status: client_1.InstanceStatus.CONNECTING,
});
}
}
// ═══════════════════════════════════════════════════════════════════════════
// SYNC INCREMENTAL — Mensagens perdidas durante downtime
// ═══════════════════════════════════════════════════════════════════════════
/**
* Verifica e notifica sobre gap de mensagens durante downtime.
*
* Consulta a última mensagem no banco e calcula quanto tempo o servidor
* ficou offline. O Baileys com syncFullHistory=true já solicita as
* mensagens pendentes automaticamente — este método apenas:
* 1. Loga o gap para visibilidade (monitoramento)
* 2. Emite 'instance:syncing' para o frontend mostrar indicador de sync
*/
async syncMissedMessages(instanceId, tenantId) {
try {
const lastMsg = await prisma_1.prisma.message.findFirst({
where: { instanceId },
orderBy: { timestamp: 'desc' },
select: { timestamp: true, messageId: true },
});
if (!lastMsg) {
logger_1.logger.info({ instanceId }, '[Sync] Nenhuma mensagem no DB — history sync completo via Baileys');
return;
}
const gap = Date.now() - lastMsg.timestamp.getTime();
const gapMinutes = Math.round(gap / 60000);
logger_1.logger.info({ instanceId, lastMsgAt: lastMsg.timestamp, gapMinutes }, '[Sync] Reconectado — gap de mensagens detectado');
this.baileysLog(tenantId, instanceId, 'sync:gap', {
lastMsgAt: lastMsg.timestamp.toISOString(),
gapMinutes,
});
// Notifica o frontend que está sincronizando
this.io.to(`tenant:${tenantId}`).emit('instance:syncing', {
instanceId,
gapMinutes,
since: lastMsg.timestamp.toISOString(),
});
}
catch (err) {
logger_1.logger.error({ err, instanceId }, '[Sync] Erro ao verificar gap de mensagens');
}
}
// ═══════════════════════════════════════════════════════════════════════════
// WATCHDOG — Detecta conexão morta silenciosamente
// ═══════════════════════════════════════════════════════════════════════════
/**
* Registra timestamp da última atividade para o watchdog.
* Chamado por TODOS os event listeners do Baileys.
*/
touchActivity(instanceId) {
this.lastActivity.set(instanceId, Date.now());
}
/**
* Inicia o watchdog para uma instância.
*
* Fluxo conservador em 3 etapas — escolhido pra minimizar reconexões:
* 1. Se inativo > TIMEOUT (10 min): envia um presence update como
* ping leve e marca timestamp. Se o socket estiver vivo, Baileys
* responde e touchActivity é chamado no próximo evento.
* 2. Se continua inativo GRACE (+30s) após o ping: confirma que a
* conexão está de fato morta. Procede pro kill.
* 3. Antes de matar: verifica cool-down (15 min desde último kill)
* pra não entrar em loop quando a rede está oscilando. Se estiver
* no cool-down, só loga e espera o próximo ciclo.
*
* Esta sequência dá ~10-11 min de folga antes do primeiro kill, e no
* mínimo 15 min entre kills consecutivos — 7-8× mais conservador que
* a versão anterior (2 min sem verificação de estado).
*/
startWatchdog(instanceId) {
this.stopWatchdog(instanceId);
const timer = setInterval(async () => {
const now = Date.now();
const lastTs = this.lastActivity.get(instanceId) ?? 0;
const elapsed = now - lastTs;
const tenantId = this.tenantByInstance.get(instanceId);
if (elapsed <= WATCHDOG_TIMEOUT_MS) {
// Conexão com atividade recente — reseta estado de ping/cool-down
this.lastWatchdogPing.delete(instanceId);
return;
}
const sock = this.sockets.get(instanceId);
// Caso 1: socket sumiu sem connection.close — reconecta imediatamente
if (!sock) {
if (tenantId) {
logger_1.logger.warn({ instanceId }, '[Watchdog] Socket ausente — reagendando conexao');
this.stopWatchdog(instanceId);
this.connect(instanceId, tenantId).catch(() => { });
}
return;
}
const pingedAt = this.lastWatchdogPing.get(instanceId) ?? 0;
const sincePing = now - pingedAt;
// Etapa 1: envia ping leve (presence update) se ainda não pingou
// neste ciclo de inatividade. Não conta como atividade — só estímulo.
if (pingedAt === 0 || sincePing > WATCHDOG_TIMEOUT_MS) {
try {
await sock.sendPresenceUpdate('available');
this.lastWatchdogPing.set(instanceId, now);
logger_1.logger.debug({ instanceId, elapsedMs: elapsed }, '[Watchdog] Ping leve enviado — aguardando resposta');
}
catch (err) {
// Ping falhou → socket já está morto, pula pra etapa de kill
logger_1.logger.warn({ instanceId, err }, '[Watchdog] Ping falhou — socket morto');
this.forceKillSocket(instanceId, sock, tenantId, elapsed);
}
return;
}
// Etapa 2: ping já foi enviado, mas ainda sem atividade após GRACE.
// Confirma que o socket está zumbi e força reconexão.
if (sincePing > WATCHDOG_PING_GRACE_MS) {
this.forceKillSocket(instanceId, sock, tenantId, elapsed);
}
}, WATCHDOG_INTERVAL_MS);
this.watchdogTimers.set(instanceId, timer);
}
/**
* Força o fechamento do socket, respeitando cool-down para evitar loop.
* Só mata se não houve outro kill nos últimos WATCHDOG_COOLDOWN_MS.
*/
forceKillSocket(instanceId, sock, tenantId, elapsedMs) {
const now = Date.now();
const lastKill = this.lastWatchdogKill.get(instanceId) ?? 0;
const sinceKill = now - lastKill;
if (sinceKill < WATCHDOG_COOLDOWN_MS) {
logger_1.logger.warn({ instanceId, elapsedMs, sinceLastKillMs: sinceKill }, '[Watchdog] Em cool-down — adiando kill para proteger numero de ban');
return;
}
logger_1.logger.warn({ instanceId, elapsedMs }, '[Watchdog] Conexao morta confirmada — forcando reconexao');
this.lastWatchdogKill.set(instanceId, now);
this.lastWatchdogPing.delete(instanceId);
try {
sock.end(new Error('Watchdog timeout'));
}
catch { /* ignore */ }
// Se o sock.end não disparar connection.close (raro), reconecta manualmente
if (!this.sockets.has(instanceId) && tenantId) {
this.stopWatchdog(instanceId);
this.connect(instanceId, tenantId).catch(() => { });
}
}
/** Para o watchdog de uma instância */
stopWatchdog(instanceId) {
const timer = this.watchdogTimers.get(instanceId);
if (timer) {
clearInterval(timer);
this.watchdogTimers.delete(instanceId);
}
// Limpa estado de ping pendente — novo ciclo começa limpo.
// NÃO limpa lastWatchdogKill aqui: o cool-down deve sobreviver a
// stop/start para bloquear reconexões em cascata.
this.lastWatchdogPing.delete(instanceId);
}
// ═══════════════════════════════════════════════════════════════════════════
// TIMER DE RECONEXÃO
// ═══════════════════════════════════════════════════════════════════════════
/** Cancela timer de reconexão pendente (evita reconexão duplicada) */
clearReconnectTimer(instanceId) {
const timer = this.reconnectTimers.get(instanceId);
if (timer) {
clearTimeout(timer);
this.reconnectTimers.delete(instanceId);
}
this.reconnectScheduledAt.delete(instanceId);
}
/**
* Agenda uma reconexão após `delay` ms.
*
* Centraliza o setTimeout + tracking do scheduledAt (usado pelo network
* probe pra saber se vale adiantar). Cancela qualquer timer anterior.
*/
scheduleReconnect(instanceId, tenantId, delay) {
this.clearReconnectTimer(instanceId);
const scheduledAt = Date.now() + delay;
this.reconnectScheduledAt.set(instanceId, scheduledAt);
const timer = setTimeout(() => {
this.reconnectScheduledAt.delete(instanceId);
this.connect(instanceId, tenantId);
}, delay);
this.reconnectTimers.set(instanceId, timer);
}
/**
* Classifica um erro de conexão como "rede transiente".
*
* Erros de DNS/TCP costumam resolver em segundos. Erros de status do
* WhatsApp (loggedOut, conflict, restartRequired) são categoria diferente
* e devem usar o backoff longo tradicional.
*/
isTransientNetworkError(err) {
if (!err)
return false;
const code = err.code || err.cause?.code || err.errno;
if (code && typeof code === 'string') {
const transient = [
'EAI_AGAIN', // DNS retry
'ENOTFOUND', // DNS não resolveu
'ECONNRESET',
'ECONNREFUSED',
'ETIMEDOUT',
'EHOSTUNREACH',
'ENETUNREACH',
'ENETDOWN',
'EPIPE',
];
if (transient.includes(code))
return true;
}
// Fallback: mensagem contém nome do host — é quase certo que é DNS/WS
const msg = (err.message || '').toString();
if (/getaddrinfo|web\.whatsapp\.com|WebSocket Error/i.test(msg))
return true;
return false;
}
// ═══════════════════════════════════════════════════════════════════════════
// NETWORK PROBE — Acorda reconexões quando a rede volta
// ═══════════════════════════════════════════════════════════════════════════
/**
* Inicia o network probe global.
*
* A cada NETWORK_PROBE_INTERVAL_MS, testa se o DNS resolve
* `web.whatsapp.com`. Se resolver E houver alguma instância com retry
* agendado para daqui a mais de PROBE_ADVANCE_THRESHOLD_MS, cancela o
* timer dessa instância e dispara o retry imediatamente.
*
* Por quê DNS em vez de HTTP? Zero bytes para o WhatsApp. DNS falha com
* EAI_AGAIN quando a rede está caída, e resolve rápido quando volta —
* é um sinal confiável de disponibilidade de rede sem tocar no WA.
*/
startNetworkProbe() {
if (this.networkProbeTimer)
return;
this.networkProbeTimer = setInterval(async () => {
// Curto-circuito: se não há retries pendentes, nem faz o DNS.
if (this.reconnectScheduledAt.size === 0)
return;
try {
await promises_2.default.lookup(PROBE_DNS_HOST);
}
catch {
return; // Rede ainda caída — não adianta nada, espera o tick seguinte.
}
// DNS OK → adianta qualquer retry que ainda falte > threshold
const now = Date.now();
for (const [instanceId, scheduledAt] of this.reconnectScheduledAt) {
const remaining = scheduledAt - now;
if (remaining <= PROBE_ADVANCE_THRESHOLD_MS)
continue;
const tenantId = this.tenantByInstance.get(instanceId);
if (!tenantId)
continue;
logger_1.logger.info({ instanceId, remainingMs: remaining }, '[NetworkProbe] Rede detectada — adiantando retry pendente');
this.clearReconnectTimer(instanceId);
// Dispara sem await — probe roda em interval e não deve bloquear
this.connect(instanceId, tenantId).catch((err) => logger_1.logger.error({ err, instanceId }, '[NetworkProbe] Falha ao adiantar retry'));
}
}, NETWORK_PROBE_INTERVAL_MS);
// Não segura o processo vivo só por causa do probe
this.networkProbeTimer.unref?.();
}
// ═══════════════════════════════════════════════════════════════════════════
// DESCONEXÃO MANUAL
// ═══════════════════════════════════════════════════════════════════════════
/**
* Desconecta uma instância manualmente (logout).
*
* Diferente de uma desconexão por erro — aqui o usuário está
* intencionalmente desconectando. Limpa todos os timers e
* faz logout no WhatsApp (invalidando a sessão).
*/
async disconnect(instanceId) {
const sock = this.sockets.get(instanceId);
if (!sock)
return;
this.clearReconnectTimer(instanceId);
this.stopWatchdog(instanceId);
this.reconnectAttempts.delete(instanceId);
// Logout manual zera o estado de cool-down: reconexão futura é um
// novo ciclo e não deve herdar o bloqueio do watchdog.
this.lastWatchdogKill.delete(instanceId);
this.lastActivity.delete(instanceId);
await sock.logout();
this.sockets.delete(instanceId);
this.contactHandlers.delete(instanceId);
this.msgHandlers.delete(instanceId);
logger_1.logger.info({ instanceId }, 'Instancia desconectada manualmente');
}
// ═══════════════════════════════════════════════════════════════════════════
// ACESSORES PÚBLICOS
// ═══════════════════════════════════════════════════════════════════════════
/** Retorna o socket Baileys ativo para uma instância (usado pelas rotas de envio) */
getSocket(instanceId) {
return this.sockets.get(instanceId);
}
/** Retorna o Socket.IO server (usado pelas rotas para emitir eventos) */
getIO() {
return this.io;
}
// ═══════════════════════════════════════════════════════════════════════════
// HELPERS INTERNOS
// ═══════════════════════════════════════════════════════════════════════════
/**
* Atualiza o status da instância no banco E no cache.
* Dupla escrita para que o frontend possa consultar status tanto
* via API (banco) quanto via polling rápido (cache DragonflyDB).
*/
async updateInstanceStatus(instanceId, status) {
await Promise.all([
prisma_1.prisma.instance.update({ where: { id: instanceId }, data: { status } })
.catch((err) => { if (err?.code !== 'P2025')
throw err; }),
dragonfly_1.dragonfly.set(CACHE_KEY_STATUS(instanceId), status),
]);
}
/** Remove arquivos de sessão do disco (após loggedOut) */
async deleteSessionFiles(instanceId) {
const sessionDir = path_1.default.resolve(env_1.env.BAILEYS_SESSIONS_PATH, instanceId);
await promises_1.default.rm(sessionDir, { recursive: true, force: true });
}
// ═══════════════════════════════════════════════════════════════════════════
// SINCRONIZAÇÃO DE CHATS
// ═══════════════════════════════════════════════════════════════════════════
/**
* Sincroniza lista de chats no banco.
*
* Chamado por: chats.upsert, chats.update, groups.upsert, groups.update,
* messaging-history.set
*
* Para cada chat:
* - Cria ou atualiza no banco com JID, nome (grupo), timestamps
* - Vincula ao Contact existente (se houver)
* - Sincroniza estado de pin/archive/unread do WhatsApp
*
* Otimização: busca todos os contatos existentes em uma única query
* e usa Map para lookup O(1) por JID.
*/
async syncChats(instanceId, tenantId, chats) {
if (chats.length === 0)
return;
// Busca todos os contatos existentes para os JIDs (1 query)
const jids = chats.map((c) => c.id).filter(Boolean);
const existingContacts = await prisma_1.prisma.contact.findMany({
where: { tenantId, instanceId, jid: { in: jids } },
select: { id: true, jid: true },
});
const contactIdByJid = new Map(existingContacts.map((c) => [c.jid, c.id]));
// Pré-carrega mapeamento LID → phone JID para chats @lid.
// ContactHandler.upsert já rodou antes (messaging-history.set processa
// contacts → chats → messages nessa ordem), então lidJid já está populado.
const lidJids = jids.filter((j) => j.endsWith('@lid'));
const lidToPhoneJid = new Map(); // @lid → @s.whatsapp.net
const lidToContactId = new Map(); // @lid → contactId
if (lidJids.length > 0) {
const resolvedByLid = await prisma_1.prisma.contact.findMany({
where: {
tenantId,
instanceId,
lidJid: { in: lidJids },
NOT: { jid: { endsWith: '@lid' } }, // só aceita se resolveu para phone JID
},
select: { id: true, jid: true, lidJid: true },
});
for (const rc of resolvedByLid) {
if (rc.lidJid) {
lidToPhoneJid.set(rc.lidJid, rc.jid);
lidToContactId.set(rc.lidJid, rc.id);
}
}
}
for (const chat of chats) {
const rawJid = chat.id;
if (!rawJid)
continue;
// Descarta JIDs de sistema: broadcasts, newsletters e conta PSA (0@s.whatsapp.net)
if (rawJid.includes('@broadcast') || rawJid.endsWith('@newsletter') || rawJid.startsWith('0@'))
continue;
// ── Resolução LID → phone JID ─────────────────────────────────────
// Chats @lid são artefatos do protocolo WhatsApp multi-device:
// o JID real do contato é @s.whatsapp.net.
// Se já temos o mapeamento (ContactHandler rodou antes), usa o phone JID.
// Se não temos resolução, descarta — seria filtrado pelo ChatRepository.
let jid = rawJid;
if (rawJid.endsWith('@lid')) {
const phoneJid = lidToPhoneJid.get(rawJid);
if (phoneJid) {
jid = phoneJid;
// Garante que o contactId mapeado seja usado abaixo
const resolvedContactId = lidToContactId.get(rawJid) ?? contactIdByJid.get(jid);
if (resolvedContactId)
contactIdByJid.set(jid, resolvedContactId);
}
else {
// Sem resolução disponível: chat será criado via handleHistory quando
// a mensagem chegar com senderPn resolvível.
logger_1.logger.debug({ rawJid, instanceId }, '[syncChats] @lid sem resolução — descartando');
continue;
}
}
try {
// Converte timestamp Unix (segundos) para Date
const lastAt = chat.conversationTimestamp
? new Date(Number(chat.conversationTimestamp) * 1000)
: undefined;
const isGroup = jid.endsWith('@g.us');
// Nome do chat: apenas grupos têm nome (subject) — individuais usam Contact.name
const groupName = isGroup ? (chat.name ?? null) : null;
const contactId = contactIdByJid.get(jid) ?? null;
await prisma_1.prisma.chat.upsert({
where: { tenantId_instanceId_jid: { tenantId, instanceId, jid } },
create: {
tenantId,
instanceId,
jid,
name: groupName,
unreadCount: chat.unreadCount ?? 0,
lastMessageAt: lastAt ?? null,
isPinned: !!chat.pinned,
isArchived: chat.archived ?? false,
...(contactId && { contactId }),
},
update: {
...(groupName && { name: groupName }),
...(contactId && { contactId }),
// Só aplica campos que vieram explicitamente no evento.
// chats.upsert pode ser parcial (nova mensagem, metadata) — se
// pinned/archived/unreadCount estiverem ausentes, não sobrescreve
// o valor salvo no banco.
...(chat.unreadCount !== undefined && { unreadCount: chat.unreadCount }),
...(lastAt && { lastMessageAt: lastAt }),
...(chat.pinned !== undefined && { isPinned: !!chat.pinned }),
...(chat.archived !== undefined && { isArchived: !!chat.archived }),
},
});
}
catch (err) {
logger_1.logger.error({ err, jid }, '[WhatsApp] Erro ao sincronizar chat');
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// HANDLER DE ATUALIZAÇÕES PARCIAIS DE CHAT (chats.update)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Processa atualizações parciais de chats (pin, archive, unread).
*
* Diferente de syncChats (usado para bulk/history), este método:
* 1. Só atualiza campos presentes na atualização (sem sobrescrever com defaults)
* 2. Emite chat:upsert via Socket.IO para atualizar o frontend em tempo real
*
* O evento chats.update do Baileys traz objetos parciais — apenas os campos
* que mudaram. Por isso não podemos usar ?? 0 para unreadCount, pois isso
* sobrescreveria o valor correto com zero.
*/
async handleChatsUpdate(instanceId, tenantId, updates) {
for (const update of updates) {
const jid = update.id;
if (!jid || jid.endsWith('@lid'))
continue;
try {
// Monta patch com apenas os campos presentes na atualização
const patch = {};
if (update.pinned !== undefined)
patch.isPinned = !!update.pinned;
if (update.archived !== undefined)
patch.isArchived = !!update.archived;
if (update.unreadCount !== undefined)
patch.unreadCount = update.unreadCount;
if (Object.keys(patch).length === 0)
continue;
const chat = await prisma_1.prisma.chat.update({
where: { tenantId_instanceId_jid: { tenantId, instanceId, jid } },
data: patch,
include: {
contact: {
select: {
name: true, verifiedName: true, notify: true,
phone: true, avatarUrl: true,
scoreReputacao: true, flagRestricao: true,
},
},
},
}).catch(() => null);
if (!chat)
continue;
// Emite para o frontend atualizar em tempo real
const c = chat.contact;
const isGroup = jid.endsWith('@g.us');
const displayName = isGroup
? (chat.name ?? null)
: (c?.name ?? c?.verifiedName ?? c?.notify ?? null);
this.io.to(`tenant:${tenantId}`).emit('chat:upsert', {
id: chat.id,
tenantId: chat.tenantId,
instanceId: chat.instanceId,
jid: chat.jid,
name: chat.name ?? null,
unreadCount: chat.unreadCount,
lastMessageAt: chat.lastMessageAt,
isPinned: chat.isPinned,
isArchived: chat.isArchived,
botPaused: chat.botPaused,
contact: c
? {
name: displayName,
phone: c.phone,
avatarUrl: c.avatarUrl,
scoreReputacao: c.scoreReputacao,
flagRestricao: c.flagRestricao,
}
: isGroup
? { name: displayName, phone: null, avatarUrl: null, scoreReputacao: 0, flagRestricao: false }
: null,
lastMessage: null,
});
}
catch (err) {
logger_1.logger.error({ err, jid }, '[WhatsApp] Erro ao aplicar chats.update');
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// DEBUG LOG (página /baileys no frontend)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Emite evento de debug para a página /baileys do frontend.
*
* Permite monitorar em tempo real todos os eventos Baileys
* sem precisar acessar os logs do servidor.
*/
baileysLog(tenantId, instanceId, event, data) {
this.io.to(`tenant:${tenantId}`).emit('baileys:log', {
timestamp: new Date().toISOString(),
sessionId: instanceId,
event,
data,
});
}
}
exports.WhatsAppConnectionManager = WhatsAppConnectionManager;
//# sourceMappingURL=WhatsAppConnectionManager.js.map