feat: centralized LID→PN normalization for all emitted events (#246)
* feat: centralized LID→PN normalization for all emitted events WhatsApp's server increasingly uses LID (Linked ID) as the primary addressing format. Consumers expect phone numbers (PN), not opaque LID identifiers. This adds comprehensive LID→PN resolution across all ev.emit() paths so downstream consumers always receive PN. Key changes: - Add resolveLidToPn() and normalizeKeyLidToPn() centralized helpers in process-message.ts for consistent LID→PN resolution - normalizeMessageJids() fast path: use alt JID directly from stanza attributes (zero I/O, eliminates race condition with LIDMappingStore) - Await storeLIDPNMappings() before normalization in message receipt flow (was fire-and-forget, could race with subsequent getPNForLID) - Normalize LID→PN in all event emission points: * messages.upsert (keys, nested reaction/poll keys, participantAlt) * presence.update (jid + participant) * message-receipt.update (key + userJid) * contacts.update (picture notifications) * blocklist.update (blocklist JIDs) * call events (chatId, from) * group metadata (participants, owner, subjectOwner) * group notifications (acting participant, add/remove/promote/demote) * newsletter notifications (author, user JIDs) * sync actions (mutation index normalization) - Make handlePresenceUpdate and handleGroupNotification async to support await on LID resolution - Add normalizeGroupMetadata() helper in groups.ts for all extractGroupMetadata call sites Performance: ~0.01ms per message (LRU cache hit). No impact on message sending. First-contact resolution ~2-5ms (one-time per contact). * fix: normalize LID→PN in handleBadAck, media retry, and PDO recovery Additional leak points found during final audit: - handleBadAck: key.remoteJid from ack stanza could be LID - messages.media-update: media retry key JIDs not normalized - CTWA PDO recovery: webMessageInfo.key from phone response not normalized * fix: address PR review — parallelize LID resolution, use helper consistently - normalizeGroupMetadata: resolve participant LIDs with Promise.all instead of sequential loop (perf on large groups) - handleCall: resolve participant JIDs with Promise.all - handleBadAck: use normalizeKeyLidToPn() helper instead of manual resolveLidToPn + assignment (consistency with other code paths) - CB:relay: resolve callCreator LID→PN before emitting
This commit is contained in:
+40
-13
@@ -50,7 +50,8 @@ import {
|
||||
isMissingKeyError,
|
||||
MAX_SYNC_ATTEMPTS,
|
||||
newLTHashState,
|
||||
processSyncAction
|
||||
processSyncAction,
|
||||
resolveLidToPn
|
||||
} from '../Utils'
|
||||
import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex'
|
||||
import processMessage from '../Utils/process-message'
|
||||
@@ -757,9 +758,16 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}, authState?.creds?.me?.id || 'resync-app-state')
|
||||
|
||||
const { onMutation } = newAppStateChunkHandler(isInitialSync)
|
||||
const lidMapping = signalRepository.lidMapping
|
||||
for (const key in globalMutationMap) {
|
||||
const mutation = globalMutationMap[key]
|
||||
if (!mutation) continue
|
||||
// Normalize LID→PN in sync action index[1] (chat/contact ID)
|
||||
if (mutation.index[1] && isAnyLidUser(mutation.index[1])) {
|
||||
const resolved = await resolveLidToPn(mutation.index[1], lidMapping, logger)
|
||||
if (resolved) mutation.index[1] = resolved
|
||||
}
|
||||
|
||||
onMutation(mutation)
|
||||
}
|
||||
}
|
||||
@@ -912,16 +920,16 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handlePresenceUpdate = ({ tag, attrs, content }: BinaryNode) => {
|
||||
const handlePresenceUpdate = async ({ tag, attrs, content }: BinaryNode) => {
|
||||
let presence: PresenceData | undefined
|
||||
const jid = attrs.from
|
||||
const participant = attrs.participant || attrs.from
|
||||
if (!jid) {
|
||||
const rawJid = attrs.from
|
||||
const rawParticipant = attrs.participant || attrs.from
|
||||
if (!rawJid) {
|
||||
logger.warn({ attrs }, 'handlePresenceUpdate: jid (attrs.from) is missing, skipping')
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldIgnoreJid(jid) && jid !== S_WHATSAPP_NET) {
|
||||
if (shouldIgnoreJid(rawJid) && rawJid !== S_WHATSAPP_NET) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -933,7 +941,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
} else if (Array.isArray(content)) {
|
||||
const [firstChild] = content
|
||||
if (!firstChild) {
|
||||
logger.warn({ jid }, 'handlePresenceUpdate: firstChild content is empty, skipping')
|
||||
logger.warn({ jid: rawJid }, 'handlePresenceUpdate: firstChild content is empty, skipping')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -952,12 +960,19 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
if (presence) {
|
||||
if (!participant) {
|
||||
logger.warn({ jid }, 'handlePresenceUpdate: participant is missing, skipping')
|
||||
if (!rawParticipant) {
|
||||
logger.warn({ jid: rawJid }, 'handlePresenceUpdate: participant is missing, skipping')
|
||||
return
|
||||
}
|
||||
|
||||
ev.emit('presence.update', { id: jid, presences: { [participant]: presence } })
|
||||
// Resolve LID→PN so consumers always see phone-number JIDs
|
||||
const lidMapping = signalRepository.lidMapping
|
||||
const [jid, participant] = await Promise.all([
|
||||
resolveLidToPn(rawJid, lidMapping, logger),
|
||||
resolveLidToPn(rawParticipant, lidMapping, logger)
|
||||
])
|
||||
|
||||
ev.emit('presence.update', { id: jid!, presences: { [participant!]: presence } })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1031,8 +1046,16 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
undefined,
|
||||
logger
|
||||
)
|
||||
const lidMapping = signalRepository.lidMapping
|
||||
for (const key in mutationMap) {
|
||||
onMutation(mutationMap[key]!)
|
||||
const mutation = mutationMap[key]!
|
||||
// Normalize LID→PN in sync action index[1] (chat/contact ID)
|
||||
if (mutation.index[1] && isAnyLidUser(mutation.index[1])) {
|
||||
const resolved = await resolveLidToPn(mutation.index[1], lidMapping, logger)
|
||||
if (resolved) mutation.index[1] = resolved
|
||||
}
|
||||
|
||||
onMutation(mutation)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1376,8 +1399,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('CB:presence', handlePresenceUpdate)
|
||||
ws.on('CB:chatstate', handlePresenceUpdate)
|
||||
ws.on('CB:presence', (node: BinaryNode) => {
|
||||
handlePresenceUpdate(node).catch(err => onUnexpectedError(err, 'handling presence update'))
|
||||
})
|
||||
ws.on('CB:chatstate', (node: BinaryNode) => {
|
||||
handlePresenceUpdate(node).catch(err => onUnexpectedError(err, 'handling chatstate update'))
|
||||
})
|
||||
|
||||
ws.on('CB:ib,,dirty', async (node: BinaryNode) => {
|
||||
const { attrs } = getBinaryNodeChild(node, 'dirty')!
|
||||
|
||||
Reference in New Issue
Block a user