diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index 94e64830..023e804d 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -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')! diff --git a/src/Socket/groups.ts b/src/Socket/groups.ts index 33194e27..56a2a0b1 100644 --- a/src/Socket/groups.ts +++ b/src/Socket/groups.ts @@ -1,7 +1,7 @@ import { proto } from '../../WAProto/index.js' import type { GroupMetadata, GroupParticipant, ParticipantAction, SocketConfig, WAMessageKey } from '../Types' import { WAMessageAddressingMode, WAMessageStubType } from '../Types' -import { generateMessageIDV2, unixTimestampSeconds } from '../Utils' +import { generateMessageIDV2, resolveLidToPn, unixTimestampSeconds } from '../Utils' import { type BinaryNode, getBinaryNodeChild, @@ -17,6 +17,41 @@ import { makeChatsSocket } from './chats' export const makeGroupsSocket = (config: SocketConfig) => { const sock = makeChatsSocket(config) const { authState, ev, query, upsertMessage } = sock + const { signalRepository } = sock + const { logger } = config + + /** Normalize group metadata participant IDs from LID to PN */ + const normalizeGroupMetadata = async (metadata: GroupMetadata): Promise => { + const lidMapping = signalRepository.lidMapping + for (const p of metadata.participants) { + if (isLidUser(p.id)) { + if (p.phoneNumber) { + // Inline PN available — use it and preserve LID + p.lid = p.id + p.id = p.phoneNumber + } else { + const resolved = await resolveLidToPn(p.id, lidMapping, logger) + if (resolved && resolved !== p.id) { + p.lid = p.id + p.id = resolved + } + } + } + } + + // Normalize owner/subjectOwner if LID + if (metadata.owner && isLidUser(metadata.owner)) { + const resolved = metadata.ownerPn || await resolveLidToPn(metadata.owner, lidMapping, logger) + if (resolved) metadata.owner = resolved + } + + if (metadata.subjectOwner && isLidUser(metadata.subjectOwner)) { + const resolved = metadata.subjectOwnerPn || await resolveLidToPn(metadata.subjectOwner, lidMapping, logger) + if (resolved) metadata.subjectOwner = resolved + } + + return metadata + } const groupQuery = async (jid: string, type: 'get' | 'set', content: BinaryNode[]) => query({ @@ -31,7 +66,7 @@ export const makeGroupsSocket = (config: SocketConfig) => { const groupMetadata = async (jid: string) => { const result = await groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }]) - return extractGroupMetadata(result) + return normalizeGroupMetadata(extractGroupMetadata(result)) } const groupFetchAllParticipating = async () => { @@ -58,16 +93,15 @@ export const makeGroupsSocket = (config: SocketConfig) => { if (groupsChild) { const groups = getBinaryNodeChildren(groupsChild, 'group') for (const groupNode of groups) { - const meta = extractGroupMetadata({ + const meta = await normalizeGroupMetadata(extractGroupMetadata({ tag: 'result', attrs: {}, content: [groupNode] - }) + })) data[meta.id] = meta } } - // TODO: properly parse LID / PN DATA sock.ev.emit('groups.update', Object.values(data)) return data @@ -101,7 +135,7 @@ export const makeGroupsSocket = (config: SocketConfig) => { })) } ]) - return extractGroupMetadata(result) + return normalizeGroupMetadata(extractGroupMetadata(result)) }, groupLeave: async (id: string) => { await groupQuery('@g.us', 'set', [ @@ -277,7 +311,7 @@ export const makeGroupsSocket = (config: SocketConfig) => { ), groupGetInviteInfo: async (code: string) => { const results = await groupQuery('@g.us', 'get', [{ tag: 'invite', attrs: { code } }]) - return extractGroupMetadata(results) + return normalizeGroupMetadata(extractGroupMetadata(results)) }, groupToggleEphemeral: async (jid: string, ephemeralExpiration: number) => { const content: BinaryNode = ephemeralExpiration diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index a2507639..c097d88c 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -49,7 +49,9 @@ import { MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, + normalizeKeyLidToPn, normalizeMessageJids, + resolveLidToPn, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, @@ -365,10 +367,14 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { case 'NotificationNewsletterAdminPromote': for (const update of updates) { if (update.jid && update.user) { + const [resolvedAuthor, resolvedUser] = await Promise.all([ + resolveLidToPn(node.attrs.from!, signalRepository.lidMapping, logger), + resolveLidToPn(update.user, signalRepository.lidMapping, logger) + ]) ev.emit('newsletter-participants.update', { id: update.jid, - author: node.attrs.from!, - user: update.user, + author: resolvedAuthor || node.attrs.from!, + user: resolvedUser || update.user, new_role: 'ADMIN', action: 'promote' }) @@ -387,7 +393,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const handleNewsletterNotification = async (node: BinaryNode) => { const from = node.attrs.from! const child = getAllBinaryNodeChildren(node)[0]! - const author = node.attrs.participant! + const rawAuthor = node.attrs.participant! + // Resolve author LID→PN (participant is a user JID that could be LID) + const author = await resolveLidToPn(rawAuthor, signalRepository.lidMapping, logger) || rawAuthor logger.info({ from, child }, 'got newsletter notification') @@ -414,10 +422,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { break case 'participant': + const resolvedParticipantUser = await resolveLidToPn(child.attrs.jid!, signalRepository.lidMapping, logger) || child.attrs.jid! const participantUpdate = { id: from, author, - user: child.attrs.jid!, + user: resolvedParticipantUser, action: child.attrs.action!, new_role: child.attrs.role! } @@ -1406,8 +1415,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } } - const handleGroupNotification = (fullNode: BinaryNode, child: BinaryNode, msg: Partial) => { - // TODO: Support PN/LID (Here is only LID now) + const handleGroupNotification = async (fullNode: BinaryNode, child: BinaryNode, msg: Partial) => { + const lidMapping = signalRepository.lidMapping const actingParticipantLid = fullNode.attrs.participant const actingParticipantPn = fullNode.attrs.participant_pn @@ -1415,13 +1424,52 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid! const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn! + // Resolve acting participant to PN — prefer inline PN, fall back to LID→PN resolution + const actingParticipant = actingParticipantPn + || await resolveLidToPn(actingParticipantLid, lidMapping, logger) + + // Resolve affected participant to PN + const affectedParticipant = affectedParticipantPn + || await resolveLidToPn(affectedParticipantLid, lidMapping, logger) + + // Store LID↔PN mappings from notification attributes + const mappingsToStore: Array<{ lid: string; pn: string }> = [] + if (actingParticipantLid && actingParticipantPn && isLidUser(actingParticipantLid) && isPnUser(actingParticipantPn)) { + mappingsToStore.push({ lid: actingParticipantLid, pn: actingParticipantPn }) + } + switch (child?.tag) { case 'create': const metadata = extractGroupMetadata(child) + // Normalize group metadata participant IDs to PN + for (const p of metadata.participants) { + if (isLidUser(p.id)) { + // Use inline phoneNumber if available, otherwise resolve via mapping + if (p.phoneNumber) { + // Store the mapping + mappingsToStore.push({ lid: p.id, pn: p.phoneNumber }) + p.lid = p.id + p.id = p.phoneNumber + } else { + const resolved = await resolveLidToPn(p.id, lidMapping, logger) + if (resolved && resolved !== p.id) { + p.lid = p.id + p.id = resolved + } + } + } + } + + // Resolve metadata owner to PN + if (metadata.owner && isLidUser(metadata.owner)) { + const resolvedOwner = metadata.ownerPn || await resolveLidToPn(metadata.owner, lidMapping, logger) + if (resolvedOwner) metadata.owner = resolvedOwner + } + msg.messageStubType = WAMessageStubType.GROUP_CREATE msg.messageStubParameters = [metadata.subject] - msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn } + msg.key = { participant: actingParticipant } ev.emit('chats.upsert', [ { @@ -1433,8 +1481,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { ev.emit('groups.upsert', [ { ...metadata, - author: actingParticipantLid, - authorPn: actingParticipantPn + author: actingParticipant } ]) break @@ -1448,8 +1495,13 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } break case 'modify': - const oldNumber = getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid!) - msg.messageStubParameters = oldNumber || [] + const oldNumbers = await Promise.all( + getBinaryNodeChildren(child, 'participant').map(async p => { + const resolved = await resolveLidToPn(p.attrs.jid!, lidMapping, logger) + return resolved || p.attrs.jid! + }) + ) + msg.messageStubParameters = oldNumbers || [] msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER break case 'promote': @@ -1460,21 +1512,48 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}` msg.messageStubType = WAMessageStubType[stubType as keyof typeof WAMessageStubType] - const participants = getBinaryNodeChildren(child, 'participant').map(({ attrs }) => { - // TODO: Store LID MAPPINGS - return { - id: attrs.jid!, - phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined, - lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined, - admin: (attrs.type || null) as GroupParticipant['admin'] - } - }) + const participants = await Promise.all( + getBinaryNodeChildren(child, 'participant').map(async ({ attrs }) => { + const rawJid = attrs.jid! + let id = rawJid + let phoneNumber: string | undefined + let lid: string | undefined + + if (isLidUser(rawJid)) { + // Primary is LID — resolve to PN + phoneNumber = isPnUser(attrs.phone_number) ? attrs.phone_number : undefined + if (phoneNumber) { + mappingsToStore.push({ lid: rawJid, pn: phoneNumber }) + lid = rawJid + id = phoneNumber + } else { + const resolved = await resolveLidToPn(rawJid, lidMapping, logger) + if (resolved && resolved !== rawJid) { + lid = rawJid + id = resolved + } + } + } else if (isPnUser(rawJid) && isLidUser(attrs.lid)) { + // Primary is PN — store LID for reference + lid = attrs.lid + mappingsToStore.push({ lid: attrs.lid!, pn: rawJid }) + } + + return { + id, + phoneNumber, + lid, + admin: (attrs.type || null) as GroupParticipant['admin'] + } + }) + ) if ( participants.length === 1 && // if recv. "remove" message and sender removed themselves // mark as left - (areJidsSameUser(participants[0]!.id, actingParticipantLid) || + (areJidsSameUser(participants[0]!.id, actingParticipant) || + areJidsSameUser(participants[0]!.id, actingParticipantLid) || areJidsSameUser(participants[0]!.id, actingParticipantPn)) && child.tag === 'remove' ) { @@ -1525,21 +1604,27 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { case 'created_membership_requests': msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD msg.messageStubParameters = [ - JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }), + JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipant }), 'created', child.attrs.request_method! ] break case 'revoked_membership_requests': const isDenied = areJidsSameUser(affectedParticipantLid, actingParticipantLid) - // TODO: LIDMAPPING SUPPORT msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD msg.messageStubParameters = [ - JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }), + JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipant }), isDenied ? 'revoked' : 'rejected' ] break } + + // Persist any LID↔PN mappings discovered from notification attributes + if (mappingsToStore.length) { + await lidMapping.storeLIDPNMappings(mappingsToStore).catch(err => { + logger.warn({ err, count: mappingsToStore.length }, 'Failed to store LID↔PN mappings from group notification') + }) + } } const processNotification = async (node: BinaryNode) => { @@ -1556,8 +1641,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { await handleMexNewsletterNotification(node) break case 'w:gp2': - // TODO: HANDLE PARTICIPANT_PN - handleGroupNotification(node, child!, result) + await handleGroupNotification(node, child!, result) break case 'mediaretry': const event = decodeMediaRetryNode(node) @@ -1591,13 +1675,16 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const setPicture = getBinaryNodeChild(node, 'set') const delPicture = getBinaryNodeChild(node, 'delete') - // TODO: WAJIDHASH stuff proper support inhouse - ev.emit('contacts.update', [ - { - id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '', - imgUrl: setPicture ? 'changed' : 'removed' - } - ]) + { + const rawPictureJid = jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '' + const pictureJid = await resolveLidToPn(rawPictureJid, signalRepository.lidMapping, logger) || rawPictureJid + ev.emit('contacts.update', [ + { + id: pictureJid, + imgUrl: setPicture ? 'changed' : 'removed' + } + ]) + } if (isJidGroup(from)) { const node = setPicture || delPicture @@ -1635,7 +1722,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const blocklists = getBinaryNodeChildren(child, 'item') for (const { attrs } of blocklists) { - const blocklist = [attrs.jid!] + const resolvedBlockJid = await resolveLidToPn(attrs.jid!, signalRepository.lidMapping, logger) || attrs.jid! + const blocklist = [resolvedBlockJid] const type = attrs.action === 'block' ? 'add' : 'remove' ev.emit('blocklist.update', { blocklist, type }) } @@ -1927,6 +2015,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { participant: attrs.participant } + // Normalize LID→PN in receipt key so events always emit PN JIDs + const lidMapping = signalRepository.lidMapping + const [resolvedRemoteJid, resolvedParticipant] = await Promise.all([ + resolveLidToPn(key.remoteJid, lidMapping, logger), + resolveLidToPn(key.participant, lidMapping, logger) + ]) + if (resolvedRemoteJid) key.remoteJid = resolvedRemoteJid + if (resolvedParticipant) key.participant = resolvedParticipant + if (shouldIgnoreJid(remoteJid!) && remoteJid !== S_WHATSAPP_NET) { logger.trace({ remoteJid }, 'ignoring receipt from jid') await sendMessageAck(node) @@ -1953,12 +2050,13 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { if (attrs.participant) { const updateKey: keyof MessageUserReceipt = status === proto.WebMessageInfo.Status.DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp' + const resolvedReceiptUserJid = await resolveLidToPn(attrs.participant, lidMapping, logger) || jidNormalizedUser(attrs.participant) ev.emit( 'message-receipt.update', ids.map(id => ({ key: { ...key, id }, receipt: { - userJid: jidNormalizedUser(attrs.participant), + userJid: jidNormalizedUser(resolvedReceiptUserJid), [updateKey]: +attrs.t! } })) @@ -2087,10 +2185,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { // Check if mapping already exists to avoid unnecessary storage operations const existingMapping = await signalRepository.lidMapping.getPNForLID(alt) if (!existingMapping) { - // Store mapping in background (non-critical, doesn't block decrypt) - signalRepository.lidMapping + // MUST await: normalizeMessageJids() runs after this and needs the mapping + // in the LIDMappingStore to resolve LID→PN for events delivered to consumers + await signalRepository.lidMapping .storeLIDPNMappings([{ lid: alt, pn: primaryJid }]) - .catch(error => logger.warn({ error, alt, primaryJid }, 'Background LID mapping storage failed')) + .catch(error => logger.warn({ error, alt, primaryJid }, 'LID mapping storage failed')) } // CRITICAL: ALWAYS migrate session, even if mapping exists @@ -2103,10 +2202,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { // Check if reverse mapping exists const existingMapping = await signalRepository.lidMapping.getLIDForPN(alt) if (!existingMapping) { - // Store mapping in background (non-critical) - signalRepository.lidMapping + // MUST await: normalizeMessageJids() runs after this and needs the mapping + // in the LIDMappingStore to resolve LID→PN for events delivered to consumers + await signalRepository.lidMapping .storeLIDPNMappings([{ lid: primaryJid, pn: alt }]) - .catch(error => logger.warn({ error, alt, primaryJid }, 'Background LID mapping storage failed')) + .catch(error => logger.warn({ error, alt, primaryJid }, 'LID mapping storage failed')) } // CRITICAL: ALWAYS migrate session, even if mapping exists @@ -2607,6 +2707,26 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { await callOfferCache.del(call.id) } + // Normalize LID→PN in call event JIDs before emitting to consumers + const callLidMapping = signalRepository.lidMapping + const [resolvedChatId, resolvedFrom, resolvedLinkCreator] = await Promise.all([ + resolveLidToPn(call.chatId, callLidMapping, logger), + resolveLidToPn(call.from, callLidMapping, logger), + resolveLidToPn(call.linkCreator, callLidMapping, logger) + ]) + if (resolvedChatId) call.chatId = resolvedChatId + if (resolvedFrom) call.from = resolvedFrom + if (resolvedLinkCreator) call.linkCreator = resolvedLinkCreator + // Resolve participant JIDs + if (call.participants) { + for (const p of call.participants) { + if (p.jid) { + const resolved = p.userPn || await resolveLidToPn(p.jid, callLidMapping, logger) + if (resolved) p.jid = resolved + } + } + } + ev.emit('call', [call]) } @@ -2802,10 +2922,12 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { // Top-level stanzas carry TURN server info, tokens and crypto keys ws.on('CB:relay', async (node: BinaryNode) => { const callId = node.attrs['call-id'] - const callCreator = node.attrs['call-creator'] + const rawCallCreator = node.attrs['call-creator'] // Both callId and callCreator must be present to emit a valid event // (call link relays may arrive without these attrs — just log them) - if (callId && callCreator) { + if (callId && rawCallCreator) { + // Resolve LID→PN for call creator + const callCreator = await resolveLidToPn(rawCallCreator, signalRepository.lidMapping, logger) || rawCallCreator logger.debug( { callId, callCreator, uuid: node.attrs.uuid }, 'received relay info' diff --git a/src/Utils/process-message.ts b/src/Utils/process-message.ts index d369bd69..a2570111 100644 --- a/src/Utils/process-message.ts +++ b/src/Utils/process-message.ts @@ -19,10 +19,13 @@ import type { WAMessage, WAMessageKey } from '../Types' +import type { LIDMappingStore } from '../Signal/lid-mapping' import { WAMessageStubType } from '../Types' import { getContentType, normalizeMessageContent } from '../Utils/messages' import { areJidsSameUser, + isAnyLidUser, + isAnyPnUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, @@ -125,42 +128,94 @@ export const cleanMessage = (message: WAMessage, meId: string, meLid: string) => } } +/** + * Resolves a LID JID to its PN equivalent using the LID mapping store. + * Returns the original JID if it's not a LID or if no mapping is found. + * Safe to call with any JID type (group, newsletter, PN, etc.). + */ +export const resolveLidToPn = async ( + jid: string | undefined | null, + lidMapping: LIDMappingStore, + logger?: ILogger +): Promise => { + if (!jid) { + return undefined + } + + if (isAnyLidUser(jid)) { + const pn = await lidMapping.getPNForLID(jid) + if (pn) { + logger?.debug({ lid: jid, pn }, 'Resolved LID to PN') + } + + return pn || jid + } + + return jid +} + +/** + * Normalizes a WAMessageKey by resolving LID→PN for remoteJid and participant. + */ +export const normalizeKeyLidToPn = async ( + key: WAMessageKey, + lidMapping: LIDMappingStore, + logger?: ILogger +): Promise => { + const [resolvedRemoteJid, resolvedParticipant] = await Promise.all([ + resolveLidToPn(key.remoteJid, lidMapping, logger), + resolveLidToPn(key.participant, lidMapping, logger) + ]) + if (resolvedRemoteJid) { + key.remoteJid = resolvedRemoteJid + } + + if (resolvedParticipant) { + key.participant = resolvedParticipant + } +} + export const normalizeMessageJids = async ( message: WAMessage, signalRepository: SignalRepositoryWithLIDStore, logger?: ILogger ): Promise => { - const resolveLidToPn = async (jid: string | undefined | null): Promise => { - if (!jid) { - return undefined - } + const lidMapping = signalRepository.lidMapping + const key = message.key - if (isLidUser(jid) || isHostedLidUser(jid)) { - const pn = await signalRepository.lidMapping.getPNForLID(jid) - if (pn) { - logger?.debug({ lid: jid, pn }, 'Resolved LID to PN for inbound message') - } else { - logger?.debug({ lid: jid }, 'PN not found for inbound LID, keeping LID') - } - - return pn || jid - } - - return jid + // FAST PATH: Use alt JIDs directly when available (avoids LIDMappingStore race condition). + // The stanza always carries both formats (LID + PN) in the attributes. + // When addressing_mode=lid, remoteJid is LID and remoteJidAlt is PN. + // When addressing_mode=pn, remoteJid is already PN (no conversion needed). + if (key.remoteJid && isAnyLidUser(key.remoteJid) && key.remoteJidAlt && isAnyPnUser(key.remoteJidAlt)) { + logger?.debug({ lid: key.remoteJid, pn: key.remoteJidAlt }, 'Resolved remoteJid LID→PN via alt (fast path)') + key.remoteJid = key.remoteJidAlt } - // Execute both lookups in parallel instead of sequentially to reduce latency - const [resolvedRemoteJid, resolvedParticipant] = await Promise.all([ - resolveLidToPn(message.key.remoteJid), - resolveLidToPn(message.key.participant) - ]) - - if (resolvedRemoteJid) { - message.key.remoteJid = resolvedRemoteJid + if (key.participant && isAnyLidUser(key.participant) && key.participantAlt && isAnyPnUser(key.participantAlt)) { + logger?.debug({ lid: key.participant, pn: key.participantAlt }, 'Resolved participant LID→PN via alt (fast path)') + key.participant = key.participantAlt } - if (resolvedParticipant) { - message.key.participant = resolvedParticipant + // SLOW PATH: Resolve any remaining LIDs via LIDMappingStore lookup + await normalizeKeyLidToPn(key, lidMapping, logger) + + // Also normalize participantAlt (the alternative JID format — can be LID when addressing_mode=pn) + if (key.participantAlt && isAnyLidUser(key.participantAlt)) { + const resolved = await resolveLidToPn(key.participantAlt, lidMapping, logger) + if (resolved) { + key.participantAlt = resolved + } + } + + // Normalize nested message keys (reaction, poll) that may contain LID JIDs + const content = normalizeMessageContent(message.message) + if (content?.reactionMessage?.key) { + await normalizeKeyLidToPn(content.reactionMessage.key, lidMapping, logger) + } + + if (content?.pollUpdateMessage?.pollCreationMessageKey) { + await normalizeKeyLidToPn(content.pollUpdateMessage.pollCreationMessageKey, lidMapping, logger) } } @@ -674,9 +729,13 @@ const processMessage = async ( response: responseMsg } + // Normalize creationMsgKey JIDs for the emitted event + const normalizedCreationKey = { ...creationMsgKey } + await normalizeKeyLidToPn(normalizedCreationKey, signalRepository.lidMapping, logger) + ev.emit('messages.update', [ { - key: creationMsgKey, + key: normalizedCreationKey, update: { eventResponses: [eventResponse] } @@ -719,7 +778,7 @@ const processMessage = async ( }) } - const participantsIncludesMe = () => participants.find(jid => areJidsSameUser(meId, jid.phoneNumber)) // ADD SUPPORT FOR LID + const participantsIncludesMe = () => participants.find(p => areJidsSameUser(meId, p.id) || areJidsSameUser(meId, p.phoneNumber)) switch (message.messageStubType) { case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER: