fix(whatsapp): mensagens do celular perdidas para contatos @lid + crash P2002
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 <noreply@anthropic.com>
This commit is contained in:
+18
-13
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ───────
|
||||
|
||||
Reference in New Issue
Block a user