import { REQUIRED_ENV } from '../server'; type WebhookConfig = { url: string; enabled: boolean; webhook_by_events: boolean; webhook_base64: boolean; events: string[]; }; type WebhookResponse = { webhook: { instanceName: string; webhook: { url: string; events: string[]; enabled: boolean; }; }; }; export class WebhookManager { private static readonly REQUIRED_EVENTS = [ 'APPLICATION_STARTUP', 'MESSAGES_UPSERT', 'GROUPS_UPSERT', 'MESSAGES_UPDATE', 'MESSAGES_DELETE', 'PRESENCE_UPDATE', 'CONTACTS_UPDATE', 'CHATS_UPDATE' ]; private static validateConfig() { const missing = REQUIRED_ENV.filter(v => !process.env[v]); if (missing.length) { throw new Error(`Missing required environment variables: ${missing.join(', ')}`); } if (!process.env.WEBHOOK_URL?.trim()) { throw new Error('WEBHOOK_URL environment variable is required and cannot be empty'); } try { const url = new URL(process.env.WEBHOOK_URL); // Enhanced internal Docker URL validation if (process.env.WEBHOOK_URL.startsWith('http://srv-captain--')) { if (!url.port) { console.warn('⚠️ Internal Docker URL missing port number - this may cause connection issues'); // Auto-fix for common CapRover default port if (process.env.PORT) { process.env.WEBHOOK_URL = process.env.WEBHOOK_URL.replace( 'http://srv-captain--', `http://srv-captain--:${process.env.PORT}/` ); console.warn(`⚠️ Auto-corrected URL to: ${process.env.WEBHOOK_URL.substring(0, 30)}...`); return; // Skip further validation for corrected URL } } } // Allow internal docker URLs in production if (process.env.NODE_ENV === 'production') { if (!['http:', 'https:', 'http://srv-captain--'].some(prefix => process.env.WEBHOOK_URL?.startsWith(prefix))) { console.warn('Production WEBHOOK_URL should use http/https or internal docker URL'); } } else if (!['http:', 'https:'].includes(url.protocol)) { throw new Error('WEBHOOK_URL must use http or https protocol'); } } catch (e) { throw new Error(`Invalid WEBHOOK_URL: ${e.message}`); } } private static getConfig(): { webhook: WebhookConfig } { return { webhook: { url: process.env.WEBHOOK_URL!, enabled: true, webhook_by_events: true, webhook_base64: true, events: this.REQUIRED_EVENTS, } }; } private static getApiUrl(): string { return `${process.env.EVOLUTION_API_URL}/webhook/set/${process.env.EVOLUTION_API_INSTANCE}`; } private static getHeaders(): HeadersInit { return { apikey: process.env.EVOLUTION_API_KEY!, 'Content-Type': 'application/json', }; } static async registerWebhook(): Promise { this.validateConfig(); // First test if our own endpoint is reachable const endpointTest = await this.testWebhookEndpoint(); if (!endpointTest) { throw new Error('Webhook endpoint test failed - check your server logs'); } const config = this.getConfig(); const apiUrl = this.getApiUrl(); const logSafeUrl = (url: string) => url ? `${url.substring(0, 20)}...` : 'invalid-url'; console.log('ℹ️ Attempting to register webhook:', { apiUrl, config: { ...config, webhook: { ...config.webhook, url: logSafeUrl(config.webhook.url), events: config.webhook.events } } }); const response = await fetch(apiUrl, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify(config), }); if (!response.ok) { let errorDetails = response.statusText; try { const errorBody = await response.text(); errorDetails += ` - ${errorBody}`; } catch { } throw new Error(`Failed to register webhook: ${errorDetails}`); } const data = await response.json(); if (!data?.enabled) { throw new Error('Webhook registration failed - not enabled in response'); } console.log('✅ Webhook successfully registered:', { url: data.url, events: data.events, id: data.id }); return data; } static async verifyWebhook(): Promise { try { const url = `${process.env.EVOLUTION_API_URL}/webhook/find/${process.env.EVOLUTION_API_INSTANCE}`; console.log('ℹ️ Verifying webhook at:', url); const response = await fetch(url, { method: 'GET', headers: this.getHeaders(), }); if (!response.ok) { const body = await response.text(); console.error('❌ Webhook verification failed:', { status: response.status, statusText: response.statusText, body }); return false; } const data = await response.json(); console.log('ℹ️ Webhook verification response:', data); const isEnabled = data?.enabled === true; if (!isEnabled) { console.error('❌ Webhook not enabled in verification response'); } return isEnabled; } catch (error) { console.error('❌ Webhook verification error:', error instanceof Error ? error.stack : error); return false; } } static async testWebhookEndpoint(): Promise { try { if (!process.env.WEBHOOK_URL) { throw new Error('WEBHOOK_URL is not set'); } // Skip self-test in production if using internal URL if (process.env.NODE_ENV === 'production' && process.env.WEBHOOK_URL.startsWith('http://srv-captain--')) { console.log('ℹ️ Skipping self-test for internal production URL'); return true; } console.log('ℹ️ Testing webhook endpoint:', process.env.WEBHOOK_URL); const testPayload = { test: true, timestamp: new Date().toISOString() }; const response = await fetch(process.env.WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(testPayload), timeout: 5000 // Add timeout to prevent hanging }); if (!response.ok) { const body = await response.text(); console.error('❌ Webhook test failed:', { status: response.status, statusText: response.statusText, body }); return false; } console.log('✅ Webhook endpoint test successful'); return true; } catch (error) { console.error('❌ Webhook test error:', error instanceof Error ? error.stack : error); return false; } } }