From adad2ffee51487bbf412b42a1a3bee5d9de069d9 Mon Sep 17 00:00:00 2001 From: fidelis05 <100060822+fidelis05@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:15:28 -0300 Subject: [PATCH 1/2] fix(baileys): resolve @lid to phone JID across history sync and live messages WhatsApp increasingly addresses events with LID identifiers (@lid) instead of the phone-based JID. Business accounts hit this almost exclusively, which left those instances with no usable chat history and messages that never linked to their chats. Two distinct problems are fixed: 1. messages.upsert resolved @lid only *after* the message row was written, so the Message row, the Chat lookup and the Contact upsert were all keyed by @lid while the swap only patched the outgoing webhook payload. Resolution now happens before any lookup or write. received.key itself is left untouched so protocol calls (readMessages, requestPlaceholderResend, fetchMessageHistory, media download) keep addressing the message exactly as WhatsApp delivered it. 2. messaging-history.set never resolved @lid on messages at all, and its chat resolution only consulted contact.phoneNumber - which history payloads frequently omit. History-sync message keys carry no remoteJidAlt or addressingMode, so resolution now goes through Baileys' signal-level LID store (signalRepository.lidMapping), batched once per payload and shared by chats, contacts and messages. The lookup is guarded and degrades to previous behaviour when the store is unavailable. Measured on a Business-linked instance (1101 messages, 91 chats): @lid messages 1035 -> 0 @lid chats 29 -> 2 (2 have no mapping, name or messages) chats with >1 message 3 -> 47 Verified for live messages.upsert as well: LID-addressed messages in both directions, including media, resolve to the phone JID and land in the existing chat rather than creating a parallel @lid one. Also includes patches/baileys+7.0.0-rc13.patch, fixing two upstream bugs that made media on those messages unrecoverable: - downloadMediaMessage tested error?.status, but getHttpStream throws a Boom carrying the status on output.statusCode, so the re-upload request was never sent. - getMediaRetryKey did not normalise a base64-string mediaKey the way getMediaKeys does, so the retry request was encrypted with a key derived from the base64 characters and WhatsApp replied DECRYPTION_ERROR. With both applied, media within WhatsApp's ~30 day retention window downloads again (verified 10/10, output validated as real Ogg/Opus). Co-Authored-By: Claude Opus 5 --- patches/baileys+7.0.0-rc13.patch | 35 +++++ .../whatsapp/whatsapp.baileys.service.ts | 143 +++++++++++++++--- 2 files changed, 158 insertions(+), 20 deletions(-) create mode 100644 patches/baileys+7.0.0-rc13.patch diff --git a/patches/baileys+7.0.0-rc13.patch b/patches/baileys+7.0.0-rc13.patch new file mode 100644 index 0000000000..cfd18972e7 --- /dev/null +++ b/patches/baileys+7.0.0-rc13.patch @@ -0,0 +1,35 @@ +diff --git a/node_modules/baileys/lib/Utils/messages-media.js b/node_modules/baileys/lib/Utils/messages-media.js +index a3d2fa6..13e763c 100644 +--- a/node_modules/baileys/lib/Utils/messages-media.js ++++ b/node_modules/baileys/lib/Utils/messages-media.js +@@ -699,6 +699,9 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, opt + }; + }; + const getMediaRetryKey = (mediaKey) => { ++ if (typeof mediaKey === 'string') { ++ mediaKey = Buffer.from(mediaKey.replace('data:;base64,', ''), 'base64'); ++ } + return hkdf(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' }); + }; + /** +diff --git a/node_modules/baileys/lib/Utils/messages.js b/node_modules/baileys/lib/Utils/messages.js +index 247b1f1..dfa8471 100644 +--- a/node_modules/baileys/lib/Utils/messages.js ++++ b/node_modules/baileys/lib/Utils/messages.js +@@ -832,9 +832,14 @@ const REUPLOAD_REQUIRED_STATUS = [410, 404]; + */ + export const downloadMediaMessage = async (message, type, options, ctx) => { + const result = await downloadMsg().catch(async (error) => { ++ const errorStatus = typeof error?.status === 'number' ++ ? error.status ++ : typeof error?.output?.statusCode === 'number' ++ ? error.output.statusCode ++ : undefined; + if (ctx && +- typeof error?.status === 'number' && // treat errors with status as HTTP failures requiring reupload +- REUPLOAD_REQUIRED_STATUS.includes(error.status)) { ++ typeof errorStatus === 'number' && // treat errors with status as HTTP failures requiring reupload ++ REUPLOAD_REQUIRED_STATUS.includes(errorStatus)) { + ctx.logger.info({ key: message.key }, 'sending reupload media request...'); + // request reupload + message = await ctx.reuploadRequest(message); diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 22839fd451..42c2567319 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -270,6 +270,7 @@ export class BaileysStartupService extends ChannelStartupService { private historySyncChatCount = 0; private historySyncContactCount = 0; private historySyncLastProgress = -1; + private readonly historySyncLidToJidMap = new Map(); // Cache TTL constants (in seconds) private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing @@ -1039,6 +1040,36 @@ export class BaileysStartupService extends ChannelStartupService { }, }; + private async resolveLidsIntoHistoryMap(jids: (string | null | undefined)[]) { + const lidStore = (this.client as any)?.signalRepository?.lidMapping; + if (!lidStore?.getPNsForLIDs) return; + + const unresolved = new Set(); + for (const jid of jids) { + if (jid?.endsWith('@lid') && !this.historySyncLidToJidMap.has(jid)) { + unresolved.add(jid); + } + } + + if (!unresolved.size) return; + + try { + const mappings = await lidStore.getPNsForLIDs([...unresolved]); + let resolved = 0; + for (const mapping of mappings ?? []) { + const { lid, pn } = mapping ?? {}; + const normalizedPn = pn ? jidNormalizedUser(pn) : null; + if (lid?.endsWith('@lid') && normalizedPn && !normalizedPn.endsWith('@lid')) { + this.historySyncLidToJidMap.set(lid, normalizedPn); + resolved += 1; + } + } + this.logger.verbose(`[historySync] LID store resolved ${resolved}/${unresolved.size} @lid jids`); + } catch (error) { + this.logger.warn(`[historySync] LID store lookup failed: ${error?.message}`); + } + } + private readonly messageHandle = { 'messaging-history.set': async ({ messages, @@ -1062,6 +1093,7 @@ export class BaileysStartupService extends ChannelStartupService { this.historySyncMessageCount = 0; this.historySyncChatCount = 0; this.historySyncContactCount = 0; + this.historySyncLidToJidMap.clear(); } this.historySyncLastProgress = normalizedProgress; @@ -1092,6 +1124,12 @@ export class BaileysStartupService extends ChannelStartupService { } } + await this.resolveLidsIntoHistoryMap([ + ...chats.map((c) => c?.id), + ...contacts.map((c) => c?.id), + ...messages.flatMap((m) => [m?.key?.remoteJid, m?.key?.participant]), + ]); + const contactsMap = new Map(); const contactsMapLidJid = new Map(); @@ -1101,6 +1139,8 @@ export class BaileysStartupService extends ChannelStartupService { if (contact?.id?.search('@lid') !== -1) { if (contact.phoneNumber) { jid = contact.phoneNumber; + } else { + jid = this.historySyncLidToJidMap.get(contact.id) ?? null; } } @@ -1113,6 +1153,10 @@ export class BaileysStartupService extends ChannelStartupService { } contactsMapLidJid.set(contact.id, { jid }); + + if (jid && jid !== contact.id && !jid.includes('@lid')) { + this.historySyncLidToJidMap.set(contact.id, jid); + } } const chatsRaw: { remoteJid: string; remoteLid: string; instanceId: string; name?: string }[] = []; @@ -1135,8 +1179,10 @@ export class BaileysStartupService extends ChannelStartupService { remoteLid = chat.id; - if (contact && contact.jid) { + if (contact?.jid && !contact.jid.includes('@lid')) { remoteJid = contact.jid; + } else { + remoteJid = this.historySyncLidToJidMap.get(chat.id) ?? null; } } @@ -1151,6 +1197,12 @@ export class BaileysStartupService extends ChannelStartupService { chatsRaw.push({ remoteJid, remoteLid, instanceId: this.instanceId, name: chat.name }); } + for (const chat of chatsRaw) { + if (chat.remoteLid && chat.remoteJid && chat.remoteLid !== chat.remoteJid) { + this.historySyncLidToJidMap.set(chat.remoteLid, chat.remoteJid); + } + } + if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { const chatsToCreateMany = JSON.parse(JSON.stringify(chatsRaw)).map((chat) => { delete chat.remoteLid; @@ -1193,6 +1245,28 @@ export class BaileysStartupService extends ChannelStartupService { m.messageTimestamp = m.messageTimestamp?.toNumber(); } + const mKey = m.key as ExtendedIMessageKey; + if (mKey.remoteJid?.includes('@lid')) { + const resolvedJid = mKey.remoteJidAlt || this.historySyncLidToJidMap.get(mKey.remoteJid); + if (resolvedJid && !resolvedJid.includes('@lid')) { + const lid = mKey.remoteJid; + mKey.remoteJid = resolvedJid; + mKey.remoteJidAlt = lid; + this.historySyncLidToJidMap.set(lid, resolvedJid); + } + } + if (mKey.participant?.includes('@lid')) { + const resolvedParticipant = + mKey.participantAlt || + contactsMapLidJid.get(mKey.participant)?.jid || + this.historySyncLidToJidMap.get(mKey.participant); + if (resolvedParticipant && !resolvedParticipant.includes('@lid')) { + const lidParticipant = mKey.participant; + mKey.participant = resolvedParticipant; + mKey.participantAlt = lidParticipant; + } + } + if (this.configService.get('CHATWOOT').ENABLED) { if (m.messageTimestamp <= timestampLimitToImport) { continue; @@ -1205,8 +1279,13 @@ export class BaileysStartupService extends ChannelStartupService { if (!m.pushName && !m.key.fromMe) { const participantJid = m.participant || m.key.participant || m.key.remoteJid; - if (participantJid && contactsMap.has(participantJid)) { - m.pushName = contactsMap.get(participantJid).name; + const participantLid = mKey.participantAlt || mKey.remoteJidAlt; + const contactMatch = + (participantJid && contactsMap.get(participantJid)) || + (participantLid && contactsMap.get(participantLid)); + + if (contactMatch) { + m.pushName = contactMatch.name; } else if (participantJid) { m.pushName = participantJid.split('@')[0]; } @@ -1363,8 +1442,25 @@ export class BaileysStartupService extends ChannelStartupService { continue; } + const rawKey = received.key as ExtendedIMessageKey; + let resolvedRemoteJid = rawKey.remoteJid; + let resolvedRemoteJidAlt = rawKey.remoteJidAlt; + let resolvedParticipant = rawKey.participant; + let resolvedParticipantAlt = rawKey.participantAlt; + let resolvedAddressingMode = (rawKey as any).addressingMode; + + if (resolvedRemoteJid?.includes('@lid') && resolvedRemoteJidAlt) { + resolvedRemoteJid = rawKey.remoteJidAlt; + resolvedRemoteJidAlt = rawKey.remoteJid; + resolvedAddressingMode = 'pn'; + } + if (resolvedParticipant?.includes('@lid') && resolvedParticipantAlt) { + resolvedParticipant = rawKey.participantAlt; + resolvedParticipantAlt = rawKey.participant; + } + const existingChat = await this.prismaRepository.chat.findFirst({ - where: { instanceId: this.instanceId, remoteJid: received.key.remoteJid }, + where: { instanceId: this.instanceId, remoteJid: resolvedRemoteJid }, select: { id: true, name: true }, }); @@ -1374,7 +1470,7 @@ export class BaileysStartupService extends ChannelStartupService { existingChat.name !== received.pushName && received.pushName.trim().length > 0 && !received.key.fromMe && - !received.key.remoteJid.includes('@g.us') + !resolvedRemoteJid.includes('@g.us') ) { this.sendDataWebhook(Events.CHATS_UPSERT, [{ ...existingChat, name: received.pushName }]); if (this.configService.get('DATABASE').SAVE_DATA.CHATS) { @@ -1384,12 +1480,27 @@ export class BaileysStartupService extends ChannelStartupService { data: { name: received.pushName }, }); } catch { - console.log(`Chat insert record ignored: ${received.key.remoteJid} - ${this.instanceId}`); + console.log(`Chat insert record ignored: ${resolvedRemoteJid} - ${this.instanceId}`); } } } - const messageRaw = this.prepareMessage(received) as any; + const messageForPersist = + resolvedRemoteJid !== rawKey.remoteJid || resolvedParticipant !== rawKey.participant + ? { + ...received, + key: { + ...received.key, + remoteJid: resolvedRemoteJid, + remoteJidAlt: resolvedRemoteJidAlt, + participant: resolvedParticipant, + participantAlt: resolvedParticipantAlt, + addressingMode: resolvedAddressingMode, + }, + } + : received; + + const messageRaw = this.prepareMessage(messageForPersist) as any; if (messageRaw.messageType === 'pollUpdateMessage') { const pollCreationKey = (messageRaw.message as any).pollUpdateMessage.pollCreationMessageKey; @@ -1546,7 +1657,7 @@ export class BaileysStartupService extends ChannelStartupService { const { pollUpdates, ...messageData } = messageRaw as any; const msg = await this.prismaRepository.message.create({ data: messageData }); - const { remoteJid } = received.key; + const remoteJid = resolvedRemoteJid; const timestamp = msg.messageTimestamp; const fromMe = received.key.fromMe.toString(); const messageKey = `${remoteJid}_${timestamp}_${fromMe}`; @@ -1601,7 +1712,7 @@ export class BaileysStartupService extends ChannelStartupService { const mimetype = mimeTypes.lookup(fileName).toString(); const fullName = join( `${this.instance.id}`, - received.key.remoteJid, + resolvedRemoteJid, mediaType, `${Date.now()}_${fileName}`, ); @@ -1665,14 +1776,6 @@ export class BaileysStartupService extends ChannelStartupService { sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`); - if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) { - const lid = messageRaw.key.remoteJid; - - messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt; - messageRaw.key.remoteJidAlt = lid; - - messageRaw.key.addressingMode = 'pn'; - } console.log(messageRaw); this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); @@ -1685,7 +1788,7 @@ export class BaileysStartupService extends ChannelStartupService { }); const contact = await this.prismaRepository.contact.findFirst({ - where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId }, + where: { remoteJid: resolvedRemoteJid, instanceId: this.instanceId }, }); const contactRaw: { @@ -1694,9 +1797,9 @@ export class BaileysStartupService extends ChannelStartupService { profilePicUrl?: string; instanceId: string; } = { - remoteJid: received.key.remoteJid, + remoteJid: resolvedRemoteJid, pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName, - profilePicUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + profilePicUrl: (await this.profilePicture(resolvedRemoteJid)).profilePictureUrl, instanceId: this.instanceId, }; From 06e1b7fe88ccda034ff19d8c7848abba10033205 Mon Sep 17 00:00:00 2001 From: fidelis05 <100060822+fidelis05@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:53:27 -0300 Subject: [PATCH 2/2] refactor(baileys): standardise @lid checks on endsWith Addresses review feedback on #2663: the added LID handling mixed includes('@lid') and endsWith('@lid'). endsWith is the stricter and correct check for a JID suffix. --- .../channel/whatsapp/whatsapp.baileys.service.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 42c2567319..4f6476fba0 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1154,7 +1154,7 @@ export class BaileysStartupService extends ChannelStartupService { contactsMapLidJid.set(contact.id, { jid }); - if (jid && jid !== contact.id && !jid.includes('@lid')) { + if (jid && jid !== contact.id && !jid.endsWith('@lid')) { this.historySyncLidToJidMap.set(contact.id, jid); } } @@ -1179,7 +1179,7 @@ export class BaileysStartupService extends ChannelStartupService { remoteLid = chat.id; - if (contact?.jid && !contact.jid.includes('@lid')) { + if (contact?.jid && !contact.jid.endsWith('@lid')) { remoteJid = contact.jid; } else { remoteJid = this.historySyncLidToJidMap.get(chat.id) ?? null; @@ -1246,21 +1246,21 @@ export class BaileysStartupService extends ChannelStartupService { } const mKey = m.key as ExtendedIMessageKey; - if (mKey.remoteJid?.includes('@lid')) { + if (mKey.remoteJid?.endsWith('@lid')) { const resolvedJid = mKey.remoteJidAlt || this.historySyncLidToJidMap.get(mKey.remoteJid); - if (resolvedJid && !resolvedJid.includes('@lid')) { + if (resolvedJid && !resolvedJid.endsWith('@lid')) { const lid = mKey.remoteJid; mKey.remoteJid = resolvedJid; mKey.remoteJidAlt = lid; this.historySyncLidToJidMap.set(lid, resolvedJid); } } - if (mKey.participant?.includes('@lid')) { + if (mKey.participant?.endsWith('@lid')) { const resolvedParticipant = mKey.participantAlt || contactsMapLidJid.get(mKey.participant)?.jid || this.historySyncLidToJidMap.get(mKey.participant); - if (resolvedParticipant && !resolvedParticipant.includes('@lid')) { + if (resolvedParticipant && !resolvedParticipant.endsWith('@lid')) { const lidParticipant = mKey.participant; mKey.participant = resolvedParticipant; mKey.participantAlt = lidParticipant; @@ -1449,12 +1449,12 @@ export class BaileysStartupService extends ChannelStartupService { let resolvedParticipantAlt = rawKey.participantAlt; let resolvedAddressingMode = (rawKey as any).addressingMode; - if (resolvedRemoteJid?.includes('@lid') && resolvedRemoteJidAlt) { + if (resolvedRemoteJid?.endsWith('@lid') && resolvedRemoteJidAlt) { resolvedRemoteJid = rawKey.remoteJidAlt; resolvedRemoteJidAlt = rawKey.remoteJid; resolvedAddressingMode = 'pn'; } - if (resolvedParticipant?.includes('@lid') && resolvedParticipantAlt) { + if (resolvedParticipant?.endsWith('@lid') && resolvedParticipantAlt) { resolvedParticipant = rawKey.participantAlt; resolvedParticipantAlt = rawKey.participant; }