From 9454a824d963a355c14e223a1ba973bb8217cda6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 01:35:20 +0000 Subject: [PATCH 1/2] feat(upstream): add comprehensive LID-PN mapping extraction for Baileys PR This folder contains the files ready for upstream WhiskeySockets/Baileys PR: Changes to src/Utils/history.ts: - Add isPersonJid() helper to validate JID types for mapping eligibility - Add extractLidPnFromConversation() for conversation-level mapping extraction - Add extractLidPnFromMessage() for message-level mapping extraction - Modify processHistoryMessage() to extract from 3 sources with deduplication - Fix validation bug: use || (OR) instead of && (AND) to prevent poisoned mappings Changes to src/__tests__/Utils/history.test.ts: - 55 comprehensive tests covering all new functions and edge cases - Tests for all JID formats (@lid, @hosted.lid, @s.whatsapp.net, @hosted) - Tests for skipped JIDs (@g.us, @broadcast, @newsletter) - Tests for bug fix validation (person + group, person + broadcast, etc.) - Tests for deduplication across all three sources Related issues: - Closes WhiskeySockets/Baileys#2282 - Partially addresses WhiskeySockets/Baileys#2281 --- upstream-baileys-pr/src/Utils/history.ts | 292 +++++++ .../src/__tests__/Utils/history.test.ts | 802 ++++++++++++++++++ 2 files changed, 1094 insertions(+) create mode 100644 upstream-baileys-pr/src/Utils/history.ts create mode 100644 upstream-baileys-pr/src/__tests__/Utils/history.test.ts diff --git a/upstream-baileys-pr/src/Utils/history.ts b/upstream-baileys-pr/src/Utils/history.ts new file mode 100644 index 00000000..689cf8f2 --- /dev/null +++ b/upstream-baileys-pr/src/Utils/history.ts @@ -0,0 +1,292 @@ +import { promisify } from 'util' +import { inflate } from 'zlib' +import { proto } from '../../WAProto/index.js' +import type { Chat, Contact, LIDMapping, WAMessage } from '../Types' +import { WAMessageStubType } from '../Types' +import { toNumber } from './generics' +import { normalizeMessageContent } from './messages' +import { downloadContentFromMessage } from './messages-media' + +const inflatePromise = promisify(inflate) + +export const downloadHistory = async (msg: proto.Message.IHistorySyncNotification, options: RequestInit) => { + const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options }) + const bufferArray: Buffer[] = [] + for await (const chunk of stream) { + bufferArray.push(chunk) + } + + let buffer: Buffer = Buffer.concat(bufferArray) + + // decompress buffer + buffer = await inflatePromise(buffer) + + const syncData = proto.HistorySync.decode(buffer) + return syncData +} + +/** + * Checks if a JID represents a person (not a group, broadcast, or newsletter). + * Only person JIDs can have valid LID-PN mappings. + * + * Valid person JID formats: + * - @s.whatsapp.net (standard phone number) + * - @lid (Logical ID) + * - @hosted (hosted phone number) + * - @hosted.lid (hosted Logical ID) + * + * Invalid (non-person) JID formats: + * - @g.us (groups) + * - @broadcast (broadcast lists) + * - @newsletter (channels) + */ +export function isPersonJid(jid: string | undefined): boolean { + if (!jid) { + return false + } + + // Person JIDs: @s.whatsapp.net, @lid, @hosted, @hosted.lid + if ( + jid.endsWith('@s.whatsapp.net') || + jid.endsWith('@lid') || + jid.endsWith('@hosted') || + jid.endsWith('@hosted.lid') + ) { + return true + } + + // Non-person JIDs: @g.us (groups), @broadcast, @newsletter + return false +} + +/** + * Checks if a JID is a LID (Logical ID) format. + * LID formats: @lid or @hosted.lid + */ +function isLidJid(jid: string): boolean { + return jid.endsWith('@lid') || jid.endsWith('@hosted.lid') +} + +/** + * Checks if a JID is a PN (Phone Number) format. + * PN formats: @s.whatsapp.net or @hosted + */ +function isPnJid(jid: string): boolean { + return jid.endsWith('@s.whatsapp.net') || jid.endsWith('@hosted') +} + +/** + * Extracts LID-PN mapping from a conversation object. + * + * Conversations can have: + * - A chat ID that is either LID or PN format + * - Optional lidJid property (alternate LID for the contact) + * - Optional pnJid property (alternate PN for the contact) + * + * Valid extraction scenarios: + * 1. Chat ID is LID + pnJid is present -> mapping found + * 2. Chat ID is PN + lidJid is present -> mapping found + * 3. lidJid + pnJid both present -> mapping found + * + * @returns LIDMapping if valid mapping found, undefined otherwise + */ +export function extractLidPnFromConversation( + chatId: string, + lidJid: string | undefined | null, + pnJid: string | undefined | null +): LIDMapping | undefined { + // Skip non-person chats (groups, broadcasts, newsletters) + if (!isPersonJid(chatId)) { + return undefined + } + + // Case 1: Both lidJid and pnJid are provided + if (lidJid && pnJid && isLidJid(lidJid) && isPnJid(pnJid)) { + return { lid: lidJid, pn: pnJid } + } + + // Case 2: Chat ID is LID format, pnJid provides the phone number + if (isLidJid(chatId) && pnJid && isPnJid(pnJid)) { + return { lid: chatId, pn: pnJid } + } + + // Case 3: Chat ID is PN format, lidJid provides the logical ID + if (isPnJid(chatId) && lidJid && isLidJid(lidJid)) { + return { lid: lidJid, pn: chatId } + } + + return undefined +} + +/** + * Extracts LID-PN mapping from message fields. + * + * Messages can have: + * - remoteJid: Primary JID of the chat/sender + * - remoteJidAlt: Alternate JID format (LID if remoteJid is PN, or vice versa) + * - participant: Sender JID in group messages + * - participantAlt: Alternate format of participant JID + * + * For group messages, participant/participantAlt take priority over remoteJid/remoteJidAlt + * since remoteJid in groups is the group JID, not a person JID. + * + * @returns LIDMapping if valid mapping found, undefined otherwise + */ +export function extractLidPnFromMessage( + remoteJid: string | undefined | null, + remoteJidAlt: string | undefined | null, + participant: string | undefined | null, + participantAlt: string | undefined | null +): LIDMapping | undefined { + // Prefer participant fields (for group messages) + let primaryJid = participant || remoteJid + let altJid = participantAlt || remoteJidAlt + + if (!primaryJid || !altJid) { + return undefined + } + + // CRITICAL: Both JIDs MUST be person JIDs to create a valid mapping. + // Using || ensures we reject if EITHER is not a person JID. + // This prevents "poisoned" mappings where a group/broadcast/newsletter + // JID gets incorrectly paired with a person JID. + if (!isPersonJid(primaryJid) || !isPersonJid(altJid)) { + return undefined + } + + // Determine which is LID and which is PN + if (isLidJid(primaryJid) && isPnJid(altJid)) { + return { lid: primaryJid, pn: altJid } + } + + if (isPnJid(primaryJid) && isLidJid(altJid)) { + return { lid: altJid, pn: primaryJid } + } + + // Both are same type (both LID or both PN) - cannot create mapping + return undefined +} + +export const processHistoryMessage = (item: proto.IHistorySync) => { + const messages: WAMessage[] = [] + const contacts: Contact[] = [] + const chats: Chat[] = [] + + // Use Map for O(1) deduplication of LID-PN mappings + const lidPnMap = new Map() + + const addLidPnMapping = (mapping: LIDMapping | undefined): void => { + if (!mapping || !mapping.lid || !mapping.pn) { + return + } + lidPnMap.set(mapping.lid, mapping) + } + + // Source 1: Extract from phoneNumberToLidMappings array (all sync types) + for (const m of item.phoneNumberToLidMappings || []) { + if (m.lidJid && m.pnJid) { + addLidPnMapping({ lid: m.lidJid, pn: m.pnJid }) + } + } + + switch (item.syncType) { + case proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP: + case proto.HistorySync.HistorySyncType.RECENT: + case proto.HistorySync.HistorySyncType.FULL: + case proto.HistorySync.HistorySyncType.ON_DEMAND: + for (const chat of item.conversations! as Chat[]) { + contacts.push({ + id: chat.id!, + name: chat.name || undefined, + lid: chat.lidJid || undefined, + phoneNumber: chat.pnJid || undefined + }) + + // Source 2: Extract from conversation metadata + addLidPnMapping( + extractLidPnFromConversation(chat.id!, chat.lidJid, chat.pnJid) + ) + + const msgs = chat.messages || [] + delete chat.messages + + for (const item of msgs) { + const message = item.message! as WAMessage + messages.push(message) + + // Source 3: Extract from message alt fields + const key = message.key + if (key) { + addLidPnMapping( + extractLidPnFromMessage( + key.remoteJid, + (key as any).remoteJidAlt, + key.participant, + (key as any).participantAlt + ) + ) + } + + if (!chat.messages?.length) { + // keep only the most recent message in the chat array + chat.messages = [{ message }] + } + + if (!message.key.fromMe && !chat.lastMessageRecvTimestamp) { + chat.lastMessageRecvTimestamp = toNumber(message.messageTimestamp) + } + + if ( + (message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_BSP || + message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_FB) && + message.messageStubParameters?.[0] + ) { + contacts.push({ + id: message.key.participant || message.key.remoteJid!, + verifiedName: message.messageStubParameters?.[0] + }) + } + } + + chats.push({ ...chat }) + } + + break + case proto.HistorySync.HistorySyncType.PUSH_NAME: + for (const c of item.pushnames!) { + contacts.push({ id: c.id!, notify: c.pushname! }) + } + + break + } + + return { + chats, + contacts, + messages, + lidPnMappings: Array.from(lidPnMap.values()), + syncType: item.syncType, + progress: item.progress + } +} + +export const downloadAndProcessHistorySyncNotification = async ( + msg: proto.Message.IHistorySyncNotification, + options: RequestInit +) => { + let historyMsg: proto.HistorySync + if (msg.initialHistBootstrapInlinePayload) { + historyMsg = proto.HistorySync.decode(await inflatePromise(msg.initialHistBootstrapInlinePayload)) + } else { + historyMsg = await downloadHistory(msg, options) + } + + return processHistoryMessage(historyMsg) +} + +export const getHistoryMsg = (message: proto.IMessage) => { + const normalizedContent = !!message ? normalizeMessageContent(message) : undefined + const anyHistoryMsg = normalizedContent?.protocolMessage?.historySyncNotification! + + return anyHistoryMsg +} diff --git a/upstream-baileys-pr/src/__tests__/Utils/history.test.ts b/upstream-baileys-pr/src/__tests__/Utils/history.test.ts new file mode 100644 index 00000000..d5eb2928 --- /dev/null +++ b/upstream-baileys-pr/src/__tests__/Utils/history.test.ts @@ -0,0 +1,802 @@ +import { proto } from '../../../WAProto/index.js' +import { + processHistoryMessage, + extractLidPnFromConversation, + extractLidPnFromMessage, + isPersonJid +} from '../../Utils/history' + +describe('isPersonJid', () => { + it('should return true for @s.whatsapp.net format', () => { + expect(isPersonJid('5511999999999@s.whatsapp.net')).toBe(true) + }) + + it('should return true for @lid format', () => { + expect(isPersonJid('123456789012345@lid')).toBe(true) + }) + + it('should return true for @hosted format', () => { + expect(isPersonJid('5511999999999@hosted')).toBe(true) + }) + + it('should return true for @hosted.lid format', () => { + expect(isPersonJid('123456789012345@hosted.lid')).toBe(true) + }) + + it('should return false for @g.us (groups)', () => { + expect(isPersonJid('123456789012345@g.us')).toBe(false) + }) + + it('should return false for @broadcast', () => { + expect(isPersonJid('status@broadcast')).toBe(false) + }) + + it('should return false for @newsletter (channels)', () => { + expect(isPersonJid('123456789012345@newsletter')).toBe(false) + }) + + it('should return false for undefined', () => { + expect(isPersonJid(undefined)).toBe(false) + }) + + it('should return false for empty string', () => { + expect(isPersonJid('')).toBe(false) + }) +}) + +describe('extractLidPnFromMessage', () => { + describe('direct messages', () => { + it('should extract mapping when remoteJid is LID and remoteJidAlt is PN', () => { + const result = extractLidPnFromMessage( + '123456789012345@lid', + '5511999999999@s.whatsapp.net', + undefined, + undefined + ) + + expect(result).toEqual({ + lid: '123456789012345@lid', + pn: '5511999999999@s.whatsapp.net' + }) + }) + + it('should extract mapping when remoteJid is PN and remoteJidAlt is LID', () => { + const result = extractLidPnFromMessage( + '5511999999999@s.whatsapp.net', + '123456789012345@lid', + undefined, + undefined + ) + + expect(result).toEqual({ + lid: '123456789012345@lid', + pn: '5511999999999@s.whatsapp.net' + }) + }) + }) + + describe('group messages', () => { + it('should extract mapping from participant fields in groups', () => { + const result = extractLidPnFromMessage( + '123456789012345@g.us', // group JID + undefined, + '5511999999999@s.whatsapp.net', // participant + '123456789012345@lid' // participantAlt + ) + + expect(result).toEqual({ + lid: '123456789012345@lid', + pn: '5511999999999@s.whatsapp.net' + }) + }) + + it('should prefer participant over remoteJid for mapping', () => { + const result = extractLidPnFromMessage( + '5511888888888@s.whatsapp.net', // ignored + '987654321098765@lid', // ignored + '5511999999999@s.whatsapp.net', // participant used + '123456789012345@lid' // participantAlt used + ) + + expect(result).toEqual({ + lid: '123456789012345@lid', + pn: '5511999999999@s.whatsapp.net' + }) + }) + }) + + describe('edge cases', () => { + it('should return undefined when no alt JID is present', () => { + const result = extractLidPnFromMessage( + '5511999999999@s.whatsapp.net', + undefined, + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined when both are same type (LID)', () => { + const result = extractLidPnFromMessage( + '123456789012345@lid', + '987654321098765@lid', + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined when both are same type (PN)', () => { + const result = extractLidPnFromMessage( + '5511999999999@s.whatsapp.net', + '5511888888888@s.whatsapp.net', + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined for newsletter messages', () => { + const result = extractLidPnFromMessage( + '123456789012345@newsletter', + undefined, + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + + // CRITICAL: Edge cases for the && vs || fix (bug fix validation) + it('should return undefined when primary is person but alt is GROUP', () => { + const result = extractLidPnFromMessage( + '123456789012345@lid', + '123456789@g.us', // group JID + undefined, + undefined + ) + + // With the fix (using ||), this should return undefined + // because group JIDs cannot be part of a valid LID-PN mapping + expect(result).toBeUndefined() + }) + + it('should return undefined when primary is person but alt is BROADCAST', () => { + const result = extractLidPnFromMessage( + '123456789012345@lid', + 'status@broadcast', // broadcast JID + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined when primary is person but alt is NEWSLETTER', () => { + const result = extractLidPnFromMessage( + '123456789012345@lid', + '123456789@newsletter', // newsletter JID + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined when alt is person but primary is GROUP', () => { + const result = extractLidPnFromMessage( + '123456789@g.us', // group JID + '5511999999999@s.whatsapp.net', + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined when alt is person but primary is BROADCAST', () => { + const result = extractLidPnFromMessage( + 'status@broadcast', // broadcast JID + '5511999999999@s.whatsapp.net', + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined when alt is person but primary is NEWSLETTER', () => { + const result = extractLidPnFromMessage( + '123456789@newsletter', // newsletter JID + '5511999999999@s.whatsapp.net', + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined when BOTH are non-person JIDs', () => { + const result = extractLidPnFromMessage( + '123456789@g.us', // group JID + 'status@broadcast', // broadcast JID + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + }) +}) + +describe('extractLidPnFromConversation', () => { + describe('LID chat with pnJid', () => { + it('should extract mapping when chat ID is @lid format with pnJid', () => { + const result = extractLidPnFromConversation( + '123456789012345@lid', + undefined, + '5511999999999@s.whatsapp.net' + ) + + expect(result).toEqual({ + lid: '123456789012345@lid', + pn: '5511999999999@s.whatsapp.net' + }) + }) + + it('should extract mapping when chat ID is @hosted.lid format with pnJid', () => { + const result = extractLidPnFromConversation( + '123456789012345@hosted.lid', + undefined, + '5511999999999@hosted' + ) + + expect(result).toEqual({ + lid: '123456789012345@hosted.lid', + pn: '5511999999999@hosted' + }) + }) + }) + + describe('PN chat with lidJid', () => { + it('should extract mapping when chat ID is @s.whatsapp.net format with lidJid', () => { + const result = extractLidPnFromConversation( + '5511999999999@s.whatsapp.net', + '123456789012345@lid', + undefined + ) + + expect(result).toEqual({ + lid: '123456789012345@lid', + pn: '5511999999999@s.whatsapp.net' + }) + }) + + it('should extract mapping when chat ID is @hosted format with lidJid', () => { + const result = extractLidPnFromConversation( + '5511999999999@hosted', + '123456789012345@hosted.lid', + undefined + ) + + expect(result).toEqual({ + lid: '123456789012345@hosted.lid', + pn: '5511999999999@hosted' + }) + }) + }) + + describe('edge cases', () => { + it('should return undefined for group chats', () => { + const result = extractLidPnFromConversation( + '123456789012345@g.us', + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined for newsletters (@newsletter)', () => { + const result = extractLidPnFromConversation( + '123456789012345@newsletter', + '11111111111111@lid', + '5511999999999@s.whatsapp.net' + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined for broadcast lists (@broadcast)', () => { + const result = extractLidPnFromConversation( + 'status@broadcast', + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined when no alternate JID is available', () => { + const result = extractLidPnFromConversation( + '123456789012345@lid', + undefined, + undefined + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined when both lidJid and pnJid are null', () => { + const result = extractLidPnFromConversation( + '5511999999999@s.whatsapp.net', + null, + null + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined for LID chat with lidJid (no pnJid)', () => { + const result = extractLidPnFromConversation( + '123456789012345@lid', + '987654321098765@lid', + undefined + ) + + expect(result).toBeUndefined() + }) + + it('should return undefined for PN chat with pnJid (no lidJid)', () => { + const result = extractLidPnFromConversation( + '5511999999999@s.whatsapp.net', + undefined, + '5511888888888@s.whatsapp.net' + ) + + expect(result).toBeUndefined() + }) + }) +}) + +describe('processHistoryMessage', () => { + describe('phoneNumberToLidMappings extraction', () => { + it('should extract LID-PN mappings from history sync payload', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [], + phoneNumberToLidMappings: [ + { lidJid: '11111111111111@lid', pnJid: '1234567890123@s.whatsapp.net' }, + { lidJid: '22222222222222@lid', pnJid: '9876543210987@s.whatsapp.net' } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([ + { lid: '11111111111111@lid', pn: '1234567890123@s.whatsapp.net' }, + { lid: '22222222222222@lid', pn: '9876543210987@s.whatsapp.net' } + ]) + }) + + it('should skip mappings with missing lidJid or pnJid', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.RECENT, + conversations: [], + phoneNumberToLidMappings: [ + { lidJid: undefined, pnJid: '1234567890123@s.whatsapp.net' }, + { lidJid: '11111111111111@lid', pnJid: undefined }, + { lidJid: '22222222222222@lid', pnJid: '9876543210987@s.whatsapp.net' } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([{ lid: '22222222222222@lid', pn: '9876543210987@s.whatsapp.net' }]) + }) + + it('should return empty array when no mappings exist', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([]) + }) + + it('should process mappings regardless of sync type', () => { + const syncTypes = [proto.HistorySync.HistorySyncType.PUSH_NAME, proto.HistorySync.HistorySyncType.ON_DEMAND] + + for (const syncType of syncTypes) { + const historySync: proto.IHistorySync = { + syncType, + conversations: [], + pushnames: [], + phoneNumberToLidMappings: [{ lidJid: '11111111111111@lid', pnJid: '1234567890123@s.whatsapp.net' }] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([{ lid: '11111111111111@lid', pn: '1234567890123@s.whatsapp.net' }]) + } + }) + }) + + describe('conversations processing', () => { + it('should extract contacts with LID and PN from conversations', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [ + { + id: '1234567890123@s.whatsapp.net', + name: 'Test User', + lidJid: '11111111111111@lid', + pnJid: '1234567890123@s.whatsapp.net' + } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.contacts).toHaveLength(1) + expect(result.contacts[0]).toEqual({ + id: '1234567890123@s.whatsapp.net', + name: 'Test User', + lid: '11111111111111@lid', + phoneNumber: '1234567890123@s.whatsapp.net' + }) + }) + + it('should extract LID-PN mappings from conversation lidJid/pnJid properties', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [ + { + id: '1234567890123@s.whatsapp.net', + name: 'Test User', + lidJid: '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 NOT extract LID-PN mappings from group conversations', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [ + { + id: '123456789@g.us', + name: 'Test Group', + lidJid: '11111111111111@lid', + pnJid: '1234567890123@s.whatsapp.net' + } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([]) + }) + }) + + describe('LID-PN extraction from conversations', () => { + it('should extract LID-PN mapping when chat ID is LID with pnJid', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [ + { + id: '11111111111111@lid', + name: 'LID User', + pnJid: '5511999999999@s.whatsapp.net' + } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([ + { lid: '11111111111111@lid', pn: '5511999999999@s.whatsapp.net' } + ]) + }) + + it('should extract LID-PN mapping when chat ID is PN with lidJid', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [ + { + id: '5511999999999@s.whatsapp.net', + name: 'PN User', + lidJid: '11111111111111@lid' + } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([ + { lid: '11111111111111@lid', pn: '5511999999999@s.whatsapp.net' } + ]) + }) + + it('should deduplicate mappings from both sources', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + phoneNumberToLidMappings: [ + { lidJid: '11111111111111@lid', pnJid: '5511999999999@s.whatsapp.net' } + ], + conversations: [ + { + id: '11111111111111@lid', + name: 'Same User', + pnJid: '5511999999999@s.whatsapp.net' + } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toHaveLength(1) + expect(result.lidPnMappings[0]).toEqual({ + lid: '11111111111111@lid', + pn: '5511999999999@s.whatsapp.net' + }) + }) + + it('should combine mappings from both sources', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + phoneNumberToLidMappings: [ + { lidJid: '11111111111111@lid', pnJid: '5511999999999@s.whatsapp.net' } + ], + conversations: [ + { + id: '22222222222222@lid', + name: 'Different User', + pnJid: '5511888888888@s.whatsapp.net' + } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toHaveLength(2) + expect(result.lidPnMappings).toContainEqual({ + lid: '11111111111111@lid', + pn: '5511999999999@s.whatsapp.net' + }) + expect(result.lidPnMappings).toContainEqual({ + lid: '22222222222222@lid', + pn: '5511888888888@s.whatsapp.net' + }) + }) + + it('should not extract mapping from group chats', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [ + { + id: '123456789012345@g.us', + name: 'Group Chat' + } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([]) + }) + + it('should handle hosted LID format', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [ + { + id: '11111111111111@hosted.lid', + name: 'Hosted LID User', + pnJid: '5511999999999@hosted' + } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([ + { lid: '11111111111111@hosted.lid', pn: '5511999999999@hosted' } + ]) + }) + + it('should extract mappings for all conversation sync types', () => { + const syncTypes = [ + proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + proto.HistorySync.HistorySyncType.RECENT, + proto.HistorySync.HistorySyncType.FULL, + proto.HistorySync.HistorySyncType.ON_DEMAND + ] + + for (const syncType of syncTypes) { + const historySync: proto.IHistorySync = { + syncType, + conversations: [ + { + id: '11111111111111@lid', + pnJid: '5511999999999@s.whatsapp.net' + } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([ + { lid: '11111111111111@lid', pn: '5511999999999@s.whatsapp.net' } + ]) + } + }) + + it('should not extract conversation mappings for PUSH_NAME sync type', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.PUSH_NAME, + pushnames: [ + { id: '5511999999999@s.whatsapp.net', pushname: 'User Name' } + ], + phoneNumberToLidMappings: [ + { lidJid: '11111111111111@lid', pnJid: '5511999999999@s.whatsapp.net' } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([ + { lid: '11111111111111@lid', pn: '5511999999999@s.whatsapp.net' } + ]) + }) + + it('should not extract mapping from newsletter conversations', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [ + { + id: '123456789012345@newsletter', + name: 'News Channel' + } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([]) + }) + + it('should not extract mapping from broadcast conversations', () => { + const historySync: proto.IHistorySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [ + { + id: 'status@broadcast', + name: 'Status' + } + ] + } + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toEqual([]) + }) + }) + + describe('LID-PN extraction from messages (remoteJidAlt)', () => { + it('should extract mapping from message with remoteJidAlt', () => { + const historySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [ + { + id: '5511999999999@s.whatsapp.net', + name: 'User', + messages: [ + { + message: { + key: { + remoteJid: '5511999999999@s.whatsapp.net', + remoteJidAlt: '11111111111111@lid', + id: 'MSG123', + fromMe: false + }, + messageTimestamp: 1234567890 + } + } + ] + } + ] + } as unknown as proto.IHistorySync + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toContainEqual({ + lid: '11111111111111@lid', + pn: '5511999999999@s.whatsapp.net' + }) + }) + + it('should extract mapping from group message with participantAlt', () => { + const historySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + conversations: [ + { + id: '123456789012345@g.us', + name: 'Group Chat', + messages: [ + { + message: { + key: { + remoteJid: '123456789012345@g.us', + participant: '5511999999999@s.whatsapp.net', + participantAlt: '11111111111111@lid', + id: 'MSG456', + fromMe: false + }, + messageTimestamp: 1234567890 + } + } + ] + } + ] + } as unknown as proto.IHistorySync + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toContainEqual({ + lid: '11111111111111@lid', + pn: '5511999999999@s.whatsapp.net' + }) + }) + + it('should deduplicate mappings from all three sources', () => { + const historySync = { + syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, + phoneNumberToLidMappings: [ + { lidJid: '11111111111111@lid', pnJid: '5511999999999@s.whatsapp.net' } + ], + conversations: [ + { + id: '11111111111111@lid', + name: 'User', + pnJid: '5511999999999@s.whatsapp.net', + messages: [ + { + message: { + key: { + remoteJid: '11111111111111@lid', + remoteJidAlt: '5511999999999@s.whatsapp.net', + id: 'MSG789', + fromMe: true + }, + messageTimestamp: 1234567890 + } + } + ] + } + ] + } as unknown as proto.IHistorySync + + const result = processHistoryMessage(historySync) + + expect(result.lidPnMappings).toHaveLength(1) + expect(result.lidPnMappings[0]).toEqual({ + lid: '11111111111111@lid', + pn: '5511999999999@s.whatsapp.net' + }) + }) + }) +}) From d96820537601f89a1ad1d847fda1372efe5e6f4d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 04:24:09 +0000 Subject: [PATCH 2/2] fix: resolve TypeScript errors in merged code - Add null check for decoded in getLIDsForPNs loop - Fix jest mock typing in process-message.test.ts --- src/Signal/lid-mapping.ts | 4 +++- src/__tests__/Utils/process-message.test.ts | 22 ++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Signal/lid-mapping.ts b/src/Signal/lid-mapping.ts index 2e3176fb..dba3e07c 100644 --- a/src/Signal/lid-mapping.ts +++ b/src/Signal/lid-mapping.ts @@ -522,7 +522,7 @@ export class LIDMappingStore { for (const [pnUser, items] of pendingByPnUser.entries()) { const lidUser = this.mappingCache.get(`pn:${pnUser}`) for (const { pn, decoded } of items) { - if (lidUser) { + if (lidUser && decoded) { const lidUserString = lidUser.toString() if (!lidUserString) { this.logger.warn({ pn, lidUser }, 'Invalid or empty LID user') @@ -548,6 +548,8 @@ export class LIDMappingStore { failedPns.add(pn) } + if (!decoded) continue + // Need to fetch from USync if (this.config.debugLogging) { this.logger.trace({ pnUser }, 'No LID mapping found, queuing for USync') diff --git a/src/__tests__/Utils/process-message.test.ts b/src/__tests__/Utils/process-message.test.ts index e4922dfb..fd530003 100644 --- a/src/__tests__/Utils/process-message.test.ts +++ b/src/__tests__/Utils/process-message.test.ts @@ -1,3 +1,4 @@ +import { jest } from '@jest/globals' import type { WAMessage } from '../../Types' import type { SignalRepositoryWithLIDStore } from '../../Types' import { cleanMessage, normalizeMessageJids } from '../../Utils/process-message' @@ -139,14 +140,17 @@ describe('cleanMessage', () => { }) describe('normalizeMessageJids', () => { - const signalRepository = { - lidMapping: { - getPNForLID: jest.fn() - } - } as unknown as SignalRepositoryWithLIDStore + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockGetPNForLID: any + let signalRepository: SignalRepositoryWithLIDStore beforeEach(() => { - jest.clearAllMocks() + mockGetPNForLID = jest.fn() + signalRepository = { + lidMapping: { + getPNForLID: mockGetPNForLID + } + } as unknown as SignalRepositoryWithLIDStore }) it('should resolve remoteJid LID to PN when mapping exists', async () => { @@ -154,7 +158,7 @@ describe('normalizeMessageJids', () => { remoteJid: '123456789@lid' }) - signalRepository.lidMapping.getPNForLID = jest.fn().mockResolvedValue('5511999999999@s.whatsapp.net') + mockGetPNForLID.mockResolvedValue('5511999999999@s.whatsapp.net') await normalizeMessageJids(message, signalRepository) @@ -166,7 +170,7 @@ describe('normalizeMessageJids', () => { remoteJid: '123456789@lid' }) - signalRepository.lidMapping.getPNForLID = jest.fn().mockResolvedValue(null) + mockGetPNForLID.mockResolvedValue(null) await normalizeMessageJids(message, signalRepository) @@ -178,7 +182,7 @@ describe('normalizeMessageJids', () => { participant: '999999999@lid' }) - signalRepository.lidMapping.getPNForLID = jest.fn().mockResolvedValue('5511888888888@s.whatsapp.net') + mockGetPNForLID.mockResolvedValue('5511888888888@s.whatsapp.net') await normalizeMessageJids(message, signalRepository)