fix(whatsapp): mensagens do celular perdidas para contatos @lid + crash P2002
continuous-integration/webhook Deploy concluido com sucesso (VPS 4)
continuous-integration/webhook Deploy concluido com sucesso (VPS 4)
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)
|
const msgHandler = new MessageHandler(sock, instanceId, tenantId, this.io, this.chatbotService)
|
||||||
this.msgHandlers.set(instanceId, msgHandler)
|
this.msgHandlers.set(instanceId, msgHandler)
|
||||||
|
|
||||||
// ─── Intercepta stanzas brutas pra extrair peer_recipient_pn ─────────
|
// ─── Intercepta stanzas brutas pra extrair PN real do destinatário ──
|
||||||
// O Baileys (decode-wa-message.js) expõe só sender_pn/participant_pn no
|
// Dois cenários distintos de mensagens do celular que chegam sem PN:
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Sem essa dica, `resolveLidToPhoneJid` retornaria null e a mensagem
|
// Cenário A (Baileys oficial): mensagem fromMe=true com remoteJid=@lid
|
||||||
// seria descartada — o usuário relatou exatamente esse sintoma ("envio
|
// O campo peer_recipient_pn na stanza bruta contém o PN do destinatário.
|
||||||
// pelo celular da sessão pro número X e a msg não aparece na inbox").
|
// 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
|
// Cenário B (InfiniteAPI v7+): mensagem fromMe=true onde o InfiniteAPI
|
||||||
// são callbacks síncronos do EventEmitter de ws, então o mapa é
|
// 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.
|
// populado antes do `messages.upsert` ser emitido de forma assíncrona.
|
||||||
;(sock.ws as any).on('CB:message', (node: any) => {
|
;(sock.ws as any).on('CB:message', (node: any) => {
|
||||||
const msgId = node?.attrs?.id
|
const msgId = node?.attrs?.id
|
||||||
const peerRecipientPn = node?.attrs?.peer_recipient_pn
|
if (!msgId) return
|
||||||
if (msgId && peerRecipientPn) {
|
// peer_recipient_pn: Cenário A (@lid não resolvido, Baileys oficial)
|
||||||
msgHandler.setPeerRecipientHint(msgId, peerRecipientPn)
|
// 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')) {
|
if (remoteJidAlt && remoteJidAlt.endsWith('@s.whatsapp.net')) {
|
||||||
jidRaw = normalizeJid(remoteJidAlt)
|
jidRaw = normalizeJid(remoteJidAlt)
|
||||||
} else {
|
} else {
|
||||||
// 2. senderPn / participantPn
|
// 2. senderPn / participantPn — SOMENTE para mensagens recebidas (fromMe=false).
|
||||||
const senderPn = (msg.key as any).senderPn || (msg.key as any).participantPn
|
// 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) {
|
if (senderPn) {
|
||||||
jidRaw = normalizeJid(senderPn)
|
jidRaw = normalizeJid(senderPn)
|
||||||
if (!jidRaw.endsWith('@s.whatsapp.net')) continue
|
if (!jidRaw.endsWith('@s.whatsapp.net')) continue
|
||||||
@@ -452,7 +456,17 @@ export class MessageHandler {
|
|||||||
*/
|
*/
|
||||||
let originalLidJid: string | null = null
|
let originalLidJid: string | null = null
|
||||||
if (key.remoteJid.endsWith('@lid')) {
|
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) {
|
if (!phoneJid) {
|
||||||
// Sem resolução: não há como saber o número real do contato.
|
// 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
|
// A mensagem será perdida, mas o mapeamento será capturado no futuro
|
||||||
@@ -480,12 +494,36 @@ export class MessageHandler {
|
|||||||
const fromMe = key.fromMe ?? false
|
const fromMe = key.fromMe ?? false
|
||||||
const remoteJidRaw = key.remoteJid
|
const remoteJidRaw = key.remoteJid
|
||||||
/** JID normalizado (sem sufixo de dispositivo :1, :2 etc) */
|
/** JID normalizado (sem sufixo de dispositivo :1, :2 etc) */
|
||||||
const remoteJid = normalizeJid(remoteJidRaw)
|
let remoteJid = normalizeJid(remoteJidRaw)
|
||||||
const messageId = key.id!
|
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 ?? '')
|
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 ────────────────────────────────────
|
// ── 1. Contact: criar ou atualizar ────────────────────────────────────
|
||||||
// REGRA IMPORTANTE sobre pushName:
|
// REGRA IMPORTANTE sobre pushName:
|
||||||
|
|||||||
@@ -223,7 +223,10 @@ export function buildSendRoutes(manager: WhatsAppConnectionManager): Router {
|
|||||||
richPayloadData = { text, footer, nativeCarousel }
|
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 } },
|
where: { instanceId_messageId: { instanceId, messageId } },
|
||||||
create: {
|
create: {
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -239,6 +242,15 @@ export function buildSendRoutes(manager: WhatsAppConnectionManager): Router {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
},
|
},
|
||||||
update: {}, // já existe (eco do Baileys chegou primeiro) — não sobrescreve
|
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 ───────
|
// ── Emite eventos Socket.IO para o frontend atualizar em tempo real ───────
|
||||||
|
|||||||
Reference in New Issue
Block a user