|
|
|
|
@ -1,4 +1,4 @@
|
|
|
|
|
//// <reference types="bun-types" />
|
|
|
|
|
/// <reference types="bun-types" />
|
|
|
|
|
import type { Database } from 'bun:sqlite';
|
|
|
|
|
import { GroupSyncService } from './services/group-sync';
|
|
|
|
|
import { ContactsService } from './services/contacts';
|
|
|
|
|
@ -11,216 +11,241 @@ import { handleHealthRequest } from './http/health';
|
|
|
|
|
import { startServices } from './http/bootstrap';
|
|
|
|
|
import { handleMessageUpsert as handleMessageUpsertFn } from './http/webhook-handler';
|
|
|
|
|
|
|
|
|
|
// 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',
|
|
|
|
|
'CHATBOT_PHONE_NUMBER',
|
|
|
|
|
'WEBHOOK_URL'
|
|
|
|
|
'EVOLUTION_API_URL',
|
|
|
|
|
'EVOLUTION_API_KEY',
|
|
|
|
|
'EVOLUTION_API_INSTANCE',
|
|
|
|
|
'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 dbInstance: Database = db;
|
|
|
|
|
|
|
|
|
|
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}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static getMessageText(message: any): string {
|
|
|
|
|
if (!message || typeof message !== 'object') return '';
|
|
|
|
|
const text =
|
|
|
|
|
message.conversation ||
|
|
|
|
|
message?.extendedTextMessage?.text ||
|
|
|
|
|
message?.imageMessage?.caption ||
|
|
|
|
|
message?.videoMessage?.caption ||
|
|
|
|
|
'';
|
|
|
|
|
return typeof text === 'string' ? text.trim() : '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async handleRequest(request: Request): Promise<Response> {
|
|
|
|
|
// Health check endpoint y métricas
|
|
|
|
|
const url = new URL(request.url);
|
|
|
|
|
if (url.pathname.endsWith('/metrics')) {
|
|
|
|
|
return await handleMetricsRequest(request, WebhookServer.dbInstance);
|
|
|
|
|
}
|
|
|
|
|
if (url.pathname.endsWith('/health')) {
|
|
|
|
|
return await handleHealthRequest(url, WebhookServer.dbInstance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
console.log('ℹ️ Incoming webhook request:')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// Normalize event name to handle different casing/format (e.g., MESSAGES_UPSERT)
|
|
|
|
|
const evt = String(payload.event);
|
|
|
|
|
const evtNorm = evt.toLowerCase().replace(/_/g, '.');
|
|
|
|
|
|
|
|
|
|
// Contabilizar evento de webhook por tipo
|
|
|
|
|
try {
|
|
|
|
|
Metrics.inc(`webhook_events_total_${evtNorm.replace(/\./g, '_')}`);
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
switch (evtNorm) {
|
|
|
|
|
case 'messages.upsert':
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
console.log('ℹ️ Handling message upsert:', {
|
|
|
|
|
groupId: payload.data?.key?.remoteJid,
|
|
|
|
|
message: payload.data?.message?.conversation,
|
|
|
|
|
rawEvent: evt
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
await WebhookServer.handleMessageUpsert(payload.data);
|
|
|
|
|
break;
|
|
|
|
|
case 'contacts.update':
|
|
|
|
|
case 'chats.update':
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
console.log('ℹ️ Handling contacts/chats update event:', { rawEvent: evt });
|
|
|
|
|
}
|
|
|
|
|
ContactsService.updateFromWebhook(payload.data);
|
|
|
|
|
break;
|
|
|
|
|
case 'groups.upsert':
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
console.log('ℹ️ Handling groups upsert event:', { rawEvent: evt });
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const res = await GroupSyncService.syncGroups();
|
|
|
|
|
GroupSyncService.refreshActiveGroupsCache();
|
|
|
|
|
const changed = GroupSyncService.getLastChangedActive();
|
|
|
|
|
if (changed.length > 0) {
|
|
|
|
|
await GroupSyncService.syncMembersForGroups(changed);
|
|
|
|
|
} else {
|
|
|
|
|
await GroupSyncService.syncMembersForActiveGroups();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('❌ Error handling groups.upsert:', e);
|
|
|
|
|
}
|
|
|
|
|
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()
|
|
|
|
|
});
|
|
|
|
|
try { Metrics.inc('webhook_errors_total'); } catch {}
|
|
|
|
|
return new Response('Invalid request', { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async handleMessageUpsert(data: any) {
|
|
|
|
|
return await handleMessageUpsertFn(data, WebhookServer.dbInstance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)}...` : 'NOT SET');
|
|
|
|
|
console.log('WHATSAPP_COMMUNITY_ID:', process.env.WHATSAPP_COMMUNITY_ID ? '***' : 'NOT SET (se mostrarán comunidades disponibles)');
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
// Run database migrations (up-only) before starting services
|
|
|
|
|
await Migrator.migrateToLatest(this.dbInstance);
|
|
|
|
|
|
|
|
|
|
// Etapa 7: seed inicial de grupos permitidos desde ALLOWED_GROUPS (best-effort)
|
|
|
|
|
try { AllowedGroups.seedFromEnv(); } catch {}
|
|
|
|
|
|
|
|
|
|
const PORT = process.env.PORT || '3007';
|
|
|
|
|
console.log('✅ Environment variables validated');
|
|
|
|
|
// A0: pre-crear contadores para que aparezcan en /metrics
|
|
|
|
|
try {
|
|
|
|
|
Metrics.inc('onboarding_prompts_sent_total', 0);
|
|
|
|
|
Metrics.inc('onboarding_prompts_skipped_total', 0);
|
|
|
|
|
Metrics.inc('onboarding_assign_failures_total', 0);
|
|
|
|
|
|
|
|
|
|
// Precalentar métricas de reacciones por emoji
|
|
|
|
|
for (const emoji of ['robot', 'warn', 'check', 'other']) {
|
|
|
|
|
Metrics.inc('reactions_enqueued_total', 0, { emoji });
|
|
|
|
|
Metrics.inc('reactions_sent_total', 0, { emoji });
|
|
|
|
|
Metrics.inc('reactions_failed_total', 0, { emoji });
|
|
|
|
|
}
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
try {
|
|
|
|
|
await startServices(this.dbInstance);
|
|
|
|
|
} 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;
|
|
|
|
|
}
|
|
|
|
|
static dbInstance: Database = db;
|
|
|
|
|
|
|
|
|
|
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}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static getMessageText(message: any): string {
|
|
|
|
|
if (!message || typeof message !== 'object') return '';
|
|
|
|
|
const text =
|
|
|
|
|
message.conversation ||
|
|
|
|
|
message?.extendedTextMessage?.text ||
|
|
|
|
|
message?.imageMessage?.caption ||
|
|
|
|
|
message?.videoMessage?.caption ||
|
|
|
|
|
'';
|
|
|
|
|
return typeof text === 'string' ? text.trim() : '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async handleRequest(request: Request): Promise<Response> {
|
|
|
|
|
// Health check endpoint y métricas
|
|
|
|
|
const url = new URL(request.url);
|
|
|
|
|
if (url.pathname.endsWith('/metrics')) {
|
|
|
|
|
return await handleMetricsRequest(request, WebhookServer.dbInstance);
|
|
|
|
|
}
|
|
|
|
|
if (url.pathname.endsWith('/health')) {
|
|
|
|
|
return await handleHealthRequest(url, WebhookServer.dbInstance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
console.log('ℹ️ Incoming webhook request:');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// Normalize event name to handle different casing/format (e.g., MESSAGES_UPSERT)
|
|
|
|
|
const evt = String(payload.event);
|
|
|
|
|
const evtNorm = evt.toLowerCase().replace(/_/g, '.');
|
|
|
|
|
|
|
|
|
|
// Contabilizar evento de webhook por tipo
|
|
|
|
|
try {
|
|
|
|
|
Metrics.inc(`webhook_events_total_${evtNorm.replace(/\./g, '_')}`);
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
switch (evtNorm) {
|
|
|
|
|
case 'messages.upsert':
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
console.log('ℹ️ Handling message upsert:', {
|
|
|
|
|
groupId: payload.data?.key?.remoteJid,
|
|
|
|
|
message: payload.data?.message?.conversation,
|
|
|
|
|
rawEvent: evt
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
await WebhookServer.handleMessageUpsert(payload.data);
|
|
|
|
|
break;
|
|
|
|
|
case 'contacts.update':
|
|
|
|
|
case 'chats.update':
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
console.log('ℹ️ Handling contacts/chats update event:', {
|
|
|
|
|
rawEvent: evt
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
ContactsService.updateFromWebhook(payload.data);
|
|
|
|
|
break;
|
|
|
|
|
case 'groups.upsert':
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
console.log('ℹ️ Handling groups upsert event:', { rawEvent: evt });
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const res = await GroupSyncService.syncGroups();
|
|
|
|
|
GroupSyncService.refreshActiveGroupsCache();
|
|
|
|
|
const changed = GroupSyncService.getLastChangedActive();
|
|
|
|
|
if (changed.length > 0) {
|
|
|
|
|
await GroupSyncService.syncMembersForGroups(changed);
|
|
|
|
|
} else {
|
|
|
|
|
await GroupSyncService.syncMembersForActiveGroups();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('❌ Error handling groups.upsert:', e);
|
|
|
|
|
}
|
|
|
|
|
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()
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
Metrics.inc('webhook_errors_total');
|
|
|
|
|
} catch {}
|
|
|
|
|
return new Response('Invalid request', { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async handleMessageUpsert(data: any) {
|
|
|
|
|
return await handleMessageUpsertFn(data, WebhookServer.dbInstance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)}...`
|
|
|
|
|
: 'NOT SET'
|
|
|
|
|
);
|
|
|
|
|
console.log(
|
|
|
|
|
'WHATSAPP_COMMUNITY_ID:',
|
|
|
|
|
process.env.WHATSAPP_COMMUNITY_ID
|
|
|
|
|
? '***'
|
|
|
|
|
: 'NOT SET (se mostrarán comunidades disponibles)'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
// Run database migrations (up-only) before starting services
|
|
|
|
|
await Migrator.migrateToLatest(this.dbInstance);
|
|
|
|
|
|
|
|
|
|
// Etapa 7: seed inicial de grupos permitidos desde ALLOWED_GROUPS (best-effort)
|
|
|
|
|
try {
|
|
|
|
|
AllowedGroups.seedFromEnv();
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
const PORT = process.env.PORT || '3007';
|
|
|
|
|
console.log('✅ Environment variables validated');
|
|
|
|
|
// A0: pre-crear contadores para que aparezcan en /metrics
|
|
|
|
|
try {
|
|
|
|
|
Metrics.inc('onboarding_prompts_sent_total', 0);
|
|
|
|
|
Metrics.inc('onboarding_prompts_skipped_total', 0);
|
|
|
|
|
Metrics.inc('onboarding_assign_failures_total', 0);
|
|
|
|
|
|
|
|
|
|
// Precalentar métricas de reacciones por emoji
|
|
|
|
|
for (const emoji of ['robot', 'warn', 'check', 'other']) {
|
|
|
|
|
Metrics.inc('reactions_enqueued_total', 0, { emoji });
|
|
|
|
|
Metrics.inc('reactions_sent_total', 0, { emoji });
|
|
|
|
|
Metrics.inc('reactions_failed_total', 0, { emoji });
|
|
|
|
|
}
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
try {
|
|
|
|
|
await startServices(this.dbInstance);
|
|
|
|
|
} 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: Request) => WebhookServer.handleRequest(request)
|
|
|
|
|
});
|
|
|
|
|
console.log(`Server running on port ${PORT}`);
|
|
|
|
|
return server;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|