b21fb0f159
Clona o satélite do mercado adaptado ao scoreodonto (ESM, sem PluginManager): - backend/newwhats/: config (ENV), auth (x-nw-key), routes /nw/* (clinica, especialidades, dentistas, procedimentos, planos — multi-tenant por clinica_id), sync-knowledge (base de conhecimento odonto → motor), webhook-receiver (HMAC, enxuto), proxy REST /api/nw/v1/* → motor /api/ext/v1/*, manifest, INTEGRACAO-MOTOR.md. - server.js: registerNewwhats(app, pool) antes do listen. - Removidos (não úteis): media-transcribe (motor já transcreve), smart-router, personas, follow-up-detector, context-builder. - Config por ENV: NEWWHATS_URL/INTEGRATION_KEY/WEBHOOK_SECRET/CLINICA_ID. - ativo::int=1 normaliza boolean vs smallint entre tabelas. - Validado: buildKnowledgeText gera 1195 chars de dados reais; registro ok no express 5.2.1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
53 lines
2.2 KiB
JavaScript
53 lines
2.2 KiB
JavaScript
// Receptor de webhooks do motor NewWhats (motor → satélite).
|
|
// POST /api/webhook/newwhats
|
|
// Header x-nw-signature: sha256=HMAC-SHA256(webhook_secret, rawBody)
|
|
// Eventos: message.new (nova mensagem WA), session.status (conexão da instância)
|
|
//
|
|
// Versão ENXUTA p/ scoreodonto: valida HMAC, registra o evento e deixa ganchos
|
|
// (TODO) para captura de lead / auto-resposta. NÃO porta o auto-responder do
|
|
// mercado (a Secretária do motor já responde via /secretaria/ask).
|
|
import { Router } from 'express'
|
|
import { createHmac, timingSafeEqual } from 'crypto'
|
|
import { getConfig } from './config.js'
|
|
|
|
function verifySignature(secret, rawBody, sigHeader) {
|
|
if (!sigHeader) return false
|
|
const expected = Buffer.from('sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'))
|
|
const received = Buffer.from(String(sigHeader))
|
|
return expected.length === received.length && timingSafeEqual(expected, received)
|
|
}
|
|
|
|
export function createWebhookReceiver(_pool) {
|
|
const router = Router()
|
|
|
|
router.post('/api/webhook/newwhats', async (req, res) => {
|
|
try {
|
|
const secret = getConfig().webhookSecret
|
|
if (secret) {
|
|
const rawBody = req.rawBody ?? Buffer.from(JSON.stringify(req.body))
|
|
if (!verifySignature(secret, rawBody, req.headers['x-nw-signature'])) {
|
|
return res.status(401).json({ error: 'Assinatura inválida' })
|
|
}
|
|
} else {
|
|
console.warn('[nw-webhook] NEWWHATS_WEBHOOK_SECRET ausente — assinatura ignorada')
|
|
}
|
|
|
|
const { event, data } = req.body || {}
|
|
if (event === 'message.new') {
|
|
// TODO(odonto): registrar lead / acionar fluxo. Hoje só loga.
|
|
console.log(`[nw-webhook] message.new de ${data?.author ?? data?.pushName ?? '?'}: ${String(data?.body ?? '').slice(0, 60)}`)
|
|
} else if (event === 'session.status') {
|
|
console.log(`[nw-webhook] session.status → ${data?.instanceId}: ${data?.status}`)
|
|
} else {
|
|
console.log(`[nw-webhook] evento não tratado: ${event}`)
|
|
}
|
|
res.json({ ok: true })
|
|
} catch (e) {
|
|
console.error('[nw-webhook] erro:', e.message)
|
|
res.status(500).json({ error: e.message })
|
|
}
|
|
})
|
|
|
|
return router
|
|
}
|