fix: extract LID-PN mappings from conversation objects in history sync (#2282)

* fix: extract LID-PN mappings from conversation objects in history sync

* fix: extract PN from userReceipt when pnJid is missing for LID chats
This commit is contained in:
João Lucas de Oliveira Lopes
2026-01-22 10:00:41 -03:00
committed by GitHub
parent 52fcad2b9c
commit f829b6d7a8
2 changed files with 302 additions and 0 deletions
+34
View File
@@ -3,6 +3,7 @@ import { inflate } from 'zlib'
import { proto } from '../../WAProto/index.js'
import type { Chat, Contact, LIDMapping, WAMessage } from '../Types'
import { WAMessageStubType } from '../Types'
import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser } from '../WABinary'
import { toNumber } from './generics'
import type { ILogger } from './logger.js'
import { normalizeMessageContent } from './messages'
@@ -10,6 +11,24 @@ import { downloadContentFromMessage } from './messages-media'
const inflatePromise = promisify(inflate)
const extractPnFromMessages = (messages: proto.IHistorySyncMsg[]): string | undefined => {
for (const msgItem of messages) {
const message = msgItem.message
// Only extract from outgoing messages (fromMe: true) in 1:1 chats
// because userReceipt.userJid is the recipient's JID
if (!message?.key?.fromMe || !message.userReceipt?.length) {
continue
}
const userJid = message.userReceipt[0]?.userJid
if (userJid && (isPnUser(userJid) || isHostedPnUser(userJid))) {
return userJid
}
}
return undefined
}
export const downloadHistory = async (msg: proto.Message.IHistorySyncNotification, options: RequestInit) => {
const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options })
const bufferArray: Buffer[] = []
@@ -54,6 +73,21 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
phoneNumber: chat.pnJid || undefined
})
const chatId = chat.id!
const isLid = isLidUser(chatId) || isHostedLidUser(chatId)
const isPn = isPnUser(chatId) || isHostedPnUser(chatId)
if (isLid && chat.pnJid) {
lidPnMappings.push({ lid: chatId, pn: chat.pnJid })
} else if (isPn && chat.lidJid) {
lidPnMappings.push({ lid: chat.lidJid, pn: chatId })
} else if (isLid && !chat.pnJid) {
// Fallback: extract PN from userReceipt in messages when pnJid is missing
const pnFromReceipt = extractPnFromMessages(chat.messages || [])
if (pnFromReceipt) {
lidPnMappings.push({ lid: chatId, pn: pnFromReceipt })
}
}
const msgs = chat.messages || []
delete chat.messages
+268
View File
@@ -91,4 +91,272 @@ describe('processHistoryMessage', () => {
})
})
})
describe('LID-PN mapping extraction from conversations', () => {
it('should extract mapping when chat.id is LID and pnJid exists', () => {
const historySync: proto.IHistorySync = {
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
conversations: [
{
id: '11111111111111@lid',
pnJid: '1234567890123@s.whatsapp.net'
}
]
}
const result = processHistoryMessage(historySync)
expect(result.lidPnMappings).toContainEqual({
lid: '11111111111111@lid',
pn: '1234567890123@s.whatsapp.net'
})
})
it('should extract mapping when chat.id is PN and lidJid exists', () => {
const historySync: proto.IHistorySync = {
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
conversations: [
{
id: '1234567890123@s.whatsapp.net',
lidJid: '11111111111111@lid'
}
]
}
const result = processHistoryMessage(historySync)
expect(result.lidPnMappings).toContainEqual({
lid: '11111111111111@lid',
pn: '1234567890123@s.whatsapp.net'
})
})
it('should not extract mapping for group chats', () => {
const historySync: proto.IHistorySync = {
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
conversations: [
{
id: '123456789012345678@g.us',
lidJid: '11111111111111@lid',
pnJid: '1234567890123@s.whatsapp.net'
}
]
}
const result = processHistoryMessage(historySync)
expect(result.lidPnMappings).toEqual([])
})
it('should combine mappings from phoneNumberToLidMappings and conversations', () => {
const historySync: proto.IHistorySync = {
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
phoneNumberToLidMappings: [{ lidJid: '11111111111111@lid', pnJid: '1111111111111@s.whatsapp.net' }],
conversations: [
{
id: '22222222222222@lid',
pnJid: '2222222222222@s.whatsapp.net'
}
]
}
const result = processHistoryMessage(historySync)
expect(result.lidPnMappings).toHaveLength(2)
expect(result.lidPnMappings).toContainEqual({
lid: '11111111111111@lid',
pn: '1111111111111@s.whatsapp.net'
})
expect(result.lidPnMappings).toContainEqual({
lid: '22222222222222@lid',
pn: '2222222222222@s.whatsapp.net'
})
})
it('should extract mapping for hosted LID users', () => {
const historySync: proto.IHistorySync = {
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
conversations: [
{
id: '11111111111111@hosted.lid',
pnJid: '1234567890123@hosted'
}
]
}
const result = processHistoryMessage(historySync)
expect(result.lidPnMappings).toContainEqual({
lid: '11111111111111@hosted.lid',
pn: '1234567890123@hosted'
})
})
it('should extract mapping for hosted PN users', () => {
const historySync: proto.IHistorySync = {
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
conversations: [
{
id: '1234567890123@hosted',
lidJid: '11111111111111@hosted.lid'
}
]
}
const result = processHistoryMessage(historySync)
expect(result.lidPnMappings).toContainEqual({
lid: '11111111111111@hosted.lid',
pn: '1234567890123@hosted'
})
})
it('should extract mapping from userReceipt when pnJid is missing and chat.id is LID', () => {
// Based on real-world case: LID chat without pnJid but userReceipt contains PN
// See: https://github.com/WhiskeySockets/Baileys/pull/2282#issuecomment-3777941679
const historySync: proto.IHistorySync = {
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
conversations: [
{
id: '211071956705386@lid',
// pnJid is intentionally missing
messages: [
{
message: {
key: {
remoteJid: '211071956705386@lid',
fromMe: true,
id: '3EB052FF8D9D00646C9994'
},
messageTimestamp: 1768320044,
userReceipt: [
{
userJid: '5518999991234@s.whatsapp.net',
receiptTimestamp: 1768320045,
readTimestamp: 1768327083
}
]
}
}
]
}
]
}
const result = processHistoryMessage(historySync)
expect(result.lidPnMappings).toContainEqual({
lid: '211071956705386@lid',
pn: '5518999991234@s.whatsapp.net'
})
})
it('should not extract mapping from userReceipt when pnJid already exists', () => {
const historySync: proto.IHistorySync = {
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
conversations: [
{
id: '211071956705386@lid',
pnJid: '5518888881234@s.whatsapp.net', // pnJid exists
messages: [
{
message: {
key: {
remoteJid: '211071956705386@lid',
fromMe: true,
id: '3EB052FF8D9D00646C9994'
},
userReceipt: [
{
userJid: '5518999991234@s.whatsapp.net' // different PN
}
]
}
}
]
}
]
}
const result = processHistoryMessage(historySync)
// Should use pnJid, not userReceipt
expect(result.lidPnMappings).toContainEqual({
lid: '211071956705386@lid',
pn: '5518888881234@s.whatsapp.net'
})
// Should NOT contain the userReceipt PN
expect(result.lidPnMappings).not.toContainEqual({
lid: '211071956705386@lid',
pn: '5518999991234@s.whatsapp.net'
})
})
it('should not extract mapping from userReceipt when fromMe is false', () => {
const historySync: proto.IHistorySync = {
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
conversations: [
{
id: '211071956705386@lid',
messages: [
{
message: {
key: {
remoteJid: '211071956705386@lid',
fromMe: false, // Not from me
id: '3EB052FF8D9D00646C9994'
},
userReceipt: [
{
userJid: '5518999991234@s.whatsapp.net'
}
]
}
}
]
}
]
}
const result = processHistoryMessage(historySync)
// Should not extract mapping when fromMe is false
expect(result.lidPnMappings).not.toContainEqual({
lid: '211071956705386@lid',
pn: '5518999991234@s.whatsapp.net'
})
})
it('should not extract mapping from userReceipt when userJid is also a LID', () => {
const historySync: proto.IHistorySync = {
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
conversations: [
{
id: '211071956705386@lid',
messages: [
{
message: {
key: {
remoteJid: '211071956705386@lid',
fromMe: true,
id: '3EB052FF8D9D00646C9994'
},
userReceipt: [
{
userJid: '152230971891797@lid' // Also a LID, not a PN
}
]
}
}
]
}
]
}
const result = processHistoryMessage(historySync)
// Should not create a LID->LID mapping
expect(result.lidPnMappings).toHaveLength(0)
})
})
})