/// import { CommandService } from './services/command'; import { ResponseQueue } from './services/response-queue'; // Bun is available globally when running under Bun runtime declare global { var Bun: typeof import('bun'); } const REQUIRED_ENV = [ 'EVOLUTION_API_URL', 'EVOLUTION_API_KEY', 'EVOLUTION_API_INSTANCE', 'WHATSAPP_COMMUNITY_ID', 'CHATBOT_PHONE_NUMBER' ]; type WebhookPayload = { event: string; instance: string; data: any; // Other fields from Evolution API }; export class WebhookServer { static async handleRequest(request: Request): Promise { // Health check endpoint if (request.url.endsWith('/health')) { return new Response('OK', { status: 200 }); } // 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 if (payload.instance !== process.env.INSTANCE_NAME) { return new Response('Invalid instance', { status: 403 }); } // 5. Route events switch (payload.event) { case 'messages.upsert': await this.handleMessageUpsert(payload.data); break; // Other events will be added later } return new Response('OK', { status: 200 }); } catch (error) { return new Response('Invalid request', { status: 400 }); } } private static async handleMessageUpsert(data: any) { // Basic message validation if (!data?.key?.remoteJid || !data.message || !data.message.conversation) 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')) { const responses = await CommandService.handle({ sender: data.key.participant, groupId: data.key.remoteJid, message: messageText, mentions: data.contextInfo?.mentionedJid || [] }); // Queue responses for sending await ResponseQueue.add(responses); } } static validateEnv() { 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 start() { this.validateEnv(); const PORT = process.env.PORT || '3007'; console.log('✅ Environment variables validated'); if (process.env.NODE_ENV !== 'test') { const server = Bun.serve({ port: parseInt(PORT), fetch: WebhookServer.handleRequest }); console.log(`Server running on port ${PORT}`); return server; } } }