From 21a81282d58e45b1eba2568b753f08e8eedf8ccb Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Mon, 18 May 2026 13:28:17 -0300 Subject: [PATCH] =?UTF-8?q?FEAT:=20Ajustes=20realizados=20para=20cria?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20templates=20de=20mensagens=20enviadas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/chat-whatsapp.md | 132 ++++++++++++++++++++ src/modules/whatsapp/whatsapp.controller.ts | 15 +++ src/modules/whatsapp/whatsapp.service.ts | 63 +++++++++- 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 docs/chat-whatsapp.md diff --git a/docs/chat-whatsapp.md b/docs/chat-whatsapp.md new file mode 100644 index 0000000..a42918d --- /dev/null +++ b/docs/chat-whatsapp.md @@ -0,0 +1,132 @@ +# Modulo de Chat WhatsApp (Backend) + +## Visao geral + +O modulo de WhatsApp do backend e desenvolvido em **NestJS** e utiliza a biblioteca **whatsapp-web.js** (que roda uma instancia headless do Chromium via **Puppeteer**) para integrar a aplicacao diretamente com o WhatsApp Web em tempo real. + +A arquitetura e constituida por quatro pilares: +1. **Puppeteer Client**: Controla o WhatsApp Web, gera o QR Code para autenticacao e escuta eventos de novas mensagens. +2. **WebSocket Gateway**: Transmite eventos em tempo real (novas mensagens, atualizacao de status) para o frontend usando Socket.io. +3. **Persistencia Hibrida**: Mantem conversas indexadas localmente via JSON para performance e o status de atribuicao de atendimento gravado no banco relacional (PostgreSQL). +4. **Servico de Atribuicao**: Gerencia o vinculo dos chats com os atendentes (`usuarios`) e suas respectivas `areas` operacionais. + +--- + +## Fluxo de Eventos e Mensagens + +### 1. Inicializacao e Conexao +* O backend inicia o cliente do `whatsapp-web.js` na porta configurada. +* Se nenhuma sessao ativa for encontrada na pasta `/whatsapp-session`, o cliente gera um QR Code em formato Base64. +* Esse QR Code e enviado via WebSocket (`qr`) para o frontend configurar o dispositivo. +* Uma vez conectado, o status muda para `CONNECTED` e a pasta de sessao e gravada localmente. + +### 2. Captura de Mensagens (`message_create`) +Utilizamos o evento global `message_create` para capturar tanto mensagens recebidas do cliente quanto mensagens enviadas pelo proprio atendente (seja pela tela ou por outro dispositivo sincronizado): + +```text +Puppeteer (Message) + -> Trata de-duplicacao ou broadcast + -> Se possuir midia, baixa os buffers em tempo real + -> Transmite via WebSocket 'message' para o frontend + -> Salva ou atualiza a conversa na persistencia local JSON +``` + +--- + +## Persistencia e Banco de Dados + +### 1. Cache Local JSON (`whatsapp-chats-persist.json`) +Para evitar sobrecarregar o banco relacional e garantir latencias imperceptiveis de scroll, os chats e seus metadados de visualizacao sao armazenados de forma persistente em um arquivo JSON local na raiz do backend. + +### 2. Tabela de Atribuicao de Chat (`whatsapp_chat_atribuicoes`) +O controle de quem esta atendendo qual chat e estritamente transacional e reside no banco PostgreSQL. + +A estrutura da tabela e criada pela migration `004_whatsapp.sql`: + +```sql +CREATE TABLE IF NOT EXISTS whatsapp_chat_atribuicoes ( + id SERIAL PRIMARY KEY, + chat_id VARCHAR(255) NOT NULL, + user_id SERIAL REFERENCES usuarios(id) ON DELETE CASCADE, + area_id SERIAL REFERENCES areas(id) ON DELETE SET NULL, + assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE(chat_id) +); +``` + +#### Regras de Atribuicao: +* `chat_id`: Identificador unico da conversa no WhatsApp (ex: `5511999999999@c.us` ou `...@lid`). +* `user_id`: ID inteiro (`usuarios.id`) do atendente que assumiu. +* `area_id`: ID inteiro (`areas.id`) do setor sob o qual o atendimento esta sendo prestado (ex: `1` para Suporte). + +--- + +## Endpoints do Modulo + +Base path: `/whatsapp` + +### 1. Enviar Mensagem +```http +POST /whatsapp/send +Content-Type: application/json +``` +**Payload**: +```json +{ + "to": "5511999999999@c.us", + "message": "Ola, tudo bem?", + "media": { + "data": "base64String...", + "mimetype": "image/png", + "filename": "comprovante.png" + } +} +``` + +### 2. Atribuir Chat +```http +POST /whatsapp/assign +Content-Type: application/json +``` +**Payload**: +```json +{ + "chatId": "5511999999999@c.us", + "userId": 4, + "areaId": 1 +} +``` + +### 3. Liberar Chat +```http +DELETE /whatsapp/release/:chatId +``` + +--- + +## Limitacoes e Solucoes de Bugs + +### 1. Payload Too Large (Upload de Midia) +Para permitir o envio de imagens, videos e audios pesados codificados em Base64, o limite padrao de payload do NestJS foi estendido para **50MB** no `main.ts` tanto para formato JSON quanto para `urlencoded`. + +### 2. Nomes Numericos do WhatsApp (Smart Name Resolution) +Devido a latencias do WhatsApp Web, as mensagens recebidas as vezes reportam apenas o telefone/JID como nome do contato. O servico de WhatsApp contem uma camada reativa de reparacao automatica: +* Checa se o nome e puramente numerico. +* Se for, faz uma chamada em background para o Puppeteer (`client.getContactById`) para obter o `notifyName` real do cliente e atualizar o cache local. + +--- + +## Como Rodar e Testar + +### Requisitos locais +* Node.js v18+ +* PostgreSQL ativo com as migrations executadas +* Google Chrome ou Chromium instalado (o Puppeteer tentara usar o bundle local se omitido) + +### Executando em desenvolvimento +```bash +cd backend +npm run dev +``` + +Os logs do NestJS no terminal indicarao as fases de inicializacao do Puppeteer, geracao de QR Code ou recuperacao de sessao ativa. diff --git a/src/modules/whatsapp/whatsapp.controller.ts b/src/modules/whatsapp/whatsapp.controller.ts index 5806929..90ae786 100644 --- a/src/modules/whatsapp/whatsapp.controller.ts +++ b/src/modules/whatsapp/whatsapp.controller.ts @@ -48,4 +48,19 @@ export class WhatsappController { async getAssignment(@Param('chatId') chatId: string) { return this.assignmentService.getAssignment(chatId); } + + @Get('templates') + async getTemplates() { + return this.whatsappService.getTemplates(); + } + + @Post('templates') + async saveTemplate(@Body() body: { name: string; content: string }) { + return this.whatsappService.saveTemplate(body.name, body.content); + } + + @Post('templates/update/:id') + async updateTemplate(@Param('id') id: string, @Body() body: { name: string; content: string }) { + return this.whatsappService.updateTemplate(Number(id), body.name, body.content); + } } diff --git a/src/modules/whatsapp/whatsapp.service.ts b/src/modules/whatsapp/whatsapp.service.ts index 7aec6b8..03b509d 100644 --- a/src/modules/whatsapp/whatsapp.service.ts +++ b/src/modules/whatsapp/whatsapp.service.ts @@ -2,6 +2,7 @@ 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 { DatabaseService } from '../../infra/database/database.service'; import * as fs from 'fs'; import * as path from 'path'; @@ -14,10 +15,35 @@ export class WhatsappService implements OnModuleInit { constructor( private readonly gateway: WhatsappGateway, - private readonly assignmentService: WhatsappAssignmentService + private readonly assignmentService: WhatsappAssignmentService, + private readonly db: DatabaseService ) {} - onModuleInit() { + async onModuleInit() { + // Inicialização da tabela de templates no banco + try { + await this.db.query(` + CREATE TABLE IF NOT EXISTS whatsapp_templates ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + content TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + ); + `); + await this.db.query(` + INSERT INTO whatsapp_templates (name, content) VALUES + ('aviso_fatura', 'Olá, {nome}. Estamos entrando em contato para lembrá-lo que a sua fatura está programada para {data}.'), + ('boas_vindas', 'Olá, {nome}! Obrigado por entrar em contato conosco. Como podemos te ajudar hoje?'), + ('lembrete_consulta', 'Olá, {nome}. Gostaríamos de confirmar o seu agendamento para {data}. Está confirmado?'), + ('suporte_tecnico', 'Olá, {nome}. Sou o atendente e irei te auxiliar no seu suporte sob protocolo {protocolo}.') + ON CONFLICT (name) DO NOTHING; + `); + this.logger.log('Tabela de templates do WhatsApp verificada/criada com sucesso no PostgreSQL!'); + } catch (err) { + this.logger.error('Erro ao verificar/criar tabela de templates no PostgreSQL:', err); + } + this.logger.log('Inicializando WhatsApp Client...'); this.client = new Client({ @@ -101,12 +127,24 @@ export class WhatsappService implements OnModuleInit { }); // Salva ou atualiza a conversa na persistência híbrida + const persistentChats = await this.loadPersistentChats(); + const isNewNumber = !persistentChats[remoteJid]; + 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 }); + + if (!msg.fromMe && isNewNumber) { + try { + this.logger.log(`Auto-resposta de boas vindas enviada para novo contato: ${remoteJid}`); + await this.client.sendMessage(remoteJid, "Olá! Seja bem-vindo a Sothis Telecom Como podemos te ajudar?"); + } catch (err) { + this.logger.error(`Erro ao enviar auto-resposta de boas vindas para ${remoteJid}:`, err); + } + } }); this.client.initialize(); @@ -307,4 +345,25 @@ export class WhatsappService implements OnModuleInit { return sentMsg; } + + async getTemplates() { + const res = await this.db.query('SELECT * FROM whatsapp_templates ORDER BY id ASC'); + return res.rows; + } + + async saveTemplate(name: string, content: string) { + const res = await this.db.query( + 'INSERT INTO whatsapp_templates (name, content) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET content = EXCLUDED.content, updated_at = CURRENT_TIMESTAMP RETURNING *', + [name, content] + ); + return res.rows[0]; + } + + async updateTemplate(id: number, name: string, content: string) { + const res = await this.db.query( + 'UPDATE whatsapp_templates SET name = $1, content = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3 RETURNING *', + [name, content, id] + ); + return res.rows[0]; + } }