Files
scoreodonto.com/backend/dist/modules/whatsapp/whatsapp.routes.js
T

170 lines
7.9 KiB
JavaScript

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildWhatsAppRoutes = buildWhatsAppRoutes;
const express_1 = require("express");
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const InstanceRepository_1 = require("./repositories/InstanceRepository");
const dragonfly_1 = require("../../infra/cache/dragonfly");
const prisma_1 = require("../../infra/database/prisma");
const logger_1 = require("../../config/logger");
const StorageProvider_1 = require("../../core/StorageProvider");
const ENGINE_CHANGE_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutos entre trocas
function buildWhatsAppRoutes(manager) {
const router = (0, express_1.Router)();
const repo = new InstanceRepository_1.InstanceRepository();
// Listar instâncias do tenant
router.get('/', async (req, res) => {
const tenantId = req.tenantId;
const instances = await repo.findAllByTenant(tenantId);
res.json(instances);
});
// Criar instância
router.post('/', async (req, res) => {
const tenantId = req.tenantId;
const { name } = req.body;
if (!name) {
res.status(400).json({ error: 'name é obrigatório' });
return;
}
const instance = await repo.create(tenantId, name);
res.status(201).json(instance);
});
// Conectar instância (emite QR via Socket.IO)
router.post('/:id/connect', async (req, res) => {
const tenantId = req.tenantId;
const id = req.params['id'];
const instance = await repo.findById(id);
if (!instance || instance.tenantId !== tenantId) {
res.status(404).json({ error: 'Instância não encontrada' });
return;
}
// Não aguarda — a conexão ocorre em background e o QR chega via Socket.IO
manager.connect(id, tenantId).catch((err) => {
console.error('[Connect] Falha ao iniciar conexão:', err);
});
res.json({ message: 'Conectando... aguarde o QR Code via WebSocket' });
});
// Desconectar instância
router.post('/:id/disconnect', async (req, res) => {
const tenantId = req.tenantId;
const id = req.params['id'];
const instance = await repo.findById(id);
if (!instance || instance.tenantId !== tenantId) {
res.status(404).json({ error: 'Instância não encontrada' });
return;
}
await manager.disconnect(id);
res.json({ message: 'Desconectado' });
});
// QR Code atual (fallback REST — normalmente via Socket.IO)
router.get('/:id/qr', async (req, res) => {
const tenantId = req.tenantId;
const id = req.params['id'];
const instance = await repo.findById(id);
if (!instance || instance.tenantId !== tenantId) {
res.status(404).json({ error: 'Instância não encontrada' });
return;
}
const qrBase64 = await dragonfly_1.dragonfly.get(`instance:${id}:qr`);
if (!qrBase64) {
res.status(404).json({ error: 'QR Code não disponível. Inicie a conexão primeiro.' });
return;
}
res.json({ qrBase64 });
});
// Deletar instância
router.delete('/:id', async (req, res) => {
const tenantId = req.tenantId;
const id = req.params['id'];
const instance = await repo.findById(id);
if (!instance || instance.tenantId !== tenantId) {
res.status(404).json({ error: 'Instância não encontrada' });
return;
}
// 1. Desconectar e deslogar a sessão do WhatsApp se estiver ativa
await manager.disconnect(id).catch(() => { });
// 2. Garantir a exclusão física da pasta de credenciais da sessão no servidor
await manager.deleteSessionFiles(id).catch(() => { });
// 3. Garantir a exclusão física da pasta de mídia local ./media/{instanceId} no servidor
const localMediaDir = path_1.default.resolve('./media', id);
await promises_1.default.rm(localMediaDir, { recursive: true, force: true }).catch((err) => {
console.error(`[deleteInstance] Erro ao deletar pasta local de mídia da instância ${id}:`, err);
});
// 4. Apagar do banco de dados
await repo.delete(id);
// 5. Deleta mídias associadas à instância no storage remoto (Wasabi/S3) de forma assíncrona
StorageProvider_1.storageProvider.deletePrefix(`whatsapp/${tenantId}/${id}/`).catch((err) => {
console.error(`[StorageProvider] Erro ao deletar mídias da instância ${id}:`, err);
});
res.json({ message: 'Instância removida com sucesso de todos os storages e servidores' });
});
// Estatísticas de mensagens da instância (contagens do banco)
router.get('/:id/stats', async (req, res) => {
const tenantId = req.tenantId;
const id = req.params['id'];
const instance = await repo.findById(id);
if (!instance || instance.tenantId !== tenantId) {
res.status(404).json({ error: 'Instância não encontrada' });
return;
}
const [received, sent, media, chats] = await Promise.all([
prisma_1.prisma.message.count({ where: { instanceId: id, fromMe: false } }),
prisma_1.prisma.message.count({ where: { instanceId: id, fromMe: true } }),
prisma_1.prisma.message.count({
where: {
instanceId: id,
type: { in: ['IMAGE', 'VIDEO', 'AUDIO', 'DOCUMENT', 'STICKER'] },
},
}),
prisma_1.prisma.chat.count({ where: { instanceId: id } }),
]);
res.json({ received, sent, media, chats });
});
// Trocar engine de uma instância (com cooldown + auditoria)
router.patch('/:id/engine', async (req, res) => {
const tenantId = req.tenantId;
const id = req.params['id'];
const { engine } = req.body;
if (engine !== 'infinite' && engine !== 'official') {
res.status(400).json({ error: 'engine deve ser "infinite" ou "official"' });
return;
}
const instance = await repo.findById(id);
if (!instance || instance.tenantId !== tenantId) {
res.status(404).json({ error: 'Instância não encontrada' });
return;
}
// Cooldown: bloqueia nova troca por 5 min após a última
const cooldownKey = `instance:${id}:engine_change_lock`;
const lockedAt = await dragonfly_1.dragonfly.get(cooldownKey);
if (lockedAt) {
const remaining = Math.ceil((ENGINE_CHANGE_COOLDOWN_MS - (Date.now() - Number(lockedAt))) / 1000);
res.status(429).json({ error: `Aguarde ${remaining}s antes de trocar a engine novamente` });
return;
}
const fromEngine = instance.engine ?? 'infinite';
if (fromEngine === engine) {
res.status(200).json({ message: 'Engine já está definida para este valor', engine });
return;
}
// Persiste a troca
await prisma_1.prisma.instance.update({ where: { id }, data: { engine } });
// Audit log estruturado
logger_1.logger.info({ tenantId, instanceId: id, fromEngine, toEngine: engine }, '[EngineChange] Troca de engine auditada');
// Aplica cooldown (5 min via TTL no Redis)
await dragonfly_1.dragonfly.psetex(cooldownKey, ENGINE_CHANGE_COOLDOWN_MS, String(Date.now()));
// Reconecta a instância com a nova engine
const isConnected = manager.getSocket(id) != null;
if (isConnected) {
await manager.disconnect(id).catch(() => { });
manager.connect(id, tenantId).catch(() => { });
}
res.json({ message: 'Engine atualizada', engine, reconnecting: isConnected });
});
return router;
}