intento arreglar server.ts y webhook-manager.ts
parent
f472d8cba4
commit
a86beb81b7
@ -1,134 +1,136 @@
|
||||
/// <reference types="bun-types" />
|
||||
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');
|
||||
var Bun: typeof import('bun');
|
||||
}
|
||||
|
||||
const REQUIRED_ENV = [
|
||||
'EVOLUTION_API_URL',
|
||||
'EVOLUTION_API_KEY',
|
||||
'EVOLUTION_API_INSTANCE',
|
||||
'WHATSAPP_COMMUNITY_ID',
|
||||
'CHATBOT_PHONE_NUMBER'
|
||||
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
|
||||
event: string;
|
||||
instance: string;
|
||||
data: any;
|
||||
// Other fields from Evolution API
|
||||
};
|
||||
|
||||
export class WebhookServer {
|
||||
static async handleRequest(request: Request): Promise<Response> {
|
||||
// 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 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();
|
||||
const isActive = await WebhookManager.verifyWebhook();
|
||||
if (!isActive) {
|
||||
console.error('❌ Webhook verification failed');
|
||||
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: WebhookServer.handleRequest
|
||||
});
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
return server;
|
||||
}
|
||||
static async handleRequest(request: Request): Promise<Response> {
|
||||
// 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 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();
|
||||
const isActive = await WebhookManager.verifyWebhook();
|
||||
if (!isActive) {
|
||||
console.error('❌ Webhook verification failed');
|
||||
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: WebhookServer.handleRequest
|
||||
});
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
return server;
|
||||
}
|
||||
}
|
||||
|
@ -1,109 +1,108 @@
|
||||
import { REQUIRED_ENV } from '../server';
|
||||
|
||||
type WebhookConfig = {
|
||||
url: string;
|
||||
webhook_by_events: boolean;
|
||||
webhook_base64: boolean;
|
||||
events: string[];
|
||||
url: string;
|
||||
webhook_by_events: boolean;
|
||||
webhook_base64: boolean;
|
||||
events: string[];
|
||||
};
|
||||
|
||||
type WebhookResponse = {
|
||||
webhook: {
|
||||
instanceName: string;
|
||||
webhook: {
|
||||
url: string;
|
||||
events: string[];
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
webhook: {
|
||||
instanceName: string;
|
||||
webhook: {
|
||||
url: string;
|
||||
events: string[];
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export class WebhookManager {
|
||||
private static readonly REQUIRED_EVENTS = [
|
||||
'APPLICATION_STARTUP',
|
||||
'messages.upsert',
|
||||
'messages.update',
|
||||
'messages.delete',
|
||||
'groups.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) {
|
||||
throw new Error('WEBHOOK_URL environment variable is required');
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(process.env.WEBHOOK_URL);
|
||||
} catch {
|
||||
throw new Error('WEBHOOK_URL must be a valid URL');
|
||||
}
|
||||
}
|
||||
|
||||
private static getConfig(): WebhookConfig {
|
||||
return {
|
||||
url: process.env.WEBHOOK_URL!,
|
||||
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<WebhookResponse> {
|
||||
this.validateConfig();
|
||||
|
||||
const response = await fetch(this.getApiUrl(), {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(this.getConfig()),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to register webhook: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data?.webhook?.webhook?.enabled) {
|
||||
throw new Error('Webhook registration failed - not enabled in response');
|
||||
}
|
||||
|
||||
console.log('✅ Webhook successfully registered:', {
|
||||
url: data.webhook.webhook.url,
|
||||
events: data.webhook.webhook.events,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async verifyWebhook(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${process.env.EVOLUTION_API_URL}/webhook/find/${process.env.EVOLUTION_API_INSTANCE}`, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) return false;
|
||||
|
||||
const data = await response.json();
|
||||
return data?.webhook?.enabled === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
private static readonly REQUIRED_EVENTS = [
|
||||
'APPLICATION_STARTUP',
|
||||
'MESSAGES.UPSERT',
|
||||
'MESSAGES.DELETE',
|
||||
'GROUPS.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) {
|
||||
throw new Error('WEBHOOK_URL environment variable is required');
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(process.env.WEBHOOK_URL);
|
||||
} catch {
|
||||
throw new Error('WEBHOOK_URL must be a valid URL');
|
||||
}
|
||||
}
|
||||
|
||||
private static getConfig(): WebhookConfig {
|
||||
return {
|
||||
url: process.env.WEBHOOK_URL!,
|
||||
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<WebhookResponse> {
|
||||
this.validateConfig();
|
||||
|
||||
const response = await fetch(this.getApiUrl(), {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(this.getConfig()),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to register webhook: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data?.webhook?.webhook?.enabled) {
|
||||
throw new Error('Webhook registration failed - not enabled in response');
|
||||
}
|
||||
|
||||
console.log('✅ Webhook successfully registered:', {
|
||||
url: data.webhook.webhook.url,
|
||||
events: data.webhook.webhook.events,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async verifyWebhook(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${process.env.EVOLUTION_API_URL}/webhook/find/${process.env.EVOLUTION_API_INSTANCE}`, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) return false;
|
||||
|
||||
const data = await response.json();
|
||||
return data?.webhook?.enabled === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue