fix: resolve merge conflicts from master

Merge master branch and resolve conflicts:
- Use import from '../WABinary/index.js' (master convention)
- Keep the bug fix: || instead of && in extractLidPnFromMessage
- Combine comprehensive tests from both branches
- Use Map for deduplication (master implementation)

All 55 tests passing.
This commit is contained in:
Claude
2026-01-20 23:22:45 +00:00
2 changed files with 915 additions and 415 deletions
+260 -143
View File
@@ -15,125 +15,18 @@ import {
isLidUser,
isPnUser,
jidDecode,
jidNormalizedUser,
} from '../WABinary/jid-utils'
jidNormalizedUser
} from '../WABinary/index.js'
const inflatePromise = promisify(inflate)
/**
* Checks if a JID represents a person (individual user) rather than a group, broadcast, or newsletter.
* Only person JIDs (LID or PN formats) can have LID-PN mappings.
*/
export function isPersonJid(jid: string | undefined): boolean {
if(!jid) {
return false
}
// Groups, broadcasts, and newsletters don't have LID-PN mappings
if(isJidGroup(jid) || isJidBroadcast(jid) || isJidNewsletter(jid)) {
return false
}
// Only person JIDs (LID or PN formats) can have mappings
return !!(
isLidUser(jid) ||
isHostedLidUser(jid) ||
isPnUser(jid) ||
isHostedPnUser(jid)
)
}
/**
* Extracts a LID-PN mapping from conversation data.
* Returns undefined if the conversation doesn't represent a valid person-to-person mapping.
*/
export function extractLidPnFromConversation(
chatId: string,
lidJid: string | undefined | null,
pnJid: string | undefined | null
): LIDMapping | undefined {
// Skip non-person JIDs (groups, broadcasts, newsletters)
if(!isPersonJid(chatId)) {
return undefined
}
const chatIsLid = isLidUser(chatId) || isHostedLidUser(chatId)
const chatIsPn = isPnUser(chatId) || isHostedPnUser(chatId)
if(chatIsLid && pnJid) {
return {
lid: jidNormalizedUser(chatId),
pn: jidNormalizedUser(pnJid)
}
}
if(chatIsPn && lidJid) {
return {
lid: jidNormalizedUser(lidJid),
pn: jidNormalizedUser(chatId)
}
}
return undefined
}
/**
* Extracts a LID-PN mapping from message data (remoteJidAlt/participantAlt fields).
* Returns undefined if the message doesn't contain a valid person-to-person mapping.
* Downloads and decompresses history sync data from WhatsApp servers.
*
* IMPORTANT: Uses || (OR) to ensure BOTH JIDs are person JIDs before extracting.
* This prevents "poisoned" mappings where one side is a group/newsletter/broadcast.
* @param msg - The history sync notification message containing download info
* @param options - Request options for the download
* @returns Decoded HistorySync protocol buffer
*/
export function extractLidPnFromMessage(
remoteJid: string | undefined | null,
remoteJidAlt: string | undefined | null,
participant: string | undefined | null,
participantAlt: string | undefined | null
): LIDMapping | undefined {
const primaryJid = participant || remoteJid
const altJid = participantAlt || remoteJidAlt
if(!primaryJid || !altJid) {
return undefined
}
// FIXED: Use || (OR) instead of && (AND)
// Both JIDs MUST be person JIDs to create a valid mapping.
// Using && would allow mixed scenarios (e.g., person + group) to proceed,
// resulting in invalid "poisoned" mappings.
if(!isPersonJid(primaryJid) || !isPersonJid(altJid)) {
return undefined
}
const primaryDecoded = jidDecode(primaryJid)
const altDecoded = jidDecode(altJid)
if(!primaryDecoded || !altDecoded) {
return undefined
}
const primaryIsLid = primaryDecoded.server === 'lid' ||
primaryDecoded.server === 'hosted.lid'
const altIsLid = altDecoded.server === 'lid' ||
altDecoded.server === 'hosted.lid'
if(primaryIsLid && !altIsLid) {
return {
lid: jidNormalizedUser(primaryJid),
pn: jidNormalizedUser(altJid)
}
}
if(!primaryIsLid && altIsLid) {
return {
lid: jidNormalizedUser(altJid),
pn: jidNormalizedUser(primaryJid)
}
}
return undefined
}
export const downloadHistory = async (msg: proto.Message.IHistorySyncNotification, options: RequestInit) => {
const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options })
const bufferArray: Buffer[] = []
@@ -150,15 +43,219 @@ export const downloadHistory = async (msg: proto.Message.IHistorySyncNotificatio
return syncData
}
/**
* Checks if a JID represents a person (can have LID-PN mapping).
* Excludes groups, broadcasts, newsletters, and bots.
*
* @param jid - The JID to check
* @returns true if the JID can have LID-PN mapping
*/
export function isPersonJid(jid: string | undefined): boolean {
if (!jid) {
return false
}
// Groups, broadcasts, and newsletters don't have LID-PN mappings
if (isJidGroup(jid) || isJidBroadcast(jid) || isJidNewsletter(jid)) {
return false
}
// Only person JIDs (LID or PN formats) can have mappings
return !!(
isLidUser(jid) ||
isHostedLidUser(jid) ||
isPnUser(jid) ||
isHostedPnUser(jid)
)
}
/**
* Extracts LID-PN mapping from a conversation object.
*
* WhatsApp uses two identifier systems:
* - LID (Logical ID): Format `{number}@lid` or `{number}@hosted.lid`
* - PN (Phone Number): Format `{number}@s.whatsapp.net` or `{number}@hosted`
*
* Conversations may have their ID in either format, with the alternate
* format stored in `lidJid` or `pnJid` properties respectively.
*
* Skips non-person JIDs:
* - `@g.us` (groups)
* - `@broadcast` (broadcast lists)
* - `@newsletter` (channels)
*
* @param chatId - The conversation ID (may be LID or PN format)
* @param lidJid - The LID JID if chat ID is PN format
* @param pnJid - The PN JID if chat ID is LID format
* @returns LID-PN mapping if extractable, undefined otherwise
*
* @example
* // Chat ID is LID, pnJid contains phone number
* extractLidPnFromConversation('123456789@lid', undefined, '5511999999999@s.whatsapp.net')
* // Returns: { lid: '123456789@lid', pn: '5511999999999@s.whatsapp.net' }
*
* @example
* // Chat ID is PN, lidJid contains LID
* extractLidPnFromConversation('5511999999999@s.whatsapp.net', '123456789@lid', undefined)
* // Returns: { lid: '123456789@lid', pn: '5511999999999@s.whatsapp.net' }
*
* @example
* // Newsletter - returns undefined (no mapping)
* extractLidPnFromConversation('123456789@newsletter', undefined, undefined)
* // Returns: undefined
*/
export function extractLidPnFromConversation(
chatId: string,
lidJid: string | undefined | null,
pnJid: string | undefined | null
): LIDMapping | undefined {
// Skip non-person JIDs (groups, broadcasts, newsletters)
if (!isPersonJid(chatId)) {
return undefined
}
// Check if chat ID is in LID format
const chatIsLid = isLidUser(chatId) || isHostedLidUser(chatId)
// Check if chat ID is in PN format
const chatIsPn = isPnUser(chatId) || isHostedPnUser(chatId)
if (chatIsLid && pnJid) {
// Chat ID is LID, pnJid contains the phone number
return {
lid: jidNormalizedUser(chatId),
pn: jidNormalizedUser(pnJid)
}
}
if (chatIsPn && lidJid) {
// Chat ID is PN, lidJid contains the LID
return {
lid: jidNormalizedUser(lidJid),
pn: jidNormalizedUser(chatId)
}
}
return undefined
}
/**
* Extracts LID-PN mapping from a message's alternative JID fields.
*
* Messages may contain alternate JID formats in:
* - `key.remoteJidAlt` - Alternative remote JID format
* - `key.participantAlt` - Alternative participant JID format (for groups)
*
* IMPORTANT: Uses || (OR) to ensure BOTH JIDs are person JIDs before extracting.
* This prevents "poisoned" mappings where one side is a group/newsletter/broadcast.
*
* @param remoteJid - The primary remote JID
* @param remoteJidAlt - The alternative remote JID (may be LID or PN)
* @param participant - The primary participant JID (for group messages)
* @param participantAlt - The alternative participant JID
* @returns LID-PN mapping if extractable, undefined otherwise
*/
export function extractLidPnFromMessage(
remoteJid: string | undefined | null,
remoteJidAlt: string | undefined | null,
participant: string | undefined | null,
participantAlt: string | undefined | null
): LIDMapping | undefined {
// For group messages, use participant fields
const primaryJid = participant || remoteJid
const altJid = participantAlt || remoteJidAlt
if (!primaryJid || !altJid) {
return undefined
}
// FIXED: Use || (OR) instead of && (AND)
// Both JIDs MUST be person JIDs to create a valid mapping.
// Using && would allow mixed scenarios (e.g., person + group) to proceed,
// resulting in invalid "poisoned" mappings.
if (!isPersonJid(primaryJid) || !isPersonJid(altJid)) {
return undefined
}
const primaryDecoded = jidDecode(primaryJid)
const altDecoded = jidDecode(altJid)
if (!primaryDecoded || !altDecoded) {
return undefined
}
// Determine which is LID and which is PN
const primaryIsLid = primaryDecoded.server === 'lid' || primaryDecoded.server === 'hosted.lid'
const altIsLid = altDecoded.server === 'lid' || altDecoded.server === 'hosted.lid'
if (primaryIsLid && !altIsLid) {
// Primary is LID, alt is PN
return {
lid: jidNormalizedUser(primaryJid),
pn: jidNormalizedUser(altJid)
}
}
if (!primaryIsLid && altIsLid) {
// Primary is PN, alt is LID
return {
lid: jidNormalizedUser(altJid),
pn: jidNormalizedUser(primaryJid)
}
}
return undefined
}
/**
* Processes a history sync message and extracts chats, contacts, messages,
* and LID-PN mappings.
*
* LID-PN mappings are extracted from three sources:
* 1. Top-level `phoneNumberToLidMappings` array in the history sync payload
* 2. Individual conversation objects that contain both LID and PN identifiers
* (via `lidJid` and `pnJid` properties)
* 3. Message objects with alternate JID fields (`remoteJidAlt`, `participantAlt`)
*
* This multi-source extraction ensures maximum mapping coverage, as WhatsApp may
* provide mappings in different locations depending on the sync type and context.
*
* Skipped JID types (no LID-PN mapping):
* - `@g.us` (groups)
* - `@broadcast` (broadcast lists)
* - `@newsletter` (channels)
*
* @param item - The history sync protocol buffer to process
* @returns Processed data including chats, contacts, messages, and LID-PN mappings
*
* @see https://github.com/WhiskeySockets/Baileys/issues/2263
*/
export const processHistoryMessage = (item: proto.IHistorySync) => {
const messages: WAMessage[] = []
const contacts: Contact[] = []
const chats: Chat[] = []
// Extract LID-PN mappings for all sync types
const lidPnMappings: LIDMapping[] = []
// Use Map for O(1) deduplication of LID-PN mappings
const lidPnMap = new Map<string, LIDMapping>()
/**
* Adds a LID-PN mapping to the map with deduplication.
* Uses LID as key since each LID should map to exactly one PN.
*/
const addLidPnMapping = (mapping: LIDMapping): void => {
// Normalize and validate
if (!mapping.lid || !mapping.pn) {
return
}
lidPnMap.set(mapping.lid, mapping)
}
// Source 1: Extract from top-level phoneNumberToLidMappings array
for (const m of item.phoneNumberToLidMappings || []) {
if (m.lidJid && m.pnJid) {
lidPnMappings.push({ lid: m.lidJid, pn: m.pnJid })
addLidPnMapping({
lid: jidNormalizedUser(m.lidJid),
pn: jidNormalizedUser(m.pnJid)
})
}
}
@@ -167,41 +264,56 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
case proto.HistorySync.HistorySyncType.RECENT:
case proto.HistorySync.HistorySyncType.FULL:
case proto.HistorySync.HistorySyncType.ON_DEMAND:
for(const chat of item.conversations! as Chat[]) {
for (const chat of item.conversations! as Chat[]) {
const chatId = chat.id!
// Source 2: Extract LID-PN mapping from conversation object
// This handles cases where the mapping isn't in phoneNumberToLidMappings
const conversationMapping = extractLidPnFromConversation(
chatId,
chat.lidJid,
chat.pnJid
)
if (conversationMapping) {
addLidPnMapping(conversationMapping)
}
contacts.push({
id: chat.id!,
id: chatId,
name: chat.name || undefined,
lid: chat.lidJid || undefined,
phoneNumber: chat.pnJid || undefined
})
// Extract LID-PN mapping from conversation (lidJid/pnJid properties)
const convMapping = extractLidPnFromConversation(
chat.id!,
chat.lidJid,
chat.pnJid
)
if(convMapping) {
lidPnMappings.push(convMapping)
}
const msgs = chat.messages || []
delete chat.messages
for(const item of msgs) {
for (const item of msgs) {
const message = item.message! as WAMessage
messages.push(message)
if(!chat.messages?.length) {
// Source 3: Extract LID-PN mapping from message's alternative JID fields
// Messages may have remoteJidAlt or participantAlt with alternate format
const messageMapping = extractLidPnFromMessage(
message.key.remoteJid,
message.key.remoteJidAlt,
message.key.participant,
message.key.participantAlt
)
if (messageMapping) {
addLidPnMapping(messageMapping)
}
if (!chat.messages?.length) {
// keep only the most recent message in the chat array
chat.messages = [{ message }]
}
if(!message.key.fromMe && !chat.lastMessageRecvTimestamp) {
if (!message.key.fromMe && !chat.lastMessageRecvTimestamp) {
chat.lastMessageRecvTimestamp = toNumber(message.messageTimestamp)
}
if(
if (
(message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_BSP ||
message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_FB) &&
message.messageStubParameters?.[0]
@@ -211,17 +323,6 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
verifiedName: message.messageStubParameters?.[0]
})
}
// Extract LID-PN mapping from message (remoteJidAlt/participantAlt fields)
const msgMapping = extractLidPnFromMessage(
message.key.remoteJid,
(message.key as { remoteJidAlt?: string }).remoteJidAlt,
message.key.participant,
(message.key as { participantAlt?: string }).participantAlt
)
if(msgMapping) {
lidPnMappings.push(msgMapping)
}
}
chats.push({ ...chat })
@@ -236,6 +337,9 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
break
}
// Convert Map back to array for return
const lidPnMappings = Array.from(lidPnMap.values())
return {
chats,
contacts,
@@ -246,6 +350,13 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
}
}
/**
* Downloads and processes a history sync notification in one step.
*
* @param msg - The history sync notification message
* @param options - Request options for the download
* @returns Processed history data
*/
export const downloadAndProcessHistorySyncNotification = async (
msg: proto.Message.IHistorySyncNotification,
options: RequestInit
@@ -260,6 +371,12 @@ export const downloadAndProcessHistorySyncNotification = async (
return processHistoryMessage(historyMsg)
}
/**
* Extracts the history sync notification from a protocol message.
*
* @param message - The protocol message to check
* @returns The history sync notification if present, undefined otherwise
*/
export const getHistoryMsg = (message: proto.IMessage) => {
const normalizedContent = !!message ? normalizeMessageContent(message) : undefined
const anyHistoryMsg = normalizedContent?.protocolMessage?.historySyncNotification!
+655 -272
View File
@@ -1,11 +1,367 @@
import { proto } from '../../../WAProto/index.js'
import {
processHistoryMessage,
extractLidPnFromConversation,
extractLidPnFromMessage,
isPersonJid,
processHistoryMessage
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', () => {
@@ -111,7 +467,6 @@ describe('processHistoryMessage', () => {
const result = processHistoryMessage(historySync)
// Should include mapping extracted from conversation
expect(result.lidPnMappings).toContainEqual({
lid: '11111111111111@lid',
pn: '1234567890123@s.whatsapp.net'
@@ -133,287 +488,315 @@ describe('processHistoryMessage', () => {
const result = processHistoryMessage(historySync)
// Should NOT include mapping from group conversation
expect(result.lidPnMappings).toEqual([])
})
})
})
describe('isPersonJid', () => {
it('should return true for LID users', () => {
expect(isPersonJid('11111111111111@lid')).toBe(true)
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([])
})
})
it('should return true for hosted LID users', () => {
expect(isPersonJid('11111111111111@hosted.lid')).toBe(true)
})
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
it('should return true for PN users (s.whatsapp.net)', () => {
expect(isPersonJid('1234567890123@s.whatsapp.net')).toBe(true)
})
const result = processHistoryMessage(historySync)
it('should return true for hosted PN users', () => {
expect(isPersonJid('1234567890123@hosted')).toBe(true)
})
expect(result.lidPnMappings).toContainEqual({
lid: '11111111111111@lid',
pn: '5511999999999@s.whatsapp.net'
})
})
it('should return false for groups', () => {
expect(isPersonJid('123456789@g.us')).toBe(false)
})
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
it('should return false for broadcasts', () => {
expect(isPersonJid('status@broadcast')).toBe(false)
})
const result = processHistoryMessage(historySync)
it('should return false for newsletters', () => {
expect(isPersonJid('123456789@newsletter')).toBe(false)
})
expect(result.lidPnMappings).toContainEqual({
lid: '11111111111111@lid',
pn: '5511999999999@s.whatsapp.net'
})
})
it('should return false for undefined', () => {
expect(isPersonJid(undefined)).toBe(false)
})
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
it('should return false for empty string', () => {
expect(isPersonJid('')).toBe(false)
})
})
describe('extractLidPnFromConversation', () => {
it('should extract mapping when chat is PN and lidJid is provided', () => {
const result = extractLidPnFromConversation(
'1234567890123@s.whatsapp.net',
'11111111111111@lid',
null
)
expect(result).toEqual({
lid: '11111111111111@lid',
pn: '1234567890123@s.whatsapp.net'
})
})
it('should extract mapping when chat is LID and pnJid is provided', () => {
const result = extractLidPnFromConversation(
'11111111111111@lid',
null,
'1234567890123@s.whatsapp.net'
)
expect(result).toEqual({
lid: '11111111111111@lid',
pn: '1234567890123@s.whatsapp.net'
})
})
it('should return undefined for group chats', () => {
const result = extractLidPnFromConversation(
'123456789@g.us',
'11111111111111@lid',
'1234567890123@s.whatsapp.net'
)
expect(result).toBeUndefined()
})
it('should return undefined for broadcast chats', () => {
const result = extractLidPnFromConversation(
'status@broadcast',
'11111111111111@lid',
'1234567890123@s.whatsapp.net'
)
expect(result).toBeUndefined()
})
it('should return undefined for newsletter chats', () => {
const result = extractLidPnFromConversation(
'123456789@newsletter',
'11111111111111@lid',
'1234567890123@s.whatsapp.net'
)
expect(result).toBeUndefined()
})
it('should return undefined when no mapping data is provided', () => {
const result = extractLidPnFromConversation(
'1234567890123@s.whatsapp.net',
null,
null
)
expect(result).toBeUndefined()
})
})
describe('extractLidPnFromMessage', () => {
it('should extract mapping when primary is LID and alt is PN', () => {
const result = extractLidPnFromMessage(
'11111111111111@lid',
'1234567890123@s.whatsapp.net',
null,
null
)
expect(result).toEqual({
lid: '11111111111111@lid',
pn: '1234567890123@s.whatsapp.net'
})
})
it('should extract mapping when primary is PN and alt is LID', () => {
const result = extractLidPnFromMessage(
'1234567890123@s.whatsapp.net',
'11111111111111@lid',
null,
null
)
expect(result).toEqual({
lid: '11111111111111@lid',
pn: '1234567890123@s.whatsapp.net'
})
})
it('should prefer participant over remoteJid', () => {
const result = extractLidPnFromMessage(
'11111111111111@lid',
'22222222222222@lid',
'33333333333333@lid',
'1234567890123@s.whatsapp.net'
)
// participant (33333333333333@lid) and participantAlt (1234567890123@s.whatsapp.net) should be used
expect(result).toEqual({
lid: '33333333333333@lid',
pn: '1234567890123@s.whatsapp.net'
})
})
it('should return undefined when primaryJid is missing', () => {
const result = extractLidPnFromMessage(
null,
'1234567890123@s.whatsapp.net',
null,
null
)
expect(result).toBeUndefined()
})
it('should return undefined when altJid is missing', () => {
const result = extractLidPnFromMessage(
'11111111111111@lid',
null,
null,
null
)
expect(result).toBeUndefined()
})
// CRITICAL: Edge cases for the && vs || fix
describe('edge cases for mixed JID types (bug fix validation)', () => {
it('should return undefined when primary is person but alt is GROUP', () => {
const result = extractLidPnFromMessage(
'11111111111111@lid',
'123456789@g.us', // group JID
null,
null
)
// 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(
'11111111111111@lid',
'status@broadcast', // broadcast JID
null,
null
)
expect(result).toBeUndefined()
})
it('should return undefined when primary is person but alt is NEWSLETTER', () => {
const result = extractLidPnFromMessage(
'11111111111111@lid',
'123456789@newsletter', // newsletter JID
null,
null
)
expect(result).toBeUndefined()
})
it('should return undefined when alt is person but primary is GROUP', () => {
const result = extractLidPnFromMessage(
'123456789@g.us', // group JID
'1234567890123@s.whatsapp.net',
null,
null
)
expect(result).toBeUndefined()
})
it('should return undefined when alt is person but primary is BROADCAST', () => {
const result = extractLidPnFromMessage(
'status@broadcast', // broadcast JID
'1234567890123@s.whatsapp.net',
null,
null
)
expect(result).toBeUndefined()
})
it('should return undefined when alt is person but primary is NEWSLETTER', () => {
const result = extractLidPnFromMessage(
'123456789@newsletter', // newsletter JID
'1234567890123@s.whatsapp.net',
null,
null
)
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
null,
null
)
expect(result).toBeUndefined()
})
})
it('should return undefined when both are same type (both LID)', () => {
const result = extractLidPnFromMessage(
'11111111111111@lid',
'22222222222222@lid',
null,
null
)
// Both are LID, no PN - can't determine mapping
expect(result).toBeUndefined()
})
it('should return undefined when both are same type (both PN)', () => {
const result = extractLidPnFromMessage(
'1234567890123@s.whatsapp.net',
'9876543210987@s.whatsapp.net',
null,
null
)
// Both are PN, no LID - can't determine mapping
expect(result).toBeUndefined()
const result = processHistoryMessage(historySync)
expect(result.lidPnMappings).toHaveLength(1)
expect(result.lidPnMappings[0]).toEqual({
lid: '11111111111111@lid',
pn: '5511999999999@s.whatsapp.net'
})
})
})
})