const db = require('../database');
const ffmpegPath = require('ffmpeg-static');
const { execFile } = require('child_process');
const path = require('path');
const fs = require('fs');
/**
* Converte buffer de áudio WebM para Ogg Opus usando ffmpeg
*/
function convertWebmToOgg(inputBuffer) {
return new Promise(function(resolve, reject) {
var tmpDir = path.join(__dirname, '../../uploads/audio');
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
var inputPath = path.join(tmpDir, 'input_' + Date.now() + '.webm');
var outputPath = path.join(tmpDir, 'output_' + Date.now() + '.ogg');
fs.writeFileSync(inputPath, inputBuffer);
execFile(ffmpegPath, [
'-y',
'-i', inputPath,
'-c:a', 'libopus',
'-b:a', '16k',
'-ar', '16000',
'-ac', '1',
outputPath
], { timeout: 30000 }, function(err) {
// Limpa arquivo temporário de entrada
try { fs.unlinkSync(inputPath); } catch(e) {}
if (err) {
try { fs.unlinkSync(outputPath); } catch(e) {}
return reject(err);
}
var outputBuffer = fs.readFileSync(outputPath);
try { fs.unlinkSync(outputPath); } catch(e) {}
resolve(outputBuffer);
});
});
}
/**
* Valida se um buffer tem cabeçalho de arquivo de mídia válido
* (defesa: rejeita JSON, HTML, ou dados criptografados salvos por engano)
* @param {Buffer} buf
* @param {string} mimeType
* @returns {boolean}
*/
function validarHeaderMidiaServida(buf, mimeType) {
if (!buf || buf.length < 4) return false;
var headerHex = buf.slice(0, 4).toString('hex');
// Valida headers de audio conhecidos
var validHeaders = ['4f676753', // OggS (ogg/opus)
'52494646', // RIFF (wav)
'66747970', // ftyp (mp4/m4a)
'494433', // ID3 (mp3)
'1a45dfa3', // WebM/Matroska EBML
'fffb', 'fff3', 'fffa', 'fff2']; // MP3 frames
for (var vh of validHeaders) {
if (headerHex.startsWith(vh)) return true;
}
// Para imagens: JPEG (FF D8 FF), PNG (89504e47), GIF (47494638), WebP (52494646 ja coberto)
if (mimeType.startsWith('image/')) {
if (headerHex.startsWith('ffd8ff') || headerHex.startsWith('89504e47') ||
headerHex.startsWith('47494638')) return true;
}
// Para documentos: ZIP/Office (504b)
if (mimeType.startsWith('application/')) {
if (headerHex.startsWith('504b')) return true;
}
return false;
}
class ChatController {
/**
* Página do chat
* GET /app/:alias/company/:empresaId/conversation/:conversaId
*/
static chatPage(req, res) {
res.sendFile(path.resolve(__dirname, '../public/chat.html'));
}
/**
* Listar conversas (sidebar esquerda)
* GET /api/:alias/conversations?empresaId=&status=A,E
*/
static async listConversations(req, res) {
try {
const { alias } = req.params;
const empresaId = parseInt(req.query.empresaId) || req.user?.empresas?.[0];
const statusFilter = req.query.status || 'A,E';
const filter = req.query.filter || ''; // 'mine', 'unassigned', 'all'
if (!empresaId || !req.user?.empresas?.some(function(e) { return Number(e) === Number(empresaId); }))
return res.status(403).json({ success: false, error: 'Sem permissão.' });
// Status via parâmetros (evita SQL injection). Aceita apenas A/E/F.
const statusList = statusFilter.split(',').map(s => s.trim().toUpperCase()).filter(s => /^[AEF]$/.test(s));
if (statusList.length === 0) statusList.push('A', 'E');
const statusPh = statusList.map(() => '?').join(',');
const params = [empresaId];
statusList.forEach(s => params.push(s));
let extraFilter = '';
if (filter === 'mine') {
extraFilter = ' AND c.CON_USUARIO_ID = ?';
params.push(req.user?.id || 0);
} else if (filter === 'unassigned') {
extraFilter = ' AND c.CON_USUARIO_ID IS NULL AND c.CON_EQUIPE_ID IS NULL';
} else if (filter === 'equipe') {
// Busca equipes do usuário logado
const minhasEquipes = await db.query(alias,
'SELECT EQU_EQUIPE_ID FROM CHATC2_USU_EQUIPES WHERE EQU_USUARIO_ID = ?', [req.user?.id]);
if (minhasEquipes.length > 0) {
const eqPh = minhasEquipes.map(() => '?').join(',');
extraFilter = ' AND c.CON_EQUIPE_ID IN (' + eqPh + ')';
minhasEquipes.forEach(e => params.push(e.EQU_EQUIPE_ID));
} else {
extraFilter = ' AND 1=0'; // Nenhuma equipe
}
}
const result = await db.query(alias, `
SELECT c.*,
(SELECT CME_TEXTO FROM CHATC2_CONVERSAS_MENSAGENS WHERE CME_CONVERSA_ID = c.CON_CODIGO_ID ORDER BY CME_DT_ENVIO DESC FETCH FIRST 1 ROWS ONLY) AS ULTIMA_MSG,
(SELECT CME_TIPO FROM CHATC2_CONVERSAS_MENSAGENS WHERE CME_CONVERSA_ID = c.CON_CODIGO_ID ORDER BY CME_DT_ENVIO DESC FETCH FIRST 1 ROWS ONLY) AS ULTIMA_TIPO,
(SELECT CME_DT_ENVIO FROM CHATC2_CONVERSAS_MENSAGENS WHERE CME_CONVERSA_ID = c.CON_CODIGO_ID ORDER BY CME_DT_ENVIO DESC FETCH FIRST 1 ROWS ONLY) AS DT_ULTIMA_MSG
FROM CHATC2_CONVERSAS c
WHERE c.CON_EMPRESA_ID = ? AND c.CON_STATUS IN (${statusPh}) AND c.CON_SITUACAO = 'A' ${extraFilter}
ORDER BY c.CON_DT_ULTIMA_MSG DESC
`, params);
// --- Busca em lote: etiquetas de todas as conversas (evita N+1 queries) ---
const conversaIds = result.map(r => r.CON_CODIGO_ID);
const labelsMap = {};
for (const cid of conversaIds) labelsMap[cid] = [];
if (conversaIds.length > 0) {
const idsPlaceholders = conversaIds.map(() => '?').join(',');
const todasEtiquetas = await db.query(alias, `
SELECT ce.CET_CONVERSA_ID, e.ETI_CODIGO_ID, e.ETI_NOME, e.ETI_COR
FROM CHATC2_ETIQUETAS e
INNER JOIN CHATC2_CONVERSAS_ETIQUETAS ce ON e.ETI_CODIGO_ID = ce.CET_ETIQUETA_ID
WHERE ce.CET_CONVERSA_ID IN (${idsPlaceholders}) AND e.ETI_SITUACAO = 'A'
`, conversaIds);
for (const et of todasEtiquetas) {
if (!labelsMap[et.CET_CONVERSA_ID]) labelsMap[et.CET_CONVERSA_ID] = [];
labelsMap[et.CET_CONVERSA_ID].push({
id: et.ETI_CODIGO_ID, nome: (et.ETI_NOME || '').trim(), cor: (et.ETI_COR || '#667eea').trim()
});
}
}
// --- Busca em lote: fotos dos clientes (se config ativo) ---
const fotosMap = {};
let fotoCelularAtivo = false;
{
const config = await db.query(alias,
'SELECT CFE_FOTO_CELULAR FROM CHATC2_CONFIGURACOES_EMPRESA WHERE CFE_EMPRESA_ID = ?', [empresaId]);
if (config.length > 0 && config[0].CFE_FOTO_CELULAR === 'S') {
fotoCelularAtivo = true;
const clienteIds = result.filter(r => r.CON_CLIENTE_ID).map(r => r.CON_CLIENTE_ID);
if (clienteIds.length > 0) {
const cliPlaceholders = clienteIds.map(() => '?').join(',');
const fotos = await db.query(alias,
'SELECT CLI_CODIGO_ID, CLI_FOTO, CLI_NOME_FOTO FROM CLIENTES WHERE CLI_CODIGO_ID IN (' + cliPlaceholders + ')',
clienteIds);
for (const f of fotos) {
const rawFoto = f.CLI_FOTO;
if (!rawFoto) continue;
let foto = null;
if (Buffer.isBuffer(rawFoto)) {
const str = rawFoto.toString('utf8');
if (/^[A-Za-z0-9+/]+=*$/.test(str.slice(0, 30))) {
foto = str;
} else {
let prefixo = 'data:image/jpeg;base64,';
if (rawFoto[0] === 0x89 && rawFoto[1] === 0x50) prefixo = 'data:image/png;base64,';
else if (rawFoto[0] === 0x47 && rawFoto[1] === 0x49) prefixo = 'data:image/gif;base64,';
else if (rawFoto[0] === 0x52 && rawFoto[1] === 0x49) prefixo = 'data:image/webp;base64,';
foto = prefixo + rawFoto.toString('base64');
}
} else {
foto = rawFoto;
}
fotosMap[f.CLI_CODIGO_ID] = foto;
}
}
}
}
// --- Monta resultado final sem novas queries ---
const conversations = result.map((row) => ({
id: row.CON_CODIGO_ID,
empresaId: row.CON_EMPRESA_ID,
instanciaId: row.CON_INSTANCIA_ID,
clienteId: row.CON_CLIENTE_ID,
numero: (row.CON_NUMERO || '').trim(),
nomeContato: (row.CON_NOME_CONTATO || '').trim(),
status: (row.CON_STATUS || '').trim(),
usuarioId: row.CON_USUARIO_ID,
equipeId: row.CON_EQUIPE_ID,
dtInicio: row.CON_DT_INICIO,
dtFinal: row.CON_DT_FINAL,
ultimaMsg: row.ULTIMA_MSG || '',
ultimaTipo: (row.ULTIMA_TIPO || 'text').trim(),
dtUltimaMsg: row.DT_ULTIMA_MSG || row.CON_DT_ULTIMA_MSG,
saudacaoEnviada: (row.CON_SAUDACAO_ENVIADA || 'N').trim(),
csatEnviado: (row.CON_CSAT_ENVIADO || 'N').trim(),
primeiraMsg: row.CON_PRIMEIRA_MSG,
labels: labelsMap[row.CON_CODIGO_ID] || [],
foto: fotosMap[row.CON_CLIENTE_ID] || null,
}));
// Contagens para cada filtro
const userId = req.user?.id || 0;
var countMine = await db.query(alias,
`SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS WHERE CON_EMPRESA_ID = ? AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' AND CON_USUARIO_ID = ?`,
[empresaId, userId]);
var countUnassigned = await db.query(alias,
`SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS WHERE CON_EMPRESA_ID = ? AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' AND CON_USUARIO_ID IS NULL AND CON_EQUIPE_ID IS NULL`,
[empresaId]);
var countEquipe = 0;
if (filter === 'equipe' || filter === 'mine' || filter === 'unassigned' || filter === 'all') {
var minhasEquipes = await db.query(alias,
'SELECT EQU_EQUIPE_ID FROM CHATC2_USU_EQUIPES WHERE EQU_USUARIO_ID = ?', [userId]);
if (minhasEquipes.length > 0) {
var eqIds = minhasEquipes.map(function(e) { return "'" + e.EQU_EQUIPE_ID + "'"; }).join(',');
var countEq = await db.query(alias,
`SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS WHERE CON_EMPRESA_ID = ? AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' AND CON_EQUIPE_ID IN (${eqIds})`,
[empresaId]);
countEquipe = countEq[0]?.CT || 0;
}
}
res.json({
success: true,
data: conversations,
contagens: {
mine: countMine[0]?.CT || 0,
unassigned: countUnassigned[0]?.CT || 0,
equipe: countEquipe,
},
});
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
}
/**
* Pegar/conversa específica
* GET /api/:alias/conversations/:id
*/
static async getConversation(req, res) {
try {
const { alias, id } = req.params;
const result = await db.query(alias,
'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]);
if (result.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' });
const row = result[0];
const empresaId = row.CON_EMPRESA_ID;
if (empresaId && !req.user?.empresas?.some(function(e) { return String(e) === String(empresaId); }))
return res.status(403).json({ success: false, error: 'Sem permissão.' });
// Verifica se o número pertence a um dependente
let dependenteInfo = null;
if (!row.CON_CLIENTE_ID && row.CON_NUMERO) {
const depNumero = row.CON_NUMERO.replace(/\D/g, '');
const depTermo = depNumero.slice(-8);
const dep = await db.query(alias,
`SELECT d.DEPC_NOME, d.DEPC_TELEFONE, d.DEPC_PARENTESCO,
c.CLI_CODIGO_ID AS TITULAR_ID, c.CLI_NOME AS TITULAR_NOME,
c.CLI_MATRICULA AS TITULAR_MATRICULA, c.CLI_PLANOS_ID AS TITULAR_PLANO_ID,
c.CLI_CIDADES_FAT_ID, c.CLI_BAIRROS_FAT_ID
FROM DEPENDENTES_CLI d
LEFT JOIN CLIENTES c ON d.DEPC_CLIENTE_ID = c.CLI_CODIGO_ID
WHERE d.DEPC_EMPRESA_ID = ? AND d.DEPC_SITUACAO = 'A'
AND REPLACE(REPLACE(REPLACE(COALESCE(d.DEPC_TELEFONE,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'`,
[empresaId, depTermo]
);
if (dep.length > 0) {
const d = dep[0];
// Busca dados adicionais do titular
let cidadeNome = null, bairroNome = null, planoNome = null;
let inadimplente = false;
if (d.TITULAR_ID) {
if (d.CLI_CIDADES_FAT_ID) {
const cid = await db.query(alias, 'SELECT CID_NOME FROM CIDADES WHERE CID_CODIGO_ID = ?', [d.CLI_CIDADES_FAT_ID]);
if (cid.length > 0) cidadeNome = cid[0].CID_NOME?.trim();
}
if (d.CLI_BAIRROS_FAT_ID) {
const bai = await db.query(alias, 'SELECT BAR_DESCRICAO FROM BAIRROS WHERE BAR_CODIGO_ID = ?', [d.CLI_BAIRROS_FAT_ID]);
if (bai.length > 0) bairroNome = bai[0].BAR_DESCRICAO?.trim();
}
if (d.TITULAR_PLANO_ID) {
const pla = await db.query(alias, 'SELECT PLA_NOME FROM PLANOS WHERE PLA_CODIGO_ID = ?', [d.TITULAR_PLANO_ID]);
if (pla.length > 0) planoNome = pla[0].PLA_NOME?.trim();
}
const tit = await db.query(alias,
'SELECT COUNT(*) AS T FROM CARNES WHERE CAR_CLIENTE_ID = ? AND CAR_SITUACAO = 0 AND CAR_DT_VENCIMENTO < CURRENT_DATE',
[d.TITULAR_ID]);
inadimplente = tit[0]?.T > 0;
}
dependenteInfo = {
nome: (d.DEPC_NOME || '').trim(),
telefone: (d.DEPC_TELEFONE || '').trim(),
parentesco: (d.DEPC_PARENTESCO || '').trim(),
titularId: d.TITULAR_ID || d.CLI_CODIGO_ID || null,
titularNome: (d.TITULAR_NOME || '').trim(),
titularMatricula: (d.TITULAR_MATRICULA || '').trim(),
titularCidade: cidadeNome,
titularBairro: bairroNome,
titularPlano: planoNome,
titularInadimplente: inadimplente,
};
}
}
// Info do cliente se existir
let clienteInfo = null;
if (row.CON_CLIENTE_ID) {
const cli = await db.query(alias, `
SELECT CLI_CODIGO_ID, CLI_NOME, CLI_MATRICULA, CLI_SITUACAO, CLI_EMAIL, CLI_CELULAR,
CLI_FONE1, CLI_ENDERECO_FAT, CLI_NUMERO_FAT, CLI_CIDADES_FAT_ID, CLI_BAIRROS_FAT_ID,
CLI_COBRADOR_ID, CLI_DIA_COBRANCA, CLI_FOTO, CLI_NOME_FOTO, CLI_PLANOS_ID
FROM CLIENTES WHERE CLI_CODIGO_ID = ?
`, [row.CON_CLIENTE_ID]);
if (cli.length > 0) {
const c = cli[0];
// Busca plano
let planoNome = null;
if (c.CLI_PLANOS_ID) {
const plano = await db.query(alias, 'SELECT PLA_NOME FROM PLANOS WHERE PLA_CODIGO_ID = ?', [c.CLI_PLANOS_ID]);
if (plano.length > 0) planoNome = plano[0].PLA_NOME?.trim();
}
// Busca cidade/bairro
let cidadeNome = null, bairroNome = null;
if (c.CLI_CIDADES_FAT_ID) {
const cidade = await db.query(alias, 'SELECT CID_NOME FROM CIDADES WHERE CID_CODIGO_ID = ?', [c.CLI_CIDADES_FAT_ID]);
if (cidade.length > 0) cidadeNome = cidade[0].CID_NOME?.trim();
}
if (c.CLI_BAIRROS_FAT_ID) {
const bairro = await db.query(alias, 'SELECT BAR_DESCRICAO FROM BAIRROS WHERE BAR_CODIGO_ID = ?', [c.CLI_BAIRROS_FAT_ID]);
if (bairro.length > 0) bairroNome = bairro[0].BAR_DESCRICAO?.trim();
}
// Inadimplente? Verifica se tem títulos abertos vencidos
let inadimplente = false;
const titulos = await db.query(alias,
`SELECT COUNT(*) AS T FROM CARNES WHERE CAR_CLIENTE_ID = ? AND CAR_SITUACAO = 0 AND CAR_DT_VENCIMENTO < CURRENT_DATE`,
[c.CLI_CODIGO_ID]);
inadimplente = titulos[0].T > 0;
clienteInfo = {
id: c.CLI_CODIGO_ID,
nome: (c.CLI_NOME || '').trim(),
matricula: (c.CLI_MATRICULA || '').trim(),
situacao: (c.CLI_SITUACAO || '').trim(),
email: (c.CLI_EMAIL || '').trim(),
celular: (c.CLI_CELULAR || '').trim(),
telefone: (c.CLI_FONE1 || '').trim(),
endereco: (c.CLI_ENDERECO_FAT || '').trim(),
numero: (c.CLI_NUMERO_FAT || '').trim(),
cidade: cidadeNome,
bairro: bairroNome,
plano: planoNome,
inadimplente,
foto: (function(){
var f = c.CLI_FOTO;
if (!f) return null;
if (Buffer.isBuffer(f)) {
var s = f.toString('utf8');
if (/^[A-Za-z0-9+/]+=*$/.test(s.slice(0, 30))) return s;
var prefixo = 'data:image/jpeg;base64,';
if (f[0] === 0x89 && f[1] === 0x50) prefixo = 'data:image/png;base64,';
else if (f[0] === 0x47 && f[1] === 0x49) prefixo = 'data:image/gif;base64,';
else if (f[0] === 0x52 && f[1] === 0x49) prefixo = 'data:image/webp;base64,';
return prefixo + f.toString('base64');
}
return f;
})(),
};
}
}
// Etiquetas
const labels = await db.query(alias, `
SELECT e.* FROM CHATC2_ETIQUETAS e INNER JOIN CHATC2_CONVERSAS_ETIQUETAS ce ON e.ETI_CODIGO_ID = ce.CET_ETIQUETA_ID
WHERE ce.CET_CONVERSA_ID = ? AND e.ETI_SITUACAO = 'A'
`, [id]);
res.json({
success: true,
data: {
id: row.CON_CODIGO_ID,
empresaId: row.CON_EMPRESA_ID,
instanciaId: row.CON_INSTANCIA_ID,
clienteId: row.CON_CLIENTE_ID,
numero: (row.CON_NUMERO || '').trim(),
nomeContato: (row.CON_NOME_CONTATO || '').trim(),
status: (row.CON_STATUS || '').trim(),
usuarioId: row.CON_USUARIO_ID,
equipeId: row.CON_EQUIPE_ID,
dtInicio: row.CON_DT_INICIO,
dtFinal: row.CON_DT_FINAL,
dtUltimaMsg: row.CON_DT_ULTIMA_MSG,
saudacaoEnviada: (row.CON_SAUDACAO_ENVIADA || 'N').trim(),
csatEnviado: (row.CON_CSAT_ENVIADO || 'N').trim(),
primeiraMsg: row.CON_PRIMEIRA_MSG,
motivoId: row.CON_MOTIVO_ID || null,
resolucao: row.CON_RESOLUCAO || null,
cliente: clienteInfo,
dependente: dependenteInfo,
labels: labels.map(l => ({ id: l.ETI_CODIGO_ID, nome: (l.ETI_NOME || '').trim(), cor: (l.ETI_COR || '#667eea').trim() })),
}
});
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
}
/**
* Pegar mensagens de uma conversa
* GET /api/:alias/conversations/:id/messages?page=&limit=
*/
/**
* Verifica se a conversa existe e pertence a uma empresa do usuário.
* Retorna { status: 404|403 } em caso de bloqueio, ou { ok:true, empresaId }.
*/
static async checarConversaEmpresa(alias, id, req) {
const r = await db.query(alias,
'SELECT CON_EMPRESA_ID FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]);
if (r.length === 0) return { status: 404 };
const emp = r[0].CON_EMPRESA_ID;
const minhas = (req.user && req.user.empresas) || [];
if (emp && !minhas.some(function(e) { return String(e) === String(emp); })) return { status: 403 };
return { ok: true, empresaId: emp };
}
static async getMessages(req, res) {
try {
const { alias, id } = req.params;
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 50;
const offset = (page - 1) * limit;
const chk = await ChatController.checarConversaEmpresa(alias, id, req);
if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' });
const msgs = await db.query(alias, `
SELECT *
FROM CHATC2_CONVERSAS_MENSAGENS
WHERE CME_CONVERSA_ID = ? AND CME_SITUACAO = 'A'
ORDER BY CME_DT_ENVIO DESC
OFFSET ? ROWS FETCH NEXT ? ROWS ONLY
`, [id, offset, limit]);
var mapped = msgs.reverse().map(function(m) { return {
id: m.CME_CODIGO_ID,
conversaId: m.CME_CONVERSA_ID,
remetente: (m.CME_REMETENTE || '').trim(),
usuarioId: m.CME_USUARIO_ID,
mensagem: m.CME_TEXTO || m.CME_MENSAGEM || '',
tipo: (m.CME_TIPO || 'text').trim(),
dtEnvio: m.CME_DT_ENVIO,
privada: (m.CME_PRIVADA || 'N').trim(),
lida: (m.CME_LIDA || 'N').trim(),
midiaId: m.CME_MIDIA_ID,
};});
// Busca transcricao para mensagens com midia
for (var i = 0; i < mapped.length; i++) {
if (mapped[i].midiaId && (mapped[i].tipo === 'audio' || mapped[i].tipo === 'ptt')) {
try {
var trans = await db.query(alias,
'SELECT MAT_TRANSCRICAO FROM CHATC2_MENSAGENS_ATENDIMENTOS WHERE MAT_CODIGO_ID = ? AND MAT_TRANSCRICAO IS NOT NULL',
[mapped[i].midiaId]);
if (trans.length > 0 && trans[0].MAT_TRANSCRICAO) {
var txt = trans[0].MAT_TRANSCRICAO;
if (typeof txt === 'object' && txt.toString) txt = txt.toString();
mapped[i].transcricao = txt;
}
} catch(e) {}
}
}
res.json({
success: true,
data: mapped,
});
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
}
/**
* Enviar mensagem
* POST /api/:alias/conversations/:id/messages
*/
static async sendMessage(req, res) {
try {
const { alias, id } = req.params;
const { mensagem, tipo, privada, nomeArquivo } = req.body;
let midiaBase64 = req.body.midiaBase64;
const usuarioId = req.user?.id;
const conv = await db.query(alias, 'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]);
if (conv.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' });
if (conv[0].CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).some(function(e) { return String(e) === String(conv[0].CON_EMPRESA_ID); }))
return res.status(403).json({ success: false, error: 'Sem permissão.' });
// Se a conversa estiver finalizada, reabre
if (conv[0].CON_STATUS === 'F') {
await db.execute(alias,
"UPDATE CHATC2_CONVERSAS SET CON_STATUS = 'A', CON_DT_FINAL = NULL WHERE CON_CODIGO_ID = ?", [id]);
}
const newId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
let midiaId = null;
// Salva mídia se houver
if (midiaBase64) {
midiaId = await db.nextId(alias, 'GEN_MENSAGENS_ATENDIMENTOS');
// Para áudio: converte WebM → OGG antes de salvar no BD
// Assim o BD já tem o OGG e o envio para Evolution usa a mesma versão
var midiaFinal = midiaBase64;
var nomeFinal = nomeArquivo || 'arquivo';
var mimeType = tipo || 'application/octet-stream';
if (tipo === 'audio') {
mimeType = 'audio/webm';
try {
var inputBuf = Buffer.from(midiaBase64, 'base64');
if (inputBuf.length > 100) {
var oggBuf = await convertWebmToOgg(inputBuf);
if (oggBuf && oggBuf.length > 100) {
midiaFinal = oggBuf.toString('base64');
mimeType = 'audio/ogg';
nomeFinal = 'audio.ogg';
console.log('[SendMsg] Áudio convertido WebM→OGG antes de salvar:', inputBuf.length, '→', oggBuf.length);
}
}
} catch(convErr) {
console.error('[SendMsg] Conversão áudio falhou, usando WebM:', convErr.message.substring(0, 80));
}
} else if (tipo === 'image') {
var ext = (nomeArquivo || '').split('.').pop().toLowerCase();
if (ext === 'png') mimeType = 'image/png';
else if (ext === 'gif') mimeType = 'image/gif';
else if (ext === 'webp') mimeType = 'image/webp';
else mimeType = 'image/jpeg';
}
await db.execute(alias, `
INSERT INTO CHATC2_MENSAGENS_ATENDIMENTOS (MAT_CODIGO_ID, MAT_CONVERSA_ID, MAT_MENSAGEM_ID, MAT_NOME_ARQUIVO, MAT_TIPO_ARQUIVO, MAT_ARQUIVO)
VALUES (?, ?, ?, ?, ?, ?)
`, [midiaId, id, newId, nomeFinal, mimeType, midiaFinal]);
// Usa a versão convertida para envio posterior à Evolution
midiaBase64 = midiaFinal;
}
// Verifica config para enviar nome do usuário
let textoFinal = mensagem || '';
try {
const config = await db.query(alias,
"SELECT CFE_ENVIAR_NOME_USUARIO FROM CHATC2_CONFIGURACOES_EMPRESA WHERE CFE_EMPRESA_ID = ?",
[conv[0].CON_EMPRESA_ID]);
if (config.length > 0 && config[0].CFE_ENVIAR_NOME_USUARIO === 'S') {
const user = await db.query(alias,
'SELECT USU_NOME FROM USUARIOS WHERE USU_CODIGO_ID = ?', [usuarioId]);
if (user.length > 0) {
const nomeUser = (user[0].USU_NOME || '').trim();
if (nomeUser && textoFinal) {
// Padrão WhatsApp: *Nome:* em negrito + quebra de linha + mensagem
textoFinal = '*' + nomeUser + ':*\n' + textoFinal;
}
}
}
} catch(e) {}
// Processa placeholders: [CLIENTE], [CONTATO], [EMPRESAR], [EMPRESAF]
textoFinal = await ChatController.processarPlaceholders(alias, id, textoFinal);
// Insere mensagem no banco
await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_SITUACAO, CME_DT_ENVIO, CME_MIDIA_ID)
VALUES (?, ?, 'U', ?, ?, ?, ?, 'A', CURRENT_TIMESTAMP, ?)
`, [newId, id, usuarioId, textoFinal, tipo || 'text', privada === 'S' ? 'S' : 'N', midiaId]);
// Atualiza data da última mensagem
await db.execute(alias,
'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?', [id]);
// Se não for privada, envia via Evolution API
if (privada !== 'S' && conv[0].CON_NUMERO) {
try {
const instancia = await db.query(alias,
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [conv[0].CON_INSTANCIA_ID]);
if (instancia.length > 0) {
const ins = instancia[0];
console.log('[SendMsg] Enviando mídia tipo:', tipo, 'tamanho:', (midiaBase64 || '').length, 'nome:', nomeArquivo, 'para:', conv[0].CON_NUMERO);
var evoResult = await sendEvolutionMessage(ins, conv[0].CON_NUMERO, textoFinal, tipo, midiaBase64, nomeArquivo);
console.log('[SendMsg] Evolution resposta:', (evoResult || '').substring(0, 200));
} else {
console.log('[SendMsg] Instância não encontrada para ID:', conv[0].CON_INSTANCIA_ID);
}
} catch (evoErr) {
console.error('[SendMsg] Erro ao enviar via Evolution:', evoErr.message);
}
}
res.json({ success: true, data: { id: newId } });
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
}
/**
* Finalizar conversa
* POST /api/:alias/conversations/:id/finalize
*/
static async finalizeConversation(req, res) {
try {
const { alias, id } = req.params;
const conv = await db.query(alias,
'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]);
if (conv.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' });
const c = conv[0];
if (c.CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).some(function(e) { return String(e) === String(c.CON_EMPRESA_ID); }))
return res.status(403).json({ success: false, error: 'Sem permissão.' });
// ===== Fluxo de Resolução: valida motivo/resolução conforme config =====
const motivoId = (req.body && req.body.motivoId) ? parseInt(req.body.motivoId, 10) : null;
const resolucao = (req.body && req.body.resolucao != null) ? String(req.body.resolucao).trim() : '';
const flagsRows = await db.query(alias,
'SELECT CFE_MOTIVO_VISUALIZAR, CFE_MOTIVO_OBRIGATORIO, CFE_RESOLUCAO_VISUALIZAR, CFE_RESOLUCAO_OBRIGATORIO, CFE_RESOLUCAO_SALVAR_MSC FROM CHATC2_CONFIGURACOES_EMPRESA WHERE CFE_EMPRESA_ID = ?',
[c.CON_EMPRESA_ID]);
const fl = flagsRows[0] || {};
const isS = (v) => String(v || 'N').trim() === 'S';
if (isS(fl.CFE_MOTIVO_VISUALIZAR) && isS(fl.CFE_MOTIVO_OBRIGATORIO) && !motivoId) {
return res.status(400).json({ success: false, error: 'Selecione o motivo do atendimento para finalizar.' });
}
if (isS(fl.CFE_RESOLUCAO_VISUALIZAR) && isS(fl.CFE_RESOLUCAO_OBRIGATORIO) && !resolucao) {
return res.status(400).json({ success: false, error: 'Preencha a resolução do atendimento para finalizar.' });
}
// Busca nome do usuário que atendeu
let usuarioNome = null;
if (c.CON_USUARIO_ID) {
const user = await db.query(alias,
'SELECT USU_NOME FROM USUARIOS WHERE USU_CODIGO_ID = ?', [c.CON_USUARIO_ID]);
if (user.length > 0) usuarioNome = (user[0].USU_NOME || '').trim();
}
// Busca nome da equipe
let equipeNome = null;
if (c.CON_EQUIPE_ID) {
const equipe = await db.query(alias,
'SELECT EQU_NOME FROM CHATC2_EQUIPES WHERE EQU_CODIGO_ID = ?', [c.CON_EQUIPE_ID]);
if (equipe.length > 0) equipeNome = (equipe[0].EQU_NOME || '').trim();
}
// Busca etiquetas
const etiquetas = await db.query(alias, `
SELECT ETI_NOME FROM CHATC2_ETIQUETAS e
INNER JOIN CHATC2_CONVERSAS_ETIQUETAS ce ON e.ETI_CODIGO_ID = ce.CET_ETIQUETA_ID
WHERE ce.CET_CONVERSA_ID = ? AND e.ETI_SITUACAO = 'A'
`, [id]);
const etiquetasDesc = etiquetas.map(function(e) { return (e.ETI_NOME || '').trim(); }).filter(Boolean).join(', ');
await db.execute(alias, `
UPDATE CHATC2_CONVERSAS SET
CON_STATUS = 'F',
CON_DT_FINAL = CURRENT_TIMESTAMP,
CON_USUARIO_NOME = ?,
CON_EQUIPE_NOME = ?,
CON_ETIQUETAS_DESC = ?,
CON_MOTIVO_ID = ?,
CON_RESOLUCAO = ?
WHERE CON_CODIGO_ID = ?
`, [usuarioNome, equipeNome, etiquetasDesc, motivoId, resolucao || null, id]);
// CSAT — NÃO envia se a conversa for agendada (skipCsat = true)
const skipCsat = req.body && req.body.skipCsat === true;
if (!skipCsat) {
const config = await db.query(alias,
'SELECT CFE_CSAT_ATIVO, CFE_CSAT_MENSAGEM FROM CHATC2_CONFIGURACOES_EMPRESA WHERE CFE_EMPRESA_ID = ?',
[conv[0].CON_EMPRESA_ID]);
if (config.length > 0 && config[0].CFE_CSAT_ATIVO === 'S') {
var csatEnviadoSucesso = false;
try {
const empresaId = conv[0].CON_EMPRESA_ID;
var baseUrl = process.env.EXTERNAL_URL || process.env.NGROK_URL || ('https://' + (req.headers.host || 'localhost:3000'));
var csatLink = baseUrl.replace(/^https?:\/\//, '') + '/app/' + alias + '/csat?alias=' + alias + '&conversa=' + id + '&empresa=' + empresaId;
const msgCsat = 'Ol\u00e1! Gostar\u00edamos de saber como foi seu atendimento.\n\nAcesse o link abaixo para avaliar:\n' + csatLink;
const inst = await db.query(alias,
"SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ? AND INS_SITUACAO = 'A'",
[conv[0].CON_INSTANCIA_ID]);
if (inst.length > 0) {
var csatResult = await sendEvolutionMessage(inst[0], conv[0].CON_NUMERO, msgCsat, 'text', null);
console.log('[CSAT] Resultado envio:', JSON.stringify(csatResult));
if (csatResult) {
try {
var parsed = typeof csatResult === 'string' ? JSON.parse(csatResult) : csatResult;
if (parsed && parsed.key && parsed.key.remoteJid) csatEnviadoSucesso = true;
} catch(e) {
if (csatResult.status || (typeof csatResult === 'string' && csatResult.length > 20)) csatEnviadoSucesso = true;
}
}
} else {
console.log('[CSAT] Instancia nao encontrada ou inativa para ID:', conv[0].CON_INSTANCIA_ID);
}
} catch(e) { console.error('[CSAT] Erro ao enviar:', e.message); }
// So marca como enviado se realmente conseguiu enviar
if (csatEnviadoSucesso) {
await db.execute(alias,
"UPDATE CHATC2_CONVERSAS SET CON_CSAT_ENVIADO = 'S' WHERE CON_CODIGO_ID = ?", [id]);
console.log('[CSAT] Marcado como enviado para conversa', id);
} else {
console.log('[CSAT] NAO enviado - Evolution API pode estar offline');
}
}
} // fecha if (!skipCsat)
// Salva resolução na tabela MENSAGENS_CLIENTES como aviso (se configurado)
if (isS(fl.CFE_RESOLUCAO_SALVAR_MSC) && resolucao && c.CON_CLIENTE_ID) {
try {
const mscId = await db.nextId(alias, 'GEN_MENSAGENS_CLIENTES');
await db.execute(alias,
`INSERT INTO MENSAGENS_CLIENTES (MSC_CODIGO_ID, MSC_CLIENTE_ID, MSC_USUARIO_ID, MSC_DT_MENSAGEM, MSC_AVISO, MSC_MENSAGEM)
VALUES (?, ?, ?, CURRENT_TIMESTAMP, 'S', ?)`,
[mscId, c.CON_CLIENTE_ID, c.CON_USUARIO_ID || null, resolucao]);
console.log('[Resolucao] Aviso salvo em MENSAGENS_CLIENTES para cliente', c.CON_CLIENTE_ID);
} catch (mscErr) {
console.error('[Resolucao] Erro ao salvar em MENSAGENS_CLIENTES:', mscErr.message);
}
}
res.json({ success: true, message: 'Conversa finalizada.' });
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
}
/**
* Criar nova conversa e enviar primeira mensagem
* POST /api/:alias/conversations/create
* Body: { empresaId, numero, nomeContato, mensagem, instanciaId, clienteId }
*/
static async createConversation(req, res) {
try {
const { alias } = req.params;
const { empresaId, numero, mensagem, instanciaId } = req.body;
let clienteId = req.body.clienteId || null;
let nomeContato = req.body.nomeContato || '';
if (!empresaId || !numero || !mensagem) {
return res.status(400).json({ success: false, error: 'Campos obrigatórios: empresaId, numero, mensagem' });
}
// Verifica permissão
const userEmpresas = req.user?.empresas || [];
if (!userEmpresas.some(function(e) { return String(e) === String(empresaId); })) {
return res.status(403).json({ success: false, error: 'Sem permissão para esta empresa.' });
}
const numeroLimpo = numero.replace(/\D/g, '').substring(0, 15);
// Se não foi informado clienteId, tenta localizar pelo número de telefone
// Fluxo: cliente → dependente (titular) → número novo (sem vínculo)
let clienteEncontrado = null;
let nomeEncontrado = null;
if (!clienteId) {
// Variações do número para busca: completo, sem DDI (55), últimos 8 dígitos
const variantes = [numeroLimpo];
if (numeroLimpo.startsWith('55')) variantes.push(numeroLimpo.substring(2));
const ultimos8 = numeroLimpo.slice(-8);
if (!variantes.includes(ultimos8)) variantes.push(ultimos8);
// 1) Busca CLIENTES
for (const termo of variantes) {
const cli = await db.query(alias, `
SELECT CLI_CODIGO_ID, CLI_NOME
FROM CLIENTES
WHERE CLI_EMPRESA_ID = ?
AND (REPLACE(REPLACE(REPLACE(COALESCE(CLI_CELULAR,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'
OR REPLACE(REPLACE(REPLACE(COALESCE(CLI_FONE1,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'
OR REPLACE(REPLACE(REPLACE(COALESCE(CLI_FONE2,''),'-',''),'(',''),')','') LIKE '%' || ? || '%')
FETCH FIRST 1 ROWS ONLY
`, [empresaId, termo, termo, termo]);
if (cli.length > 0) {
clienteEncontrado = cli[0].CLI_CODIGO_ID;
nomeEncontrado = (cli[0].CLI_NOME || '').trim();
break;
}
}
// 2) Se não achou cliente, busca DEPENDENTES
if (!clienteEncontrado) {
for (const termo of variantes) {
const dep = await db.query(alias, `
SELECT d.DEPC_CLIENTE_ID AS TITULAR_ID, d.DEPC_NOME, c.CLI_NOME AS TITULAR_NOME
FROM DEPENDENTES_CLI d
LEFT JOIN CLIENTES c ON d.DEPC_CLIENTE_ID = c.CLI_CODIGO_ID
WHERE d.DEPC_EMPRESA_ID = ? AND d.DEPC_SITUACAO = 'A'
AND REPLACE(REPLACE(REPLACE(COALESCE(d.DEPC_TELEFONE,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'
FETCH FIRST 1 ROWS ONLY
`, [empresaId, termo]);
if (dep.length > 0) {
clienteEncontrado = dep[0].TITULAR_ID;
nomeEncontrado = (dep[0].TITULAR_NOME || dep[0].DEPC_NOME || '').trim();
break;
}
}
}
if (clienteEncontrado) {
clienteId = clienteEncontrado;
if (!nomeContato || nomeContato === numeroLimpo) nomeContato = nomeEncontrado;
}
}
// Verifica se já existe conversa ABERTA ou EM ESPERA para este número
// Tenta com o número exato, e também com os últimos 8 dígitos
let existente = await db.query(alias, `
SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_NUMERO
FROM CHATC2_CONVERSAS
WHERE CON_NUMERO = ? AND CON_EMPRESA_ID = ?
AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A'
ORDER BY CON_CODIGO_ID ASC
FETCH FIRST 1 ROWS ONLY
`, [numeroLimpo, empresaId]);
// Se não achou pelo número exato, tenta pelos últimos 8 dígitos
if (existente.length === 0) {
const ultimos8 = numeroLimpo.slice(-8);
existente = await db.query(alias, `
SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_NUMERO
FROM CHATC2_CONVERSAS
WHERE CON_NUMERO LIKE '%' || ? || '%' AND CON_EMPRESA_ID = ?
AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A'
ORDER BY CON_CODIGO_ID ASC
FETCH FIRST 1 ROWS ONLY
`, [ultimos8, empresaId]);
}
let conversaId;
if (existente.length > 0) {
// Reusa a conversa existente (prioriza a de menor ID)
conversaId = existente[0].CON_CODIGO_ID;
// Se tem clienteId e a conversa não tinha, atualiza
if (clienteId && !existente[0].CON_CLIENTE_ID) {
await db.execute(alias,
'UPDATE CHATC2_CONVERSAS SET CON_CLIENTE_ID = ?, CON_NOME_CONTATO = ? WHERE CON_CODIGO_ID = ?',
[clienteId, nomeContato || '', conversaId]);
}
// Adiciona mensagem na conversa existente
const msgId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
const textoProcessado = await ChatController.processarPlaceholders(alias, conversaId, mensagem);
await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [msgId, conversaId, req.user?.id, textoProcessado]);
await db.execute(alias,
'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?',
[conversaId]);
// Envia via Evolution
const resultado = await db.query(alias,
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [instanciaId || null]);
if (resultado.length > 0) {
sendEvolutionMessage(resultado[0], numeroLimpo, textoProcessado, 'text', null).catch(function(){});
}
return res.json({ success: true, data: { id: conversaId, reutilizada: true } });
}
// Gera ID da nova conversa
const newId = await db.nextId(alias, 'GEN_CHATC2_CONVERSAS');
// Se não informou instância, pega a primeira ativa
let instId = instanciaId;
if (!instId) {
const inst = await db.query(alias,
"SELECT INS_CODIGO_ID FROM CHATC2_INSTANCIAS WHERE INS_SITUACAO = 'A' FETCH FIRST 1 ROWS ONLY");
if (inst.length > 0) instId = inst[0].INS_CODIGO_ID;
}
// Insere conversa
await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS (CON_CODIGO_ID, CON_EMPRESA_ID, CON_INSTANCIA_ID, CON_NUMERO,
CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_SITUACAO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG)
VALUES (?, ?, ?, ?, ?, ?, 'A', 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [newId, empresaId, instId, numeroLimpo, nomeContato || numeroLimpo, clienteId || null]);
// Insere primeira mensagem
const msgId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
const textoProcessadoNew = await ChatController.processarPlaceholders(alias, newId, mensagem);
await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [msgId, newId, req.user?.id, textoProcessadoNew]);
// Envia via Evolution API
try {
const resultado = await db.query(alias,
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [instId]);
if (resultado.length > 0) {
await sendEvolutionMessage(resultado[0], numeroLimpo, textoProcessadoNew, 'text', null);
}
} catch (evoErr) {
console.error('Erro ao enviar primeira mensagem via Evolution:', evoErr.message);
}
res.json({ success: true, data: { id: newId, msgId } });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
}
/**
* Atribuir conversa a usuário
* POST /api/:alias/conversations/:id/assign
*/
static async assignConversation(req, res) {
try {
const { alias, id } = req.params;
const { usuarioId } = req.body;
const chk = await ChatController.checarConversaEmpresa(alias, id, req);
if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' });
await db.execute(alias,
"UPDATE CHATC2_CONVERSAS SET CON_USUARIO_ID = ?, CON_STATUS = 'A' WHERE CON_CODIGO_ID = ?",
[usuarioId, id]);
res.json({ success: true });
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
}
/**
* Atribuir conversa a uma equipe
* POST /api/:alias/conversations/:id/assign-team
*/
static async assignTeam(req, res) {
try {
const { alias, id } = req.params;
const { equipeId } = req.body;
const chk = await ChatController.checarConversaEmpresa(alias, id, req);
if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' });
await db.execute(alias,
'UPDATE CHATC2_CONVERSAS SET CON_EQUIPE_ID = ? WHERE CON_CODIGO_ID = ?',
[equipeId || null, id]);
res.json({ success: true });
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
}
/**
* Adicionar/remover etiqueta da conversa
* POST /api/:alias/conversations/:id/labels
*/
static async toggleLabel(req, res) {
try {
const { alias, id } = req.params;
const { etiquetaId, ativar } = req.body;
const chk = await ChatController.checarConversaEmpresa(alias, id, req);
if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' });
if (ativar) {
await db.execute(alias,
'INSERT INTO CHATC2_CONVERSAS_ETIQUETAS (CET_CONVERSA_ID, CET_ETIQUETA_ID) VALUES (?, ?)',
[id, etiquetaId]);
} else {
await db.execute(alias,
'DELETE FROM CHATC2_CONVERSAS_ETIQUETAS WHERE CET_CONVERSA_ID = ? AND CET_ETIQUETA_ID = ?',
[id, etiquetaId]);
}
res.json({ success: true });
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
}
/**
* Buscar mídia (imagem/audio) pelo ID
* GET /api/:alias/media/:mediaId
*/
static async getMedia(req, res) {
const { alias, mediaId } = req.params;
if (!alias || !mediaId) {
return res.status(400).json({ success: false, error: 'Parâmetros inválidos.' });
}
let responded = false;
function respond(status, data) {
if (responded) return;
responded = true;
res.status(status).json(data);
}
function respondBuffer(mimeType, nomeArquivo, buffer) {
if (responded) return;
responded = true;
const totalSize = buffer.length;
// Suporte a byte-range para áudio (permite seek no player HTML5)
const range = res.req.headers.range;
if (range && mimeType.startsWith('audio/')) {
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : totalSize - 1;
const chunkSize = (end - start) + 1;
res.status(206);
res.set('Content-Range', 'bytes ' + start + '-' + end + '/' + totalSize);
res.set('Accept-Ranges', 'bytes');
res.set('Content-Length', chunkSize);
res.set('Content-Type', mimeType);
res.set('Content-Disposition', 'inline; filename="' + encodeURIComponent(nomeArquivo) + '"');
res.send(buffer.slice(start, end + 1));
return;
}
res.set('Content-Type', mimeType);
res.set('Accept-Ranges', 'bytes');
res.set('Content-Length', totalSize);
res.set('Content-Disposition', 'inline; filename="' + encodeURIComponent(nomeArquivo) + '"');
res.send(buffer);
}
try {
// Autenticação:
/