FEAT: Ajustes realizados para criação de templates de mensagens enviadas
All checks were successful
Deploy Dev / deploy (push) Successful in 4s

This commit is contained in:
Rafael Alves Lopes 2026-05-18 13:28:17 -03:00
parent 8c28e9c479
commit 21a81282d5
3 changed files with 208 additions and 2 deletions

132
docs/chat-whatsapp.md Normal file
View File

@ -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.

View File

@ -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);
}
}

View File

@ -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];
}
}