Skip to content

Commit f829b6d

Browse files
authored
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
1 parent 52fcad2 commit f829b6d

2 files changed

Lines changed: 302 additions & 0 deletions

File tree

src/Utils/history.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,32 @@ import { inflate } from 'zlib'
33
import { proto } from '../../WAProto/index.js'
44
import type { Chat, Contact, LIDMapping, WAMessage } from '../Types'
55
import { WAMessageStubType } from '../Types'
6+
import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser } from '../WABinary'
67
import { toNumber } from './generics'
78
import type { ILogger } from './logger.js'
89
import { normalizeMessageContent } from './messages'
910
import { downloadContentFromMessage } from './messages-media'
1011

1112
const inflatePromise = promisify(inflate)
1213

14+
const extractPnFromMessages = (messages: proto.IHistorySyncMsg[]): string | undefined => {
15+
for (const msgItem of messages) {
16+
const message = msgItem.message
17+
// Only extract from outgoing messages (fromMe: true) in 1:1 chats
18+
// because userReceipt.userJid is the recipient's JID
19+
if (!message?.key?.fromMe || !message.userReceipt?.length) {
20+
continue
21+
}
22+
23+
const userJid = message.userReceipt[0]?.userJid
24+
if (userJid && (isPnUser(userJid) || isHostedPnUser(userJid))) {
25+
return userJid
26+
}
27+
}
28+
29+
return undefined
30+
}
31+
1332
export const downloadHistory = async (msg: proto.Message.IHistorySyncNotification, options: RequestInit) => {
1433
const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options })
1534
const bufferArray: Buffer[] = []
@@ -54,6 +73,21 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
5473
phoneNumber: chat.pnJid || undefined
5574
})
5675

76+
const chatId = chat.id!
77+
const isLid = isLidUser(chatId) || isHostedLidUser(chatId)
78+
const isPn = isPnUser(chatId) || isHostedPnUser(chatId)
79+
if (isLid && chat.pnJid) {
80+
lidPnMappings.push({ lid: chatId, pn: chat.pnJid })
81+
} else if (isPn && chat.lidJid) {
82+
lidPnMappings.push({ lid: chat.lidJid, pn: chatId })
83+
} else if (isLid && !chat.pnJid) {
84+
// Fallback: extract PN from userReceipt in messages when pnJid is missing
85+
const pnFromReceipt = extractPnFromMessages(chat.messages || [])
86+
if (pnFromReceipt) {
87+
lidPnMappings.push({ lid: chatId, pn: pnFromReceipt })
88+
}
89+
}
90+
5791
const msgs = chat.messages || []
5892
delete chat.messages
5993

src/__tests__/Utils/history.test.ts

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,272 @@ describe('processHistoryMessage', () => {
9191
})
9292
})
9393
})
94+
95+
describe('LID-PN mapping extraction from conversations', () => {
96+
it('should extract mapping when chat.id is LID and pnJid exists', () => {
97+
const historySync: proto.IHistorySync = {
98+
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
99+
conversations: [
100+
{
101+
id: '11111111111111@lid',
102+
pnJid: '1234567890123@s.whatsapp.net'
103+
}
104+
]
105+
}
106+
107+
const result = processHistoryMessage(historySync)
108+
109+
expect(result.lidPnMappings).toContainEqual({
110+
lid: '11111111111111@lid',
111+
pn: '1234567890123@s.whatsapp.net'
112+
})
113+
})
114+
115+
it('should extract mapping when chat.id is PN and lidJid exists', () => {
116+
const historySync: proto.IHistorySync = {
117+
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
118+
conversations: [
119+
{
120+
id: '1234567890123@s.whatsapp.net',
121+
lidJid: '11111111111111@lid'
122+
}
123+
]
124+
}
125+
126+
const result = processHistoryMessage(historySync)
127+
128+
expect(result.lidPnMappings).toContainEqual({
129+
lid: '11111111111111@lid',
130+
pn: '1234567890123@s.whatsapp.net'
131+
})
132+
})
133+
134+
it('should not extract mapping for group chats', () => {
135+
const historySync: proto.IHistorySync = {
136+
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
137+
conversations: [
138+
{
139+
id: '123456789012345678@g.us',
140+
lidJid: '11111111111111@lid',
141+
pnJid: '1234567890123@s.whatsapp.net'
142+
}
143+
]
144+
}
145+
146+
const result = processHistoryMessage(historySync)
147+
148+
expect(result.lidPnMappings).toEqual([])
149+
})
150+
151+
it('should combine mappings from phoneNumberToLidMappings and conversations', () => {
152+
const historySync: proto.IHistorySync = {
153+
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
154+
phoneNumberToLidMappings: [{ lidJid: '11111111111111@lid', pnJid: '1111111111111@s.whatsapp.net' }],
155+
conversations: [
156+
{
157+
id: '22222222222222@lid',
158+
pnJid: '2222222222222@s.whatsapp.net'
159+
}
160+
]
161+
}
162+
163+
const result = processHistoryMessage(historySync)
164+
165+
expect(result.lidPnMappings).toHaveLength(2)
166+
expect(result.lidPnMappings).toContainEqual({
167+
lid: '11111111111111@lid',
168+
pn: '1111111111111@s.whatsapp.net'
169+
})
170+
expect(result.lidPnMappings).toContainEqual({
171+
lid: '22222222222222@lid',
172+
pn: '2222222222222@s.whatsapp.net'
173+
})
174+
})
175+
176+
it('should extract mapping for hosted LID users', () => {
177+
const historySync: proto.IHistorySync = {
178+
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
179+
conversations: [
180+
{
181+
id: '11111111111111@hosted.lid',
182+
pnJid: '1234567890123@hosted'
183+
}
184+
]
185+
}
186+
187+
const result = processHistoryMessage(historySync)
188+
189+
expect(result.lidPnMappings).toContainEqual({
190+
lid: '11111111111111@hosted.lid',
191+
pn: '1234567890123@hosted'
192+
})
193+
})
194+
195+
it('should extract mapping for hosted PN users', () => {
196+
const historySync: proto.IHistorySync = {
197+
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
198+
conversations: [
199+
{
200+
id: '1234567890123@hosted',
201+
lidJid: '11111111111111@hosted.lid'
202+
}
203+
]
204+
}
205+
206+
const result = processHistoryMessage(historySync)
207+
208+
expect(result.lidPnMappings).toContainEqual({
209+
lid: '11111111111111@hosted.lid',
210+
pn: '1234567890123@hosted'
211+
})
212+
})
213+
214+
it('should extract mapping from userReceipt when pnJid is missing and chat.id is LID', () => {
215+
// Based on real-world case: LID chat without pnJid but userReceipt contains PN
216+
// See: https://github.com/WhiskeySockets/Baileys/pull/2282#issuecomment-3777941679
217+
const historySync: proto.IHistorySync = {
218+
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
219+
conversations: [
220+
{
221+
id: '211071956705386@lid',
222+
// pnJid is intentionally missing
223+
messages: [
224+
{
225+
message: {
226+
key: {
227+
remoteJid: '211071956705386@lid',
228+
fromMe: true,
229+
id: '3EB052FF8D9D00646C9994'
230+
},
231+
messageTimestamp: 1768320044,
232+
userReceipt: [
233+
{
234+
userJid: '5518999991234@s.whatsapp.net',
235+
receiptTimestamp: 1768320045,
236+
readTimestamp: 1768327083
237+
}
238+
]
239+
}
240+
}
241+
]
242+
}
243+
]
244+
}
245+
246+
const result = processHistoryMessage(historySync)
247+
248+
expect(result.lidPnMappings).toContainEqual({
249+
lid: '211071956705386@lid',
250+
pn: '5518999991234@s.whatsapp.net'
251+
})
252+
})
253+
254+
it('should not extract mapping from userReceipt when pnJid already exists', () => {
255+
const historySync: proto.IHistorySync = {
256+
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
257+
conversations: [
258+
{
259+
id: '211071956705386@lid',
260+
pnJid: '5518888881234@s.whatsapp.net', // pnJid exists
261+
messages: [
262+
{
263+
message: {
264+
key: {
265+
remoteJid: '211071956705386@lid',
266+
fromMe: true,
267+
id: '3EB052FF8D9D00646C9994'
268+
},
269+
userReceipt: [
270+
{
271+
userJid: '5518999991234@s.whatsapp.net' // different PN
272+
}
273+
]
274+
}
275+
}
276+
]
277+
}
278+
]
279+
}
280+
281+
const result = processHistoryMessage(historySync)
282+
283+
// Should use pnJid, not userReceipt
284+
expect(result.lidPnMappings).toContainEqual({
285+
lid: '211071956705386@lid',
286+
pn: '5518888881234@s.whatsapp.net'
287+
})
288+
// Should NOT contain the userReceipt PN
289+
expect(result.lidPnMappings).not.toContainEqual({
290+
lid: '211071956705386@lid',
291+
pn: '5518999991234@s.whatsapp.net'
292+
})
293+
})
294+
295+
it('should not extract mapping from userReceipt when fromMe is false', () => {
296+
const historySync: proto.IHistorySync = {
297+
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
298+
conversations: [
299+
{
300+
id: '211071956705386@lid',
301+
messages: [
302+
{
303+
message: {
304+
key: {
305+
remoteJid: '211071956705386@lid',
306+
fromMe: false, // Not from me
307+
id: '3EB052FF8D9D00646C9994'
308+
},
309+
userReceipt: [
310+
{
311+
userJid: '5518999991234@s.whatsapp.net'
312+
}
313+
]
314+
}
315+
}
316+
]
317+
}
318+
]
319+
}
320+
321+
const result = processHistoryMessage(historySync)
322+
323+
// Should not extract mapping when fromMe is false
324+
expect(result.lidPnMappings).not.toContainEqual({
325+
lid: '211071956705386@lid',
326+
pn: '5518999991234@s.whatsapp.net'
327+
})
328+
})
329+
330+
it('should not extract mapping from userReceipt when userJid is also a LID', () => {
331+
const historySync: proto.IHistorySync = {
332+
syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
333+
conversations: [
334+
{
335+
id: '211071956705386@lid',
336+
messages: [
337+
{
338+
message: {
339+
key: {
340+
remoteJid: '211071956705386@lid',
341+
fromMe: true,
342+
id: '3EB052FF8D9D00646C9994'
343+
},
344+
userReceipt: [
345+
{
346+
userJid: '152230971891797@lid' // Also a LID, not a PN
347+
}
348+
]
349+
}
350+
}
351+
]
352+
}
353+
]
354+
}
355+
356+
const result = processHistoryMessage(historySync)
357+
358+
// Should not create a LID->LID mapping
359+
expect(result.lidPnMappings).toHaveLength(0)
360+
})
361+
})
94362
})

0 commit comments

Comments
 (0)