feat(history): comprehensive LID-PN mapping with full JID format support
Enhances LID-PN mapping extraction to handle all WhatsApp JID formats and extract mappings from multiple sources within history sync data. New JID validation: - Add `isPersonJid()` helper to identify JIDs that can have LID-PN mappings - Skip non-person JIDs: @newsletter (channels), @broadcast, @g.us (groups) Triple-source extraction: 1. Top-level `phoneNumberToLidMappings` array (existing) 2. Conversation objects via `lidJid`/`pnJid` properties (existing) 3. Message objects via `remoteJidAlt`/`participantAlt` fields (NEW) New `extractLidPnFromMessage()` function: - Extracts mappings from message key alternative JID fields - Handles both direct messages (remoteJidAlt) and group messages (participantAlt) - Properly identifies LID vs PN based on server type JID formats handled: - @s.whatsapp.net - Standard phone number - @lid - Logical ID (privacy) - @hosted - Hosted phone number - @hosted.lid - Hosted LID 46 comprehensive tests covering all scenarios. Related: WhiskeySockets/Baileys#2263
This commit is contained in:
+129
-3
@@ -9,8 +9,12 @@ import { downloadContentFromMessage } from './messages-media'
|
||||
import {
|
||||
isHostedLidUser,
|
||||
isHostedPnUser,
|
||||
isJidBroadcast,
|
||||
isJidGroup,
|
||||
isJidNewsletter,
|
||||
isLidUser,
|
||||
isPnUser,
|
||||
jidDecode,
|
||||
jidNormalizedUser
|
||||
} from '../WABinary/index.js'
|
||||
|
||||
@@ -39,6 +43,32 @@ 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.
|
||||
*
|
||||
@@ -49,6 +79,11 @@ export const downloadHistory = async (msg: proto.Message.IHistorySyncNotificatio
|
||||
* 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
|
||||
@@ -63,12 +98,22 @@ export const downloadHistory = async (msg: proto.Message.IHistorySyncNotificatio
|
||||
* // 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
|
||||
@@ -93,16 +138,85 @@ export function extractLidPnFromConversation(
|
||||
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)
|
||||
*
|
||||
* @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
|
||||
}
|
||||
|
||||
// Skip non-person JIDs
|
||||
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 two sources:
|
||||
* 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 dual extraction ensures maximum mapping coverage, as WhatsApp may
|
||||
* provide mappings in either or both locations depending on the sync type.
|
||||
* 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
|
||||
@@ -172,6 +286,18 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
|
||||
const message = item.message! as WAMessage
|
||||
messages.push(message)
|
||||
|
||||
// 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 }]
|
||||
|
||||
@@ -1,5 +1,156 @@
|
||||
import { proto } from '../../../WAProto/index.js'
|
||||
import { processHistoryMessage, extractLidPnFromConversation } from '../../Utils/history'
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractLidPnFromConversation', () => {
|
||||
describe('LID chat with pnJid', () => {
|
||||
@@ -71,6 +222,26 @@ describe('extractLidPnFromConversation', () => {
|
||||
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',
|
||||
@@ -379,5 +550,144 @@ describe('processHistoryMessage', () => {
|
||||
{ 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)', () => {
|
||||
// Note: remoteJidAlt and participantAlt are runtime extensions to IMessageKey
|
||||
// defined in WAMessageKey type. We cast via unknown for test data.
|
||||
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)
|
||||
|
||||
// Should have only 1 mapping (deduplicated across all 3 sources)
|
||||
expect(result.lidPnMappings).toHaveLength(1)
|
||||
expect(result.lidPnMappings[0]).toEqual({
|
||||
lid: '11111111111111@lid',
|
||||
pn: '5511999999999@s.whatsapp.net'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user