import { EventDto } from '@api/integrations/event/event.dto'; import { PrismaRepository } from '@api/repository/repository.service'; import { WAMonitoringService } from '@api/services/monitor.service'; import { wa } from '@api/types/wa.types'; import { configService, Log, Webhook } from '@config/env.config'; import { Logger } from '@config/logger.config'; // import { BadRequestException } from '@exceptions'; import axios, { AxiosInstance } from 'axios'; import * as jwt from 'jsonwebtoken'; import { EmitData, EventController, EventControllerInterface } from '../event.controller'; export class WebhookController extends EventController implements EventControllerInterface { private readonly logger = new Logger('WebhookController'); constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { super(prismaRepository, waMonitor, true, 'webhook'); } override async set(instanceName: string, data: EventDto): Promise { // if (!/^(https?:\/\/)/.test(data.webhook.url)) { // throw new BadRequestException('Invalid "url" property'); // } if (!data.webhook?.enabled) { data.webhook.events = []; } else { if (0 === data.webhook.events.length) { data.webhook.events = EventController.events; } } return this.prisma.webhook.upsert({ where: { instanceId: this.monitor.waInstances[instanceName].instanceId, }, update: { enabled: data.webhook?.enabled, events: data.webhook?.events, url: data.webhook?.url, headers: data.webhook?.headers, webhookBase64: data.webhook.base64, webhookByEvents: data.webhook.byEvents, }, create: { enabled: data.webhook?.enabled, events: data.webhook?.events, instanceId: this.monitor.waInstances[instanceName].instanceId, url: data.webhook?.url, headers: data.webhook?.headers, webhookBase64: data.webhook.base64, webhookByEvents: data.webhook.byEvents, }, }); } public async emit({ instanceName, origin, event, data, serverUrl, dateTime, sender, apiKey, local, integration, }: EmitData): Promise { if (integration && !integration.includes('webhook')) { return; } const instance = (await this.get(instanceName)) as wa.LocalWebHook; const webhookConfig = configService.get('WEBHOOK'); const webhookLocal = instance?.events; const webhookHeaders = { ...((instance?.headers as Record) || {}) }; if (webhookHeaders && 'jwt_key' in webhookHeaders) { const jwtKey = webhookHeaders['jwt_key']; const jwtToken = this.generateJwtToken(jwtKey); webhookHeaders['Authorization'] = `Bearer ${jwtToken}`; delete webhookHeaders['jwt_key']; } const we = event.replace(/[.-]/gm, '_').toUpperCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase(); const enabledLog = configService.get('LOG').LEVEL.includes('WEBHOOKS'); const regex = /^(https?:\/\/)/; const webhookData = { event, instance: instanceName, data, destination: instance?.url || `${webhookConfig.GLOBAL.URL}/${transformedWe}`, date_time: dateTime, sender, server_url: serverUrl, apikey: apiKey, }; if (local && instance?.enabled) { if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { let baseURL: string; if (instance?.webhookByEvents) { baseURL = `${instance?.url}/${transformedWe}`; } else { baseURL = instance?.url; } if (enabledLog) { const logData = { local: `${origin}.sendData-Webhook`, url: baseURL, ...webhookData, }; this.logger.log(logData); } try { if (instance?.enabled && regex.test(instance.url)) { const httpService = axios.create({ baseURL, headers: webhookHeaders as Record | undefined, timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000, }); await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl); } } catch (error) { this.logger.error({ local: `${origin}.sendData-Webhook`, message: `Todas as tentativas falharam: ${error?.message}`, hostName: error?.hostname, syscall: error?.syscall, code: error?.code, error: error?.errno, stack: error?.stack, name: error?.name, url: baseURL, server_url: serverUrl, }); } } } if (webhookConfig.GLOBAL?.ENABLED) { if (webhookConfig.EVENTS[we]) { let globalURL = webhookConfig.GLOBAL.URL; if (webhookConfig.GLOBAL.WEBHOOK_BY_EVENTS) { globalURL = `${globalURL}/${transformedWe}`; } if (enabledLog) { const logData = { local: `${origin}.sendData-Webhook-Global`, url: globalURL, ...webhookData, }; this.logger.log(logData); } try { if (regex.test(globalURL)) { const httpService = axios.create({ baseURL: globalURL, timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000, }); await this.retryWebhookRequest( httpService, webhookData, `${origin}.sendData-Webhook-Global`, globalURL, serverUrl, ); } } catch (error) { this.logger.error({ local: `${origin}.sendData-Webhook-Global`, message: `Todas as tentativas falharam: ${error?.message}`, hostName: error?.hostname, syscall: error?.syscall, code: error?.code, error: error?.errno, stack: error?.stack, name: error?.name, url: globalURL, server_url: serverUrl, }); } } } } private async retryWebhookRequest( httpService: AxiosInstance, webhookData: any, origin: string, baseURL: string, serverUrl: string, maxRetries?: number, delaySeconds?: number, ): Promise { const webhookConfig = configService.get('WEBHOOK'); const maxRetryAttempts = maxRetries ?? webhookConfig.RETRY?.MAX_ATTEMPTS ?? 10; const initialDelay = delaySeconds ?? webhookConfig.RETRY?.INITIAL_DELAY_SECONDS ?? 5; const useExponentialBackoff = webhookConfig.RETRY?.USE_EXPONENTIAL_BACKOFF ?? true; const maxDelay = webhookConfig.RETRY?.MAX_DELAY_SECONDS ?? 300; const jitterFactor = webhookConfig.RETRY?.JITTER_FACTOR ?? 0.2; const nonRetryableStatusCodes = webhookConfig.RETRY?.NON_RETRYABLE_STATUS_CODES ?? [400, 401, 403, 404, 422]; let attempts = 0; while (attempts < maxRetryAttempts) { try { await httpService.post('', webhookData); if (attempts > 0) { this.logger.log({ local: `${origin}`, message: `Sucesso no envio após ${attempts + 1} tentativas`, url: baseURL, }); } return; } catch (error) { attempts++; const isTimeout = error.code === 'ECONNABORTED'; if (error?.response?.status && nonRetryableStatusCodes.includes(error.response.status)) { this.logger.error({ local: `${origin}`, message: `Erro não recuperável (${error.response.status}): ${error?.message}. Cancelando retentativas.`, statusCode: error?.response?.status, url: baseURL, server_url: serverUrl, }); throw error; } this.logger.error({ local: `${origin}`, message: `Tentativa ${attempts}/${maxRetryAttempts} falhou: ${isTimeout ? 'Timeout da requisição' : error?.message}`, hostName: error?.hostname, syscall: error?.syscall, code: error?.code, isTimeout, statusCode: error?.response?.status, error: error?.errno, stack: error?.stack, name: error?.name, url: baseURL, server_url: serverUrl, }); if (attempts === maxRetryAttempts) { throw error; } let nextDelay = initialDelay; if (useExponentialBackoff) { nextDelay = Math.min(initialDelay * Math.pow(2, attempts - 1), maxDelay); const jitter = nextDelay * jitterFactor * (Math.random() * 2 - 1); nextDelay = Math.max(initialDelay, nextDelay + jitter); } this.logger.log({ local: `${origin}`, message: `Aguardando ${nextDelay.toFixed(1)} segundos antes da próxima tentativa`, url: baseURL, }); await new Promise((resolve) => setTimeout(resolve, nextDelay * 1000)); } } } private generateJwtToken(authToken: string): string { try { const payload = { iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 600, // 10 min expiration app: 'evolution', action: 'webhook', }; const token = jwt.sign(payload, authToken, { algorithm: 'HS256' }); return token; } catch (error) { this.logger.error({ local: 'WebhookController.generateJwtToken', message: `JWT generation failed: ${error?.message}`, }); throw error; } } }