Bot: endpoint /debug para diagnosticar entrantes + estado de conexión
GET /debug (Basic auth, QR_TOKEN) devuelve el estado de la conexión de WhatsApp y un anillo de los últimos mensajes entrantes (remoteJid real, fromMe, tipo) y el resultado del matching lead↔teléfono. Para diagnosticar por qué el bot no responde (conexión zombi vs formato de jid @lid vs no llega el mensaje). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,9 @@ export class WebhookListener implements OnApplicationBootstrap {
|
|||||||
// Estado de vinculación de WhatsApp, alimentado por WhatsappService (sin dependencia circular).
|
// Estado de vinculación de WhatsApp, alimentado por WhatsappService (sin dependencia circular).
|
||||||
private qrActual: string | null = null;
|
private qrActual: string | null = null;
|
||||||
private conectado = false;
|
private conectado = false;
|
||||||
|
// Diagnóstico: estado de conexión + últimos eventos entrantes (anillo de 15).
|
||||||
|
private connState: Record<string, unknown> = {};
|
||||||
|
private inbound: any[] = [];
|
||||||
|
|
||||||
constructor(private readonly api: ApiClient) {}
|
constructor(private readonly api: ApiClient) {}
|
||||||
|
|
||||||
@@ -33,12 +36,20 @@ export class WebhookListener implements OnApplicationBootstrap {
|
|||||||
this.conectado = b;
|
this.conectado = b;
|
||||||
if (b) this.qrActual = null;
|
if (b) this.qrActual = null;
|
||||||
}
|
}
|
||||||
|
setConnState(o: Record<string, unknown>) {
|
||||||
|
this.connState = o;
|
||||||
|
}
|
||||||
|
pushInbound(o: Record<string, unknown>) {
|
||||||
|
this.inbound.unshift(o);
|
||||||
|
if (this.inbound.length > 15) this.inbound.pop();
|
||||||
|
}
|
||||||
|
|
||||||
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
|
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||||
const url = req.url || '';
|
const url = req.url || '';
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
if (url.startsWith('/qr')) return this.handleQrPage(req, res);
|
if (url.startsWith('/qr')) return this.handleQrPage(req, res);
|
||||||
|
if (url.startsWith('/debug')) return this.handleDebug(req, res);
|
||||||
res.writeHead(404).end('Not Found');
|
res.writeHead(404).end('Not Found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -84,15 +95,37 @@ export class WebhookListener implements OnApplicationBootstrap {
|
|||||||
return crypto.timingSafeEqual(ha, hb);
|
return crypto.timingSafeEqual(ha, hb);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Página de vinculación: muestra el QR de Baileys como imagen escaneable.
|
// Auth Basic: contraseña = QR_TOKEN (en cabecera, nunca en la URL).
|
||||||
// Auth por cabecera (HTTP Basic, contraseña = QR_TOKEN), nunca por query string.
|
private basicAuthOk(req: http.IncomingMessage): boolean {
|
||||||
private async handleQrPage(req: http.IncomingMessage, res: http.ServerResponse) {
|
|
||||||
const expected = process.env.QR_TOKEN || '';
|
const expected = process.env.QR_TOKEN || '';
|
||||||
const auth = (req.headers['authorization'] as string) || '';
|
const auth = (req.headers['authorization'] as string) || '';
|
||||||
const pass = auth.startsWith('Basic ')
|
const pass = auth.startsWith('Basic ')
|
||||||
? Buffer.from(auth.slice(6), 'base64').toString('utf8').split(':').slice(1).join(':')
|
? Buffer.from(auth.slice(6), 'base64').toString('utf8').split(':').slice(1).join(':')
|
||||||
: '';
|
: '';
|
||||||
if (!this.tokenValido(pass, expected)) {
|
return this.tokenValido(pass, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagnóstico: estado de conexión + últimos eventos entrantes. Misma auth que /qr.
|
||||||
|
private handleDebug(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||||
|
if (!this.basicAuthOk(req)) {
|
||||||
|
res
|
||||||
|
.writeHead(401, {
|
||||||
|
'WWW-Authenticate': 'Basic realm="Reformix QR", charset="UTF-8"',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Referrer-Policy': 'no-referrer',
|
||||||
|
})
|
||||||
|
.end(JSON.stringify({ ok: false, error: 'No autorizado' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res
|
||||||
|
.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' })
|
||||||
|
.end(JSON.stringify({ conectado: this.conectado, connState: this.connState, inbound: this.inbound }, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Página de vinculación: muestra el QR de Baileys como imagen escaneable.
|
||||||
|
// Auth por cabecera (HTTP Basic, contraseña = QR_TOKEN), nunca por query string.
|
||||||
|
private async handleQrPage(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||||
|
if (!this.basicAuthOk(req)) {
|
||||||
res
|
res
|
||||||
.writeHead(401, {
|
.writeHead(401, {
|
||||||
'WWW-Authenticate': 'Basic realm="Reformix QR", charset="UTF-8"',
|
'WWW-Authenticate': 'Basic realm="Reformix QR", charset="UTF-8"',
|
||||||
|
|||||||
@@ -196,6 +196,13 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
|
|||||||
this.sock.ev.on('connection.update', (update) => {
|
this.sock.ev.on('connection.update', (update) => {
|
||||||
const { connection, lastDisconnect, qr } = update;
|
const { connection, lastDisconnect, qr } = update;
|
||||||
|
|
||||||
|
this.webhookListener.setConnState({
|
||||||
|
connection: connection ?? null,
|
||||||
|
hasQr: !!qr,
|
||||||
|
lastDisconnect: (lastDisconnect?.error as Boom)?.output?.statusCode ?? null,
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
if (qr) {
|
if (qr) {
|
||||||
QRCode.generate(qr, { small: true });
|
QRCode.generate(qr, { small: true });
|
||||||
console.log('\n📲 Escanea este QR con WhatsApp (o abre la página /qr, protegida con QR_TOKEN)\n');
|
console.log('\n📲 Escanea este QR con WhatsApp (o abre la página /qr, protegida con QR_TOKEN)\n');
|
||||||
@@ -218,6 +225,12 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
|
|||||||
this.sock.ev.on('messages.upsert', async ({ messages, type }) => {
|
this.sock.ev.on('messages.upsert', async ({ messages, type }) => {
|
||||||
if (type !== 'notify') return;
|
if (type !== 'notify') return;
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
|
this.webhookListener.pushInbound({
|
||||||
|
remoteJid: msg.key.remoteJid ?? null,
|
||||||
|
fromMe: !!msg.key.fromMe,
|
||||||
|
msgType: msg.message ? Object.keys(msg.message)[0] : null,
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
});
|
||||||
if (msg.key.fromMe) continue;
|
if (msg.key.fromMe) continue;
|
||||||
if (!msg.key.remoteJid) continue;
|
if (!msg.key.remoteJid) continue;
|
||||||
if (msg.key.remoteJid.includes('@g.us')) continue;
|
if (msg.key.remoteJid.includes('@g.us')) continue;
|
||||||
@@ -274,6 +287,7 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.webhookListener.pushInbound({ stage: 'match', telefono, leadId: leadId ?? null, at: new Date().toISOString() });
|
||||||
if (!leadId) {
|
if (!leadId) {
|
||||||
this.logger.log(`Mensaje ignorado de ${telefono}: lead no registrado. Debe iniciarse desde la web.`);
|
this.logger.log(`Mensaje ignorado de ${telefono}: lead no registrado. Debe iniciarse desde la web.`);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user