|
|
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 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<wa.LocalWebHook> { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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<void> { |
|
|
if (integration && !integration.includes('webhook')) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const instance = (await this.get(instanceName)) as wa.LocalWebHook; |
|
|
|
|
|
const webhookConfig = configService.get<Webhook>('WEBHOOK'); |
|
|
const webhookLocal = instance?.events; |
|
|
const webhookHeaders = { ...((instance?.headers as Record<string, string>) || {}) }; |
|
|
|
|
|
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>('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<string, string> | 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<void> { |
|
|
const webhookConfig = configService.get<Webhook>('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, |
|
|
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; |
|
|
} |
|
|
} |
|
|
} |
|
|
|