///
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;
}
}