import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { Client, LocalAuth, MessageMedia } from 'whatsapp-web.js'; import { WhatsappGateway } from './whatsapp.gateway'; import { WhatsappAssignmentService } from './whatsapp-assignment.service'; import * as fs from 'fs'; import * as path from 'path'; @Injectable() export class WhatsappService implements OnModuleInit { private client: Client; private readonly logger = new Logger(WhatsappService.name); private status: 'DISCONNECTED' | 'AWAITING_QR' | 'CONNECTED' = 'DISCONNECTED'; private currentQr: string | null = null; constructor( private readonly gateway: WhatsappGateway, private readonly assignmentService: WhatsappAssignmentService ) {} onModuleInit() { this.logger.log('Inicializando WhatsApp Client...'); this.client = new Client({ authStrategy: new LocalAuth({ dataPath: './whatsapp-session' }), puppeteer: { headless: true, executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--disable-gpu'] }, webVersionCache: { type: 'none' } }); this.client.on('qr', (qr) => { this.logger.log('QR Code recebido. Envie para o frontend.'); this.status = 'AWAITING_QR'; this.currentQr = qr; this.gateway.emitQrCode(qr); this.gateway.emitStatus(this.status); }); this.client.on('ready', () => { this.logger.log('WhatsApp Web Conectado!'); this.status = 'CONNECTED'; this.currentQr = null; this.gateway.emitStatus(this.status); }); this.client.on('authenticated', () => { this.logger.log('WhatsApp Autenticado'); }); this.client.on('auth_failure', (msg) => { this.logger.error('Falha na Autenticação do WhatsApp', msg); this.status = 'DISCONNECTED'; this.gateway.emitStatus(this.status); }); this.client.on('disconnected', (reason) => { this.logger.warn('WhatsApp Desconectado', reason); this.status = 'DISCONNECTED'; this.gateway.emitStatus(this.status); }); // Mudar para message_create captura tanto mensagens recebidas quanto enviadas no WhatsApp this.client.on('message_create', async (msg) => { if (msg.from === 'status@broadcast') return; const remoteJid = msg.id.remote || (msg.fromMe ? msg.to : msg.from); this.logger.log(`Mensagem registrada (fromMe: ${msg.fromMe}) remote: ${remoteJid} - ${msg.body}`); let mediaData: any = null; if (msg.hasMedia) { try { this.logger.log(`Baixando mídia em tempo real de ${msg.id._serialized}...`); const media = await msg.downloadMedia(); if (media) { mediaData = { mimetype: media.mimetype, data: media.data, filename: media.filename || 'arquivo' }; } } catch (err) { this.logger.error(`Erro ao baixar mídia em tempo real de ${msg.id._serialized}:`, err); } } // Transmite a mensagem em tempo real para o frontend this.gateway.emitNewMessage({ from: remoteJid, body: msg.body, timestamp: msg.timestamp, isGroupMsg: remoteJid.endsWith('@g.us'), id: msg.id._serialized, fromMe: msg.fromMe, notifyName: msg['_data']?.notifyName || '', hasMedia: msg.hasMedia, media: mediaData }); // Salva ou atualiza a conversa na persistência híbrida await this.addOrUpdatePersistentChat(remoteJid, { name: msg['_data']?.notifyName || remoteJid.split('@')[0], preview: msg.hasMedia ? `[Mídia: ${mediaData?.filename || 'Arquivo'}]` : (msg.body || '[Mídia]'), timestamp: msg.timestamp, unreadCount: msg.fromMe ? 0 : 1 }); }); this.client.initialize(); } private getPersistFilePath() { return path.join(process.cwd(), 'whatsapp-chats-persist.json'); } private async loadPersistentChats(): Promise { try { const filepath = this.getPersistFilePath(); if (!fs.existsSync(filepath)) { return {}; } const data = fs.readFileSync(filepath, 'utf-8'); return JSON.parse(data); } catch (err) { this.logger.error('Erro ao ler chats persistentes:', err); return {}; } } private async savePersistentChats(chats: any): Promise { try { const filepath = this.getPersistFilePath(); fs.writeFileSync(filepath, JSON.stringify(chats, null, 2), 'utf-8'); } catch (err) { this.logger.error('Erro ao salvar chats persistentes:', err); } } private async addOrUpdatePersistentChat(chatId: string, data: { name?: string, preview?: string, timestamp?: number, unreadCount?: number }) { if (chatId === 'status@broadcast' || chatId.endsWith('@g.us')) return; const chats = await this.loadPersistentChats(); const existing = chats[chatId] || {}; const isNumber = (val: string) => /^\d+$/.test(val); let finalName = existing.name || data.name || chatId.split('@')[0]; if (data.name && !isNumber(data.name)) { finalName = data.name; } if (isNumber(finalName) && this.status === 'CONNECTED') { try { const contact = await this.client.getContactById(chatId); if (contact) { if (contact.name && !isNumber(contact.name)) { finalName = contact.name; } else if (contact.pushname && !isNumber(contact.pushname)) { finalName = contact.pushname; } } } catch (err) { // Ignorar erros na consulta do Puppeteer } } chats[chatId] = { id: { server: chatId.split('@')[1] || 'c.us', user: chatId.split('@')[0], _serialized: chatId }, name: finalName, isGroup: false, isReadOnly: false, unreadCount: data.unreadCount !== undefined ? data.unreadCount : (existing.unreadCount || 0), timestamp: data.timestamp || existing.timestamp || Math.floor(Date.now() / 1000), archived: false, pinned: false, isLocked: false, isMuted: false, preview: data.preview || existing.preview || '' }; await this.savePersistentChats(chats); } getStatus() { return this.status; } getCurrentQr() { return this.currentQr; } async getChats() { if (this.status !== 'CONNECTED') return []; let liveChats: any[] = []; try { liveChats = await this.client.getChats(); } catch (err) { this.logger.error('Erro ao chamar client.getChats():', err); } const persistentChatsObj = await this.loadPersistentChats(); const mergedChatsMap = new Map(); // Adiciona os chats persistentes locais primeiro Object.values(persistentChatsObj).forEach((c: any) => { mergedChatsMap.set(c.id._serialized, c); }); // Adiciona/Mescla com os chats em tempo real do Puppeteer liveChats.forEach((c: any) => { if (!c.isGroup && c.id.server === 'c.us') { const serializedId = c.id._serialized; const existingPersistent = persistentChatsObj[serializedId] || {}; const isNumber = (val: string) => /^\d+$/.test(val); let finalName = c.name || existingPersistent.name || c.id.user; if (isNumber(c.name) && existingPersistent.name && !isNumber(existingPersistent.name)) { finalName = existingPersistent.name; } mergedChatsMap.set(serializedId, { id: c.id, name: finalName, isGroup: c.isGroup, isReadOnly: c.isReadOnly || false, unreadCount: c.unreadCount !== undefined ? c.unreadCount : (existingPersistent.unreadCount || 0), timestamp: c.timestamp || existingPersistent.timestamp || Math.floor(Date.now() / 1000), archived: c.archived || false, pinned: c.pinned || false, isLocked: c.isLocked || false, isMuted: c.isMuted || false, preview: c.lastMessage ? (c.lastMessage.body || '[Mídia]') : (existingPersistent.preview || '') }); } }); const conversas = Array.from(mergedChatsMap.values()); // Ordenar chats pelo timestamp mais recente conversas.sort((a, b) => b.timestamp - a.timestamp); // Buscar todas as atribuições para enriquecer os chats return Promise.all(conversas.map(async chat => { const assignment = await this.assignmentService.getAssignment(chat.id._serialized); return { ...chat, assignment: assignment || null }; })); } async getChatMessages(chatId: string) { if (this.status !== 'CONNECTED') return []; const chat = await this.client.getChatById(chatId); return chat.fetchMessages({ limit: 50 }); } async getMessageMedia(chatId: string, messageId: string) { if (this.status !== 'CONNECTED') throw new Error('WhatsApp não está conectado'); this.logger.log(`Buscando mídia do chat ${chatId}, mensagem ${messageId}`); const chat = await this.client.getChatById(chatId); const messages = await chat.fetchMessages({ limit: 50 }); const msg = messages.find(m => m.id._serialized === messageId); if (msg && msg.hasMedia) { this.logger.log(`Baixando mídia para mensagem ${messageId}...`); const media = await msg.downloadMedia(); if (media) { return { mimetype: media.mimetype, data: media.data, filename: media.filename || 'arquivo' }; } } throw new Error('Mídia não encontrada para esta mensagem'); } async sendMessage(to: string, message: string, media?: { data: string; mimetype: string; filename?: string }) { if (this.status !== 'CONNECTED') throw new Error('WhatsApp não está conectado'); let sentMsg; if (media) { this.logger.log(`Enviando mídia para ${to}: ${media.filename} (${media.mimetype})`); const messageMedia = new MessageMedia(media.mimetype, media.data, media.filename); sentMsg = await this.client.sendMessage(to, messageMedia, { caption: message }); } else { sentMsg = await this.client.sendMessage(to, message); } // Sincronizar na persistência também! await this.addOrUpdatePersistentChat(to, { name: to.split('@')[0], preview: media ? `[Mídia: ${media.filename || 'Arquivo'}]` : message, timestamp: Math.floor(Date.now() / 1000), unreadCount: 0 }); return sentMsg; } }