From 9d082a17e4e07de6b8cc6b764ef50df010113605 Mon Sep 17 00:00:00 2001 From: VPS 4 Deploy Agent Date: Tue, 12 May 2026 03:10:50 +0200 Subject: [PATCH] fix(whatsapp): mensagens do celular perdidas para contatos @lid + crash P2002 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Três correções independentes para mensagens enviadas/recebidas via celular: 1. WhatsAppConnectionManager: o listener CB:message agora captura também recipient_pn (InfiniteAPI v7+) além de peer_recipient_pn (Baileys oficial), garantindo que o PN correto do destinatário seja gravado no peerRecipientHints antes do messages.upsert ser emitido. 2. MessageHandler.processMessage: adicionado fast path fromMe=false via remoteJidAlt para contatos @lid (InfiniteAPI já resolve o PN do remetente). Para fromMe=true onde InfiniteAPI resolveu @lid→nosso próprio JID incorretamente, recupera o JID correto via peerRecipientHints antes de descartar como self-chat. 3. MessageHandler.processHistoryBatch: corrigido guard senderPn que não testava !msg.key.fromMe; para mensagens enviadas, senderPn é o nosso próprio JID, não o destinatário — o que criava self-chats corrompidos no DB. 4. whatsapp.send.routes: adicionado .catch P2002 no upsert da rota POST /send; race condition com eco Baileys causava unhandled rejection → exit(1) no Node 20. Co-Authored-By: Claude Sonnet 4.6 --- .../connection/WhatsAppConnectionManager.ts | 31 +++++++----- .../whatsapp/handlers/MessageHandler.ts | 50 ++++++++++++++++--- .../modules/whatsapp/whatsapp.send.routes.ts | 14 +++++- 3 files changed, 75 insertions(+), 20 deletions(-) diff --git a/clube67/newwhats.local/backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.ts b/clube67/newwhats.local/backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.ts index cf56298..478616e 100644 --- a/clube67/newwhats.local/backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.ts +++ b/clube67/newwhats.local/backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.ts @@ -285,24 +285,29 @@ export class WhatsAppConnectionManager { const msgHandler = new MessageHandler(sock, instanceId, tenantId, this.io, this.chatbotService) this.msgHandlers.set(instanceId, msgHandler) - // ─── Intercepta stanzas brutas pra extrair peer_recipient_pn ───────── - // O Baileys (decode-wa-message.js) expõe só sender_pn/participant_pn no - // key — mas NÃO expõe peer_recipient_pn, que é a única fonte de verdade - // pro telefone real do DESTINATÁRIO em mensagens fromMe=true enviadas - // pelo celular pra contatos @lid novos (sem mapeamento salvo). + // ─── Intercepta stanzas brutas pra extrair PN real do destinatário ── + // Dois cenários distintos de mensagens do celular que chegam sem PN: // - // Sem essa dica, `resolveLidToPhoneJid` retornaria null e a mensagem - // seria descartada — o usuário relatou exatamente esse sintoma ("envio - // pelo celular da sessão pro número X e a msg não aparece na inbox"). + // Cenário A (Baileys oficial): mensagem fromMe=true com remoteJid=@lid + // O campo peer_recipient_pn na stanza bruta contém o PN do destinatário. + // Baileys não expõe esse campo em key, por isso lemos aqui. // - // O listener roda em paralelo com o handler interno do Baileys: ambos - // são callbacks síncronos do EventEmitter de ws, então o mapa é + // Cenário B (InfiniteAPI v7+): mensagem fromMe=true onde o InfiniteAPI + // resolve automaticamente @lid → nosso próprio JID (usando sender_pn + // da stanza, que para fromMe=true é NOSSO PN, não o do destinatário). + // O PN correto do destinatário está em recipient_pn na stanza. + // Sem essa dica, processMessage descarta a mensagem como self-chat. + // + // O listener é síncrono (mesmo EventEmitter do ws), então o mapa é // populado antes do `messages.upsert` ser emitido de forma assíncrona. ;(sock.ws as any).on('CB:message', (node: any) => { const msgId = node?.attrs?.id - const peerRecipientPn = node?.attrs?.peer_recipient_pn - if (msgId && peerRecipientPn) { - msgHandler.setPeerRecipientHint(msgId, peerRecipientPn) + if (!msgId) return + // peer_recipient_pn: Cenário A (@lid não resolvido, Baileys oficial) + // recipient_pn: Cenário B (InfiniteAPI resolveu @lid para nosso PN) + const hint = node?.attrs?.peer_recipient_pn ?? node?.attrs?.recipient_pn + if (hint) { + msgHandler.setPeerRecipientHint(msgId, hint) } }) diff --git a/clube67/newwhats.local/backend/src/modules/whatsapp/handlers/MessageHandler.ts b/clube67/newwhats.local/backend/src/modules/whatsapp/handlers/MessageHandler.ts index f747608..8fc436f 100644 --- a/clube67/newwhats.local/backend/src/modules/whatsapp/handlers/MessageHandler.ts +++ b/clube67/newwhats.local/backend/src/modules/whatsapp/handlers/MessageHandler.ts @@ -279,8 +279,12 @@ export class MessageHandler { if (remoteJidAlt && remoteJidAlt.endsWith('@s.whatsapp.net')) { jidRaw = normalizeJid(remoteJidAlt) } else { - // 2. senderPn / participantPn - const senderPn = (msg.key as any).senderPn || (msg.key as any).participantPn + // 2. senderPn / participantPn — SOMENTE para mensagens recebidas (fromMe=false). + // Para fromMe=true, senderPn é o NOSSO próprio JID (somos o remetente), + // não o JID do destinatário — usar aqui criaria um self-chat corrompido no DB. + const senderPn = !msg.key.fromMe + ? ((msg.key as any).senderPn || (msg.key as any).participantPn) + : null if (senderPn) { jidRaw = normalizeJid(senderPn) if (!jidRaw.endsWith('@s.whatsapp.net')) continue @@ -452,7 +456,17 @@ export class MessageHandler { */ let originalLidJid: string | null = null if (key.remoteJid.endsWith('@lid')) { - const phoneJid = await this.resolveLidToPhoneJid(key.remoteJid, key as any) + // Fast path para fromMe=false: InfiniteAPI preenche remoteJidAlt com o PN + // do remetente (que é o contato) quando addressing_mode=pn. Para fromMe=true, + // remoteJidAlt contem o NOSSO próprio PN (senderAlt = sender_pn = nosso número) + // — por isso o fast path só é seguro para mensagens recebidas. + const remoteJidAlt = (key as any).remoteJidAlt as string | undefined + let phoneJid: string | null = null + if (!key.fromMe && remoteJidAlt && remoteJidAlt.endsWith('@s.whatsapp.net')) { + phoneJid = normalizeJid(remoteJidAlt) + } else { + phoneJid = await this.resolveLidToPhoneJid(key.remoteJid, key as any) + } if (!phoneJid) { // Sem resolução: não há como saber o número real do contato. // A mensagem será perdida, mas o mapeamento será capturado no futuro @@ -480,12 +494,36 @@ export class MessageHandler { const fromMe = key.fromMe ?? false const remoteJidRaw = key.remoteJid /** JID normalizado (sem sufixo de dispositivo :1, :2 etc) */ - const remoteJid = normalizeJid(remoteJidRaw) + let remoteJid = normalizeJid(remoteJidRaw) const messageId = key.id! - // Ignora mensagens de/para o próprio número da sessão (self-chat / notas pessoais) + // Ignora mensagens de/para o próprio número da sessão (self-chat / notas pessoais). + // + // Caso especial (InfiniteAPI v7+): para mensagens fromMe=true enviadas do celular + // a um contato LID, o InfiniteAPI pode ter resolvido remoteJid para o nosso próprio + // JID (usando sender_pn = nosso PN em vez de recipient_pn = PN do destinatário). + // Nesse caso, o CB:message listener armazenou recipient_pn em peerRecipientHints. + // Tentamos recuperar o JID correto antes de descartar como self-chat. const ownJid = normalizeJid(this.sock.user?.id ?? '') - if (ownJid && remoteJid === ownJid) return + if (ownJid && remoteJid === ownJid) { + if (fromMe) { + const hint = this.peerRecipientHints.get(messageId) + if (hint) { + this.peerRecipientHints.delete(messageId) + const correctJid = normalizeJid(hint) + if (correctJid && correctJid.endsWith('@s.whatsapp.net') && correctJid !== ownJid) { + remoteJid = correctJid + ;(key as any).remoteJid = correctJid + } else { + return + } + } else { + return + } + } else { + return + } + } // ── 1. Contact: criar ou atualizar ──────────────────────────────────── // REGRA IMPORTANTE sobre pushName: diff --git a/clube67/newwhats.local/backend/src/modules/whatsapp/whatsapp.send.routes.ts b/clube67/newwhats.local/backend/src/modules/whatsapp/whatsapp.send.routes.ts index f3e9f4c..cde9598 100644 --- a/clube67/newwhats.local/backend/src/modules/whatsapp/whatsapp.send.routes.ts +++ b/clube67/newwhats.local/backend/src/modules/whatsapp/whatsapp.send.routes.ts @@ -223,7 +223,10 @@ export function buildSendRoutes(manager: WhatsAppConnectionManager): Router { richPayloadData = { text, footer, nativeCarousel } } - const msg = await prisma.message.upsert({ + // Race condition: o eco do Baileys pode chegar ANTES desta linha e criar o + // registro primeiro, fazendo o upsert falhar com P2002 (unique constraint). + // Nesses casos, buscamos a mensagem que já foi criada pelo eco. + let msg = await prisma.message.upsert({ where: { instanceId_messageId: { instanceId, messageId } }, create: { tenantId, @@ -239,6 +242,15 @@ export function buildSendRoutes(manager: WhatsAppConnectionManager): Router { timestamp: new Date(), }, update: {}, // já existe (eco do Baileys chegou primeiro) — não sobrescreve + }).catch(async (err: any) => { + if (err?.code === 'P2002') { + // Eco do Baileys criou o registro antes de nós — busca e retorna ele + const existing = await prisma.message.findUnique({ + where: { instanceId_messageId: { instanceId, messageId } }, + }) + if (existing) return existing + } + throw err }) // ── Emite eventos Socket.IO para o frontend atualizar em tempo real ───────