intento arreglar server.ts y webhook-manager.ts

pull/1/head
borja 7 months ago
parent f472d8cba4
commit a86beb81b7

@ -1,134 +1,136 @@
/// <reference types="bun-types" /> /// <reference types="bun-types" />
import { CommandService } from './services/command'; import { CommandService } from './services/command';
import { ResponseQueue } from './services/response-queue'; import { ResponseQueue } from './services/response-queue';
import { WebhookManager } from './services/webhook-manager';
// Bun is available globally when running under Bun runtime // Bun is available globally when running under Bun runtime
declare global { declare global {
var Bun: typeof import('bun'); var Bun: typeof import('bun');
} }
const REQUIRED_ENV = [ export const REQUIRED_ENV = [
'EVOLUTION_API_URL', 'EVOLUTION_API_URL',
'EVOLUTION_API_KEY', 'EVOLUTION_API_KEY',
'EVOLUTION_API_INSTANCE', 'EVOLUTION_API_INSTANCE',
'WHATSAPP_COMMUNITY_ID', 'WHATSAPP_COMMUNITY_ID',
'CHATBOT_PHONE_NUMBER' 'CHATBOT_PHONE_NUMBER',
'WEBHOOK_URL'
]; ];
type WebhookPayload = { type WebhookPayload = {
event: string; event: string;
instance: string; instance: string;
data: any; data: any;
// Other fields from Evolution API // Other fields from Evolution API
}; };
export class WebhookServer { export class WebhookServer {
static async handleRequest(request: Request): Promise<Response> { static async handleRequest(request: Request): Promise<Response> {
// Health check endpoint // Health check endpoint
if (request.url.endsWith('/health')) { if (request.url.endsWith('/health')) {
return new Response('OK', { status: 200 }); return new Response('OK', { status: 200 });
} }
// 1. Method validation // 1. Method validation
if (request.method !== 'POST') { if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 }); return new Response('Method not allowed', { status: 405 });
} }
// 2. Content-Type validation // 2. Content-Type validation
const contentType = request.headers.get('content-type'); const contentType = request.headers.get('content-type');
if (!contentType?.includes('application/json')) { if (!contentType?.includes('application/json')) {
return new Response('Invalid content type', { status: 400 }); return new Response('Invalid content type', { status: 400 });
} }
try { try {
// 3. Parse and validate payload // 3. Parse and validate payload
const payload = await request.json() as WebhookPayload; const payload = await request.json() as WebhookPayload;
if (!payload.event || !payload.instance) { if (!payload.event || !payload.instance) {
return new Response('Invalid payload', { status: 400 }); return new Response('Invalid payload', { status: 400 });
} }
// 4. Verify instance matches // 4. Verify instance matches
if (payload.instance !== process.env.INSTANCE_NAME) { if (payload.instance !== process.env.INSTANCE_NAME) {
return new Response('Invalid instance', { status: 403 }); return new Response('Invalid instance', { status: 403 });
} }
// 5. Route events // 5. Route events
switch (payload.event) { switch (payload.event) {
case 'messages.upsert': case 'messages.upsert':
await this.handleMessageUpsert(payload.data); await this.handleMessageUpsert(payload.data);
break; break;
// Other events will be added later // Other events will be added later
} }
return new Response('OK', { status: 200 }); return new Response('OK', { status: 200 });
} catch (error) { } catch (error) {
return new Response('Invalid request', { status: 400 }); return new Response('Invalid request', { status: 400 });
} }
} }
private static async handleMessageUpsert(data: any) { private static async handleMessageUpsert(data: any) {
// Basic message validation // Basic message validation
if (!data?.key?.remoteJid || !data.message || !data.message.conversation) return; if (!data?.key?.remoteJid || !data.message || !data.message.conversation) return;
// Forward to command service only if: // Forward to command service only if:
// 1. It's a text message (has conversation field) // 1. It's a text message (has conversation field)
// 2. Starts with /tarea command // 2. Starts with /tarea command
const messageText = data.message.conversation; const messageText = data.message.conversation;
if (typeof messageText === 'string' && messageText.trim().startsWith('/tarea')) { if (typeof messageText === 'string' && messageText.trim().startsWith('/tarea')) {
const responses = await CommandService.handle({ const responses = await CommandService.handle({
sender: data.key.participant, sender: data.key.participant,
groupId: data.key.remoteJid, groupId: data.key.remoteJid,
message: messageText, message: messageText,
mentions: data.contextInfo?.mentionedJid || [] mentions: data.contextInfo?.mentionedJid || []
}); });
// Queue responses for sending // Queue responses for sending
await ResponseQueue.add(responses); await ResponseQueue.add(responses);
} }
} }
static validateEnv() { static validateEnv() {
const missing = REQUIRED_ENV.filter(v => !process.env[v]); const missing = REQUIRED_ENV.filter(v => !process.env[v]);
if (missing.length) { if (missing.length) {
console.error('❌ Missing required environment variables:'); console.error('❌ Missing required environment variables:');
missing.forEach(v => console.error(`- ${v}`)); missing.forEach(v => console.error(`- ${v}`));
console.error('Add these to your CapRover environment configuration'); console.error('Add these to your CapRover environment configuration');
process.exit(1); process.exit(1);
} }
if (process.env.CHATBOT_PHONE_NUMBER && if (process.env.CHATBOT_PHONE_NUMBER &&
!/^\d+$/.test(process.env.CHATBOT_PHONE_NUMBER)) { !/^\d+$/.test(process.env.CHATBOT_PHONE_NUMBER)) {
console.error('❌ CHATBOT_PHONE_NUMBER must contain only digits'); console.error('❌ CHATBOT_PHONE_NUMBER must contain only digits');
process.exit(1); process.exit(1);
} }
} }
static async start() { static async start() {
this.validateEnv(); this.validateEnv();
const PORT = process.env.PORT || '3007'; const PORT = process.env.PORT || '3007';
console.log('✅ Environment variables validated'); console.log('✅ Environment variables validated');
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
try { try {
await WebhookManager.registerWebhook(); await WebhookManager.registerWebhook();
const isActive = await WebhookManager.verifyWebhook(); const isActive = await WebhookManager.verifyWebhook();
if (!isActive) { if (!isActive) {
console.error('❌ Webhook verification failed'); console.error('❌ Webhook verification failed');
process.exit(1); process.exit(1);
} }
} catch (error) { } catch (error) {
console.error('❌ Failed to setup webhook:', error instanceof Error ? error.message : error); console.error('❌ Failed to setup webhook:', error instanceof Error ? error.message : error);
process.exit(1); process.exit(1);
} }
} }
const server = Bun.serve({ const server = Bun.serve({
port: parseInt(PORT), port: parseInt(PORT),
fetch: WebhookServer.handleRequest fetch: WebhookServer.handleRequest
}); });
console.log(`Server running on port ${PORT}`); console.log(`Server running on port ${PORT}`);
return server; return server;
} }
} }

@ -1,109 +1,108 @@
import { REQUIRED_ENV } from '../server'; import { REQUIRED_ENV } from '../server';
type WebhookConfig = { type WebhookConfig = {
url: string; url: string;
webhook_by_events: boolean; webhook_by_events: boolean;
webhook_base64: boolean; webhook_base64: boolean;
events: string[]; events: string[];
}; };
type WebhookResponse = { type WebhookResponse = {
webhook: { webhook: {
instanceName: string; instanceName: string;
webhook: { webhook: {
url: string; url: string;
events: string[]; events: string[];
enabled: boolean; enabled: boolean;
}; };
}; };
}; };
export class WebhookManager { export class WebhookManager {
private static readonly REQUIRED_EVENTS = [ private static readonly REQUIRED_EVENTS = [
'APPLICATION_STARTUP', 'APPLICATION_STARTUP',
'messages.upsert', 'MESSAGES.UPSERT',
'messages.update', 'MESSAGES.DELETE',
'messages.delete', 'GROUPS.UPDATE',
'groups.update', ];
];
private static validateConfig() {
private static validateConfig() { const missing = REQUIRED_ENV.filter(v => !process.env[v]);
const missing = REQUIRED_ENV.filter(v => !process.env[v]); if (missing.length) {
if (missing.length) { throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
throw new Error(`Missing required environment variables: ${missing.join(', ')}`); }
}
if (!process.env.WEBHOOK_URL) {
if (!process.env.WEBHOOK_URL) { throw new Error('WEBHOOK_URL environment variable is required');
throw new Error('WEBHOOK_URL environment variable is required'); }
}
try {
try { new URL(process.env.WEBHOOK_URL);
new URL(process.env.WEBHOOK_URL); } catch {
} catch { throw new Error('WEBHOOK_URL must be a valid URL');
throw new Error('WEBHOOK_URL must be a valid URL'); }
} }
}
private static getConfig(): WebhookConfig {
private static getConfig(): WebhookConfig { return {
return { url: process.env.WEBHOOK_URL!,
url: process.env.WEBHOOK_URL!, webhook_by_events: true,
webhook_by_events: true, webhook_base64: true,
webhook_base64: true, events: this.REQUIRED_EVENTS,
events: this.REQUIRED_EVENTS, };
}; }
}
private static getApiUrl(): string {
private static getApiUrl(): string { return `${process.env.EVOLUTION_API_URL}/webhook/set/${process.env.EVOLUTION_API_INSTANCE}`;
return `${process.env.EVOLUTION_API_URL}/webhook/set/${process.env.EVOLUTION_API_INSTANCE}`; }
}
private static getHeaders(): HeadersInit {
private static getHeaders(): HeadersInit { return {
return { apikey: process.env.EVOLUTION_API_KEY!,
apikey: process.env.EVOLUTION_API_KEY!, 'Content-Type': 'application/json',
'Content-Type': 'application/json', };
}; }
}
static async registerWebhook(): Promise<WebhookResponse> {
static async registerWebhook(): Promise<WebhookResponse> { this.validateConfig();
this.validateConfig();
const response = await fetch(this.getApiUrl(), {
const response = await fetch(this.getApiUrl(), { method: 'POST',
method: 'POST', headers: this.getHeaders(),
headers: this.getHeaders(), body: JSON.stringify(this.getConfig()),
body: JSON.stringify(this.getConfig()), });
});
if (!response.ok) {
if (!response.ok) { throw new Error(`Failed to register webhook: ${response.statusText}`);
throw new Error(`Failed to register webhook: ${response.statusText}`); }
}
const data = await response.json();
const data = await response.json();
if (!data?.webhook?.webhook?.enabled) {
if (!data?.webhook?.webhook?.enabled) { throw new Error('Webhook registration failed - not enabled in response');
throw new Error('Webhook registration failed - not enabled in response'); }
}
console.log('✅ Webhook successfully registered:', {
console.log('✅ Webhook successfully registered:', { url: data.webhook.webhook.url,
url: data.webhook.webhook.url, events: data.webhook.webhook.events,
events: data.webhook.webhook.events, });
});
return data;
return data; }
}
static async verifyWebhook(): Promise<boolean> {
static async verifyWebhook(): Promise<boolean> { try {
try { const response = await fetch(`${process.env.EVOLUTION_API_URL}/webhook/find/${process.env.EVOLUTION_API_INSTANCE}`, {
const response = await fetch(`${process.env.EVOLUTION_API_URL}/webhook/find/${process.env.EVOLUTION_API_INSTANCE}`, { method: 'GET',
method: 'GET', headers: this.getHeaders(),
headers: this.getHeaders(), });
});
if (!response.ok) return false;
if (!response.ok) return false;
const data = await response.json();
const data = await response.json(); return data?.webhook?.enabled === true;
return data?.webhook?.enabled === true; } catch {
} catch { return false;
return false; }
} }
}
} }

Loading…
Cancel
Save