170 lines
7.9 KiB
JavaScript
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;
|
|
}
|