/// import { CommandService } from './services/command'; import { ResponseQueue } from './services/response-queue'; import { WebhookManager } from './services/webhook-manager'; // Bun is available globally when running under Bun runtime declare global { var Bun: typeof import('bun'); } export const REQUIRED_ENV = [ 'EVOLUTION_API_URL', 'EVOLUTION_API_KEY', 'EVOLUTION_API_INSTANCE', 'WHATSAPP_COMMUNITY_ID', 'CHATBOT_PHONE_NUMBER', 'WEBHOOK_URL' ]; type WebhookPayload = { event: string; instance: string; data: any; // Other fields from Evolution API }; export class WebhookServer { private static getBaseUrl(request: Request): string { const proto = request.headers.get('x-forwarded-proto') || 'http'; const host = request.headers.get('x-forwarded-host') || request.headers.get('host'); return `${proto}://${host}`; } static async handleRequest(request: Request): Promise { // Health check endpoint const url = new URL(request.url); if (url.pathname.endsWith('/health')) { return new Response('OK', { status: 200 }); } if (process.env.NODE_ENV !== 'test') { console.log('â„šī¸ Incoming webhook request:') // console.log('â„šī¸ Incoming webhook request:', { // method: request.method, // path: url.pathname, // time: new Date().toISOString() // }); } // 1. Method validation if (request.method !== 'POST') { return new Response('đŸšĢ Method not allowed', { status: 405 }); } // 2. Content-Type validation const contentType = request.headers.get('content-type'); if (!contentType?.includes('application/json')) { return new Response('đŸšĢ Invalid content type', { status: 400 }); } try { // 3. Parse and validate payload const payload = await request.json() as WebhookPayload; if (!payload.event || !payload.instance) { return new Response('đŸšĢ Invalid payload', { status: 400 }); } // 4. Verify instance matches (skip in test environment unless TEST_VERIFY_INSTANCE is set) if ((process.env.NODE_ENV !== 'test' || process.env.TEST_VERIFY_INSTANCE) && payload.instance !== process.env.EVOLUTION_API_INSTANCE) { return new Response('đŸšĢ Invalid instance', { status: 403 }); } // 5. Route events // console.log('â„šī¸ Webhook event received:', { // event: payload.event, // instance: payload.instance, // data: payload.data ? '[...]' : null // }); switch (payload.event) { case 'messages.upsert': // console.log('â„šī¸ Handling message upsert:', { // groupId: payload.data?.key?.remoteJid, // message: payload.data?.message?.conversation // }); await WebhookServer.handleMessageUpsert(payload.data); break; // Other events will be added later } return new Response('OK', { status: 200 }); } catch (error) { console.error('❌ Error processing webhook:', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, time: new Date().toISOString() }); return new Response('Invalid request', { status: 400 }); } } static async handleMessageUpsert(data: any) { if (!data?.key?.remoteJid || !data.message || !data.message.conversation) { console.log('âš ī¸ Invalid message format - missing required fields'); return; } // Forward to command service only if: // 1. It's a text message (has conversation field) // 2. Starts with /tarea command const messageText = data.message.conversation; if (typeof messageText === 'string' && messageText.trim().startsWith('/tarea')) { // Parse command components const commandParts = messageText.trim().split(/\s+/); const command = commandParts[0].toLowerCase(); if (commandParts.length < 2) { console.log('âš ī¸ Invalid /tarea command - missing action'); return; } const action = commandParts[1].toLowerCase(); let descriptionParts = []; let dueDate = ''; // Process remaining parts for (let i = 2; i < commandParts.length; i++) { const part = commandParts[i]; // Check for date (YYYY-MM-DD) if (/^\d{4}-\d{2}-\d{2}$/.test(part)) { const date = new Date(part); const today = new Date(); today.setHours(0, 0, 0, 0); if (date >= today) { dueDate = part; } else { descriptionParts.push(part); // Keep past dates in description } } else { descriptionParts.push(part); } } const description = descriptionParts.join(' '); console.log('🔍 Detected /tarea command:', { timestamp: new Date().toISOString(), from: data.key.participant, group: data.key.remoteJid, rawMessage: messageText }); const mentions = data.contextInfo?.mentionedJid || []; console.log('✅ Successfully parsed command:', { action, description, dueDate: dueDate || 'none', mentionCount: mentions.length }); const responses = await CommandService.handle({ sender: data.key.participant, groupId: data.key.remoteJid, message: messageText, mentions }); // Queue responses for sending await ResponseQueue.add(responses); } } static validateEnv() { console.log('â„šī¸ Checking environment variables...'); console.log('EVOLUTION_API_URL:', process.env.EVOLUTION_API_URL ? '***' : 'MISSING'); console.log('EVOLUTION_API_INSTANCE:', process.env.EVOLUTION_API_INSTANCE || 'MISSING'); console.log('WEBHOOK_URL:', process.env.WEBHOOK_URL ? `${process.env.WEBHOOK_URL.substring(0, 20)}...` : 'MISSING'); const missing = REQUIRED_ENV.filter(v => !process.env[v]); if (missing.length) { console.error('❌ Missing required environment variables:'); missing.forEach(v => console.error(`- ${v}`)); console.error('Add these to your CapRover environment configuration'); process.exit(1); } if (process.env.CHATBOT_PHONE_NUMBER && !/^\d+$/.test(process.env.CHATBOT_PHONE_NUMBER)) { console.error('❌ CHATBOT_PHONE_NUMBER must contain only digits'); process.exit(1); } } static async start() { this.validateEnv(); const PORT = process.env.PORT || '3007'; console.log('✅ Environment variables validated'); if (process.env.NODE_ENV !== 'test') { try { await WebhookManager.registerWebhook(); // Add small delay to allow webhook to propagate await new Promise(resolve => setTimeout(resolve, 1000)); const isActive = await WebhookManager.verifyWebhook(); if (!isActive) { console.error('❌ Webhook verification failed - retrying in 2 seconds...'); await new Promise(resolve => setTimeout(resolve, 2000)); const isActiveRetry = await WebhookManager.verifyWebhook(); if (!isActiveRetry) { console.error('❌ Webhook verification failed after retry'); process.exit(1); } } } catch (error) { console.error('❌ Failed to setup webhook:', error instanceof Error ? error.message : error); process.exit(1); } } const server = Bun.serve({ port: parseInt(PORT), fetch: (request) => WebhookServer.handleRequest(request) }); console.log(`Server running on port ${PORT}`); return server; } }