import { HttpService } from '@nestjs/axios'; import { Logger } from '@nestjs/common'; import crypto from 'crypto'; import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { WEBHOOK_RESPONSE_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/webhook/webhook-response'; export type CallWebhookJobData = { targetUrl: string; eventName: string; objectMetadata: { id: string; nameSingular: string }; workspaceId: string; webhookId: string; eventDate: Date; record: any; updatedFields?: string[]; secret?: string; }; @Processor(MessageQueue.webhookQueue) export class CallWebhookJob { private readonly logger = new Logger(CallWebhookJob.name); constructor( private readonly httpService: HttpService, private readonly analyticsService: AnalyticsService, ) {} private generateSignature( payload: CallWebhookJobData, secret: string, timestamp: string, ): string { return crypto .createHmac('sha256', secret) .update(`${timestamp}:${JSON.stringify(payload)}`) .digest('hex'); } @Process(CallWebhookJob.name) async handle(data: CallWebhookJobData): Promise { const commonPayload = { url: data.targetUrl, webhookId: data.webhookId, eventName: data.eventName, }; const analytics = this.analyticsService.createAnalyticsContext({ workspaceId: data.workspaceId, }); try { const headers: Record = { 'Content-Type': 'application/json', }; const { secret, ...payloadWithoutSecret } = data; if (secret) { headers['X-Twenty-Webhook-Timestamp'] = Date.now().toString(); headers['X-Twenty-Webhook-Signature'] = this.generateSignature( payloadWithoutSecret, secret, headers['X-Twenty-Webhook-Timestamp'], ); headers['X-Twenty-Webhook-Nonce'] = crypto .randomBytes(16) .toString('hex'); } const response = await this.httpService.axiosRef.post( data.targetUrl, payloadWithoutSecret, { headers }, ); const success = response.status >= 200 && response.status < 300; analytics.track(WEBHOOK_RESPONSE_EVENT, { status: response.status, success, ...commonPayload, }); } catch (err) { analytics.track(WEBHOOK_RESPONSE_EVENT, { success: false, ...commonPayload, ...(err.response && { status: err.response.status }), }); this.logger.error( `Error calling webhook on targetUrl '${data.targetUrl}': ${err}`, ); } } }