alterações de melhorias
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Serviço unificado de comunicação com Evolution API.
|
||||
*
|
||||
* Centraliza TODAS as chamadas HTTP para a Evolution API que antes estavam
|
||||
* duplicadas em chatController, triageController e evolutionController.
|
||||
*
|
||||
* Uso:
|
||||
* const evolution = require('../services/evolutionService');
|
||||
* await evolution.sendText(instancia, numero, texto);
|
||||
* await evolution.sendMedia(instancia, numero, tipo, base64, nome);
|
||||
* const result = await evolution.request(url, apiKey, endpoint, 'POST', body);
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
// ============================================================
|
||||
// Função interna: requisição HTTP de baixo nível
|
||||
// ============================================================
|
||||
function _request(baseUrl, apiKey, endpoint, method, body) {
|
||||
const urlClean = (baseUrl || '').replace(/\/+$/, '');
|
||||
if (!urlClean || !apiKey) return Promise.reject(new Error('URL ou API Key não configurados'));
|
||||
|
||||
const parsedUrl = new URL(urlClean + endpoint);
|
||||
const lib = parsedUrl.protocol === 'https:' ? https : http;
|
||||
const data = body ? JSON.stringify(body) : '';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: parsedUrl.hostname,
|
||||
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
||||
path: parsedUrl.pathname + parsedUrl.search,
|
||||
method: method || 'GET',
|
||||
timeout: 30000,
|
||||
headers: { 'Content-Type': 'application/json', 'apikey': apiKey },
|
||||
};
|
||||
if (data) options.headers['Content-Length'] = Buffer.byteLength(data);
|
||||
|
||||
const req = lib.request(options, (resHttp) => {
|
||||
let respBody = '';
|
||||
resHttp.on('data', chunk => respBody += chunk);
|
||||
resHttp.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(respBody);
|
||||
parsed._httpStatus = resHttp.statusCode;
|
||||
if (resHttp.statusCode >= 400) {
|
||||
reject(Object.assign(
|
||||
new Error(parsed.error || parsed.response?.message?.[0] || 'HTTP ' + resHttp.statusCode),
|
||||
{ _httpStatus: resHttp.statusCode, response: parsed }
|
||||
));
|
||||
} else {
|
||||
resolve(parsed);
|
||||
}
|
||||
} catch (e) {
|
||||
if (resHttp.statusCode >= 400) {
|
||||
reject(Object.assign(new Error('HTTP ' + resHttp.statusCode), { _httpStatus: resHttp.statusCode, raw: respBody }));
|
||||
} else {
|
||||
resolve({ raw: respBody, _httpStatus: resHttp.statusCode });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
||||
if (data) req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Função auxiliar: extrai credenciais da instância
|
||||
// ============================================================
|
||||
function _extrairCredenciais(instancia) {
|
||||
return {
|
||||
url: ((instancia.INS_URL || '').trim()).replace(/\/+$/, ''),
|
||||
apiKey: (instancia.INS_API_KEY || '').trim(),
|
||||
instanceName: (instancia.INS_INSTANCE_NAME || '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// API PÚBLICA
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Requisição HTTP genérica para a Evolution API.
|
||||
* Usada para operações avançadas: QR Code, fetchInstances, restart, fetchProfile, etc.
|
||||
*
|
||||
* @param {string} baseUrl - URL base da Evolution (ex: https://evo.empresa.com)
|
||||
* @param {string} apiKey - API Key da Evolution
|
||||
* @param {string} endpoint - Caminho (ex: /instance/fetchInstances)
|
||||
* @param {string} method - GET, POST, PUT, DELETE
|
||||
* @param {object} [body] - Corpo da requisição (opcional)
|
||||
* @returns {Promise<object>} Resposta parseada (JSON) ou { raw, _httpStatus }
|
||||
*/
|
||||
async function request(baseUrl, apiKey, endpoint, method, body) {
|
||||
return _request(baseUrl, apiKey, endpoint, method, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Envia mensagem de texto via Evolution API.
|
||||
*
|
||||
* @param {object} instancia - Objeto da instância (linha do banco: INS_URL, INS_API_KEY, INS_INSTANCE_NAME)
|
||||
* @param {string} numero - Número do destinatário (com ou sem formatação)
|
||||
* @param {string} texto - Texto da mensagem
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendText(instancia, numero, texto) {
|
||||
const { url, apiKey, instanceName } = _extrairCredenciais(instancia);
|
||||
if (!url || !apiKey || !instanceName || !numero) return;
|
||||
|
||||
const numeroLimpo = numero.replace(/\D/g, '');
|
||||
const endpoint = '/message/sendText/' + encodeURIComponent(instanceName);
|
||||
const payload = { number: numeroLimpo, text: texto || '', delay: 0 };
|
||||
|
||||
try {
|
||||
await _request(url, apiKey, endpoint, 'POST', payload);
|
||||
} catch (err) {
|
||||
console.error('[Evolution] Erro ao enviar texto:', err.message?.substring(0, 100));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envia mídia (imagem, áudio, vídeo, documento) via Evolution API.
|
||||
*
|
||||
* @param {object} instancia - Objeto da instância
|
||||
* @param {string} numero - Número do destinatário
|
||||
* @param {string} tipo - 'image', 'audio', 'video', 'document'
|
||||
* @param {string} midiaBase64 - Conteúdo em base64
|
||||
* @param {string} [nomeArquivo] - Nome do arquivo
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendMedia(instancia, numero, tipo, midiaBase64, nomeArquivo) {
|
||||
const { url, apiKey, instanceName } = _extrairCredenciais(instancia);
|
||||
if (!url || !apiKey || !instanceName || !numero || !midiaBase64) return;
|
||||
|
||||
const numeroLimpo = numero.replace(/\D/g, '');
|
||||
const fileName = nomeArquivo || 'arquivo';
|
||||
|
||||
let endpoint, payload;
|
||||
|
||||
if (tipo === 'audio') {
|
||||
// Envia como mensagem de voz (PTT) via sendWhatsAppAudio
|
||||
endpoint = '/message/sendWhatsAppAudio/' + encodeURIComponent(instanceName);
|
||||
payload = { number: numeroLimpo, audio: midiaBase64, encoding: true };
|
||||
console.log('[Evolution] Enviando áudio PTT, tamanho base64:', midiaBase64.length);
|
||||
} else if (tipo === 'image') {
|
||||
endpoint = '/message/sendMedia/' + encodeURIComponent(instanceName);
|
||||
payload = { number: numeroLimpo, mediatype: 'image', media: midiaBase64, fileName };
|
||||
} else if (tipo === 'video') {
|
||||
endpoint = '/message/sendMedia/' + encodeURIComponent(instanceName);
|
||||
payload = { number: numeroLimpo, mediatype: 'video', media: midiaBase64, fileName };
|
||||
} else {
|
||||
// document e outros tipos
|
||||
endpoint = '/message/sendMedia/' + encodeURIComponent(instanceName);
|
||||
payload = { number: numeroLimpo, mediatype: 'document', media: midiaBase64, fileName };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await _request(url, apiKey, endpoint, 'POST', payload);
|
||||
console.log('[Evolution] Mídia enviada, status:', result._httpStatus);
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error('[Evolution] Erro ao enviar mídia:', err.message?.substring(0, 100));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Baixa mídia da Evolution API via GET /chat/getMedia/.
|
||||
* Usado no webhook para buscar áudio/imagem/vídeo/documento recebidos.
|
||||
*
|
||||
* @param {string} baseUrl - URL base da Evolution
|
||||
* @param {string} apiKey - API Key
|
||||
* @param {string} instanceName - Nome da instância
|
||||
* @param {string} messageKey - ID da mensagem (data.key.id)
|
||||
* @returns {Promise<Buffer|null>}
|
||||
*/
|
||||
async function downloadMedia(baseUrl, apiKey, instanceName, messageKey) {
|
||||
if (!baseUrl || !apiKey || !instanceName || !messageKey) return null;
|
||||
|
||||
const urlClean = baseUrl.replace(/\/+$/, '');
|
||||
const getUrl = urlClean + '/chat/getMedia/' + encodeURIComponent(instanceName) + '/' + encodeURIComponent(messageKey);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const u = new URL(getUrl);
|
||||
const lib = u.protocol === 'https:' ? https : http;
|
||||
const req = lib.get(getUrl, { timeout: 20000, headers: { 'apikey': apiKey } }, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', (c) => chunks.push(c));
|
||||
res.on('end', () => {
|
||||
const full = Buffer.concat(chunks);
|
||||
if (res.statusCode !== 200) {
|
||||
console.log('[Evolution] downloadMedia HTTP ' + res.statusCode + ': ' + full.toString('utf8').substring(0, 100));
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(full);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', () => resolve(null));
|
||||
req.on('timeout', () => { req.destroy(); resolve(null); });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { request, sendText, sendMedia, downloadMedia };
|
||||
Reference in New Issue
Block a user