feat: WhatsApp Nucleo con Nuxt 4 + Baileys v7
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 6m46s
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 6m46s
Reemplazo completo de Evolution API por implementación directa con Baileys. Características: - Dashboard completo con Nuxt UI v4 - Soporte para múltiples instancias de WhatsApp - Conexión via QR code o pairing code - Persistencia de mensajes en PostgreSQL - API REST para integraciones externas - Webhooks con firma HMAC - SSE para actualizaciones en tiempo real - Autenticación con Authentik
This commit is contained in:
171
server/services/webhooks/dispatcher.ts
Normal file
171
server/services/webhooks/dispatcher.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Webhook Dispatcher
|
||||
* Sends events to configured webhooks
|
||||
*/
|
||||
import crypto from 'crypto'
|
||||
import { query } from '../../utils/database'
|
||||
import { baileysManager } from '../baileys/manager'
|
||||
|
||||
interface Webhook {
|
||||
id: string
|
||||
url: string
|
||||
secret: string | null
|
||||
events: string[]
|
||||
headers: Record<string, string>
|
||||
retry_count: number
|
||||
timeout_ms: number
|
||||
instance_id: string | null
|
||||
}
|
||||
|
||||
class WebhookDispatcher {
|
||||
private initialized = false
|
||||
|
||||
async initialize() {
|
||||
if (this.initialized) return
|
||||
|
||||
// Listen to all Baileys events
|
||||
const events = [
|
||||
'message.received',
|
||||
'message.sent',
|
||||
'message.status',
|
||||
'instance.connected',
|
||||
'instance.disconnected',
|
||||
'instance.status',
|
||||
'instance.qr'
|
||||
]
|
||||
|
||||
for (const eventType of events) {
|
||||
baileysManager.on(eventType, (data: any) => {
|
||||
this.dispatch(data.instanceId || null, eventType, data)
|
||||
})
|
||||
}
|
||||
|
||||
this.initialized = true
|
||||
console.log('[WebhookDispatcher] Initialized')
|
||||
}
|
||||
|
||||
async dispatch(instanceId: string | null, eventType: string, payload: any) {
|
||||
try {
|
||||
// Get matching webhooks
|
||||
const webhooks = await query<Webhook>(
|
||||
`SELECT id, url, secret, events, headers, retry_count, timeout_ms, instance_id
|
||||
FROM webhooks
|
||||
WHERE is_active = TRUE
|
||||
AND $1 = ANY(events)
|
||||
AND (instance_id IS NULL OR instance_id = $2)`,
|
||||
[eventType, instanceId]
|
||||
)
|
||||
|
||||
for (const webhook of webhooks.rows) {
|
||||
this.deliverWithRetry(webhook, eventType, payload)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WebhookDispatcher] Error dispatching:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private async deliverWithRetry(
|
||||
webhook: Webhook,
|
||||
eventType: string,
|
||||
payload: any,
|
||||
attempt = 1
|
||||
) {
|
||||
try {
|
||||
await this.deliver(webhook, eventType, payload)
|
||||
|
||||
// Log success
|
||||
await this.logDelivery(webhook.id, eventType, payload, {
|
||||
status: 200,
|
||||
attempt,
|
||||
success: true
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message
|
||||
|
||||
// Log failure
|
||||
await this.logDelivery(webhook.id, eventType, payload, {
|
||||
status: 0,
|
||||
attempt,
|
||||
success: false,
|
||||
error: errorMessage
|
||||
})
|
||||
|
||||
// Retry if under limit
|
||||
if (attempt < webhook.retry_count) {
|
||||
const delay = Math.pow(2, attempt) * 1000 // Exponential backoff
|
||||
setTimeout(() => {
|
||||
this.deliverWithRetry(webhook, eventType, payload, attempt + 1)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async deliver(webhook: Webhook, eventType: string, payload: any) {
|
||||
const body = JSON.stringify({
|
||||
event: eventType,
|
||||
timestamp: new Date().toISOString(),
|
||||
data: payload
|
||||
})
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Webhook-Event': eventType,
|
||||
'X-Webhook-Timestamp': Date.now().toString(),
|
||||
...(webhook.headers || {})
|
||||
}
|
||||
|
||||
// Add HMAC signature if secret is configured
|
||||
if (webhook.secret) {
|
||||
const signature = crypto
|
||||
.createHmac('sha256', webhook.secret)
|
||||
.update(body)
|
||||
.digest('hex')
|
||||
headers['X-Webhook-Signature'] = `sha256=${signature}`
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), webhook.timeout_ms)
|
||||
|
||||
try {
|
||||
const response = await fetch(webhook.url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
private async logDelivery(
|
||||
webhookId: string,
|
||||
eventType: string,
|
||||
payload: any,
|
||||
result: { status: number; attempt: number; success: boolean; error?: string }
|
||||
) {
|
||||
try {
|
||||
await query(
|
||||
`INSERT INTO webhook_logs (webhook_id, event_type, payload, response_status, error_message, attempt, delivered_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
webhookId,
|
||||
eventType,
|
||||
JSON.stringify(payload),
|
||||
result.status,
|
||||
result.error || null,
|
||||
result.attempt,
|
||||
result.success ? new Date() : null
|
||||
]
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('[WebhookDispatcher] Error logging delivery:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const webhookDispatcher = new WebhookDispatcher()
|
||||
Reference in New Issue
Block a user