/** * Webhook Dispatcher * Sends events to configured webhooks */ import crypto from 'crypto' import { query } from '../../utils/database' import { baileysManager } from '../baileys/manager' interface Webhook { id: string url: string secret: string | null events: string[] headers: Record retry_count: number timeout_ms: number instance_id: string | null } class WebhookDispatcher { private initialized = false async initialize() { if (this.initialized) return // Listen to all Baileys events const events = [ 'message.received', 'message.sent', 'message.status', 'instance.connected', 'instance.disconnected', 'instance.status', 'instance.qr' ] for (const eventType of events) { baileysManager.on(eventType, (data: any) => { this.dispatch(data.instanceId || null, eventType, data) }) } this.initialized = true console.log('[WebhookDispatcher] Initialized') } async dispatch(instanceId: string | null, eventType: string, payload: any) { try { // Get matching webhooks const webhooks = await query( `SELECT id, url, secret, events, headers, retry_count, timeout_ms, instance_id FROM webhooks WHERE is_active = TRUE AND $1 = ANY(events) AND (instance_id IS NULL OR instance_id = $2)`, [eventType, instanceId] ) for (const webhook of webhooks.rows) { this.deliverWithRetry(webhook, eventType, payload) } } catch (error) { console.error('[WebhookDispatcher] Error dispatching:', error) } } private async deliverWithRetry( webhook: Webhook, eventType: string, payload: any, attempt = 1 ) { try { await this.deliver(webhook, eventType, payload) // Log success await this.logDelivery(webhook.id, eventType, payload, { status: 200, attempt, success: true }) } catch (error) { const errorMessage = (error as Error).message // Log failure await this.logDelivery(webhook.id, eventType, payload, { status: 0, attempt, success: false, error: errorMessage }) // Retry if under limit if (attempt < webhook.retry_count) { const delay = Math.pow(2, attempt) * 1000 // Exponential backoff setTimeout(() => { this.deliverWithRetry(webhook, eventType, payload, attempt + 1) }, delay) } } } private async deliver(webhook: Webhook, eventType: string, payload: any) { const body = JSON.stringify({ event: eventType, timestamp: new Date().toISOString(), data: payload }) const headers: Record = { 'Content-Type': 'application/json', 'X-Webhook-Event': eventType, 'X-Webhook-Timestamp': Date.now().toString(), ...(webhook.headers || {}) } // Add HMAC signature if secret is configured if (webhook.secret) { const signature = crypto .createHmac('sha256', webhook.secret) .update(body) .digest('hex') headers['X-Webhook-Signature'] = `sha256=${signature}` } // Check if the URL is pointing to our own debug receiver // If so, use internal URL to bypass authentik let targetUrl = webhook.url if (webhook.url.includes('/api/debug/webhook-receiver')) { const internalPort = process.env.PORT || 3000 targetUrl = `http://localhost:${internalPort}/api/debug/webhook-receiver` } const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), webhook.timeout_ms) try { const response = await fetch(targetUrl, { method: 'POST', headers, body, signal: controller.signal }) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } } finally { clearTimeout(timeout) } } private async logDelivery( webhookId: string, eventType: string, payload: any, result: { status: number; attempt: number; success: boolean; error?: string } ) { try { await query( `INSERT INTO webhook_logs (webhook_id, event_type, payload, response_status, error_message, attempt, delivered_at) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [ webhookId, eventType, JSON.stringify(payload), result.status, result.error || null, result.attempt, result.success ? new Date() : null ] ) } catch (error) { console.error('[WebhookDispatcher] Error logging delivery:', error) } } } export const webhookDispatcher = new WebhookDispatcher()