Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5c07163f8 | |||
| 0a43ad849d | |||
| ac7bb321d4 |
+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')!
|
||||
|
||||
+43
-7
@@ -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,43 @@ 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<GroupMetadata> => {
|
||||
const lidMapping = signalRepository.lidMapping
|
||||
|
||||
// Resolve all participant LIDs in parallel for better performance on large groups
|
||||
await Promise.all(metadata.participants.map(async (p) => {
|
||||
if (isLidUser(p.id)) {
|
||||
if (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
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
// Normalize owner/subjectOwner if LID (parallel)
|
||||
const [resolvedOwner, resolvedSubjectOwner] = await Promise.all([
|
||||
metadata.owner && isLidUser(metadata.owner)
|
||||
? (metadata.ownerPn || resolveLidToPn(metadata.owner, lidMapping, logger))
|
||||
: null,
|
||||
metadata.subjectOwner && isLidUser(metadata.subjectOwner)
|
||||
? (metadata.subjectOwnerPn || resolveLidToPn(metadata.subjectOwner, lidMapping, logger))
|
||||
: null
|
||||
])
|
||||
if (resolvedOwner) metadata.owner = resolvedOwner
|
||||
if (resolvedSubjectOwner) metadata.subjectOwner = resolvedSubjectOwner
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
const groupQuery = async (jid: string, type: 'get' | 'set', content: BinaryNode[]) =>
|
||||
query({
|
||||
@@ -31,7 +68,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 +95,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 +137,7 @@ export const makeGroupsSocket = (config: SocketConfig) => {
|
||||
}))
|
||||
}
|
||||
])
|
||||
return extractGroupMetadata(result)
|
||||
return normalizeGroupMetadata(extractGroupMetadata(result))
|
||||
},
|
||||
groupLeave: async (id: string) => {
|
||||
await groupQuery('@g.us', 'set', [
|
||||
@@ -277,7 +313,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
|
||||
|
||||
+159
-34
@@ -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<WAMessage>) => {
|
||||
// TODO: Support PN/LID (Here is only LID now)
|
||||
const handleGroupNotification = async (fullNode: BinaryNode, child: BinaryNode, msg: Partial<WAMessage>) => {
|
||||
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
|
||||
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: attrs.jid!,
|
||||
phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
|
||||
lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
|
||||
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,11 +1641,12 @@ 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)
|
||||
// Normalize LID→PN in media retry key before emitting
|
||||
await normalizeKeyLidToPn(event.key, signalRepository.lidMapping, logger)
|
||||
ev.emit('messages.media-update', [event])
|
||||
break
|
||||
case 'encrypt':
|
||||
@@ -1591,13 +1677,16 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
const setPicture = getBinaryNodeChild(node, 'set')
|
||||
const delPicture = getBinaryNodeChild(node, 'delete')
|
||||
|
||||
// TODO: WAJIDHASH stuff proper support inhouse
|
||||
{
|
||||
const rawPictureJid = jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || ''
|
||||
const pictureJid = await resolveLidToPn(rawPictureJid, signalRepository.lidMapping, logger) || rawPictureJid
|
||||
ev.emit('contacts.update', [
|
||||
{
|
||||
id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
|
||||
id: pictureJid,
|
||||
imgUrl: setPicture ? 'changed' : 'removed'
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
if (isJidGroup(from)) {
|
||||
const node = setPicture || delPicture
|
||||
@@ -1635,7 +1724,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 +2017,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 +2052,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 +2187,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 +2204,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 +2709,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 in parallel
|
||||
if (call.participants) {
|
||||
await Promise.all(call.participants.map(async (p) => {
|
||||
if (p.jid) {
|
||||
const resolved = p.userPn || await resolveLidToPn(p.jid, callLidMapping, logger)
|
||||
if (resolved) p.jid = resolved
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
ev.emit('call', [call])
|
||||
}
|
||||
|
||||
@@ -2615,6 +2737,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
|
||||
const handleBadAck = async ({ attrs }: BinaryNode) => {
|
||||
const key: WAMessageKey = { remoteJid: attrs.from, fromMe: true, id: attrs.id }
|
||||
await normalizeKeyLidToPn(key, signalRepository.lidMapping, logger)
|
||||
|
||||
// WARNING: REFRAIN FROM ENABLING THIS FOR NOW. IT WILL CAUSE A LOOP
|
||||
// // current hypothesis is that if pash is sent in the ack
|
||||
@@ -2802,10 +2925,12 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
// Top-level <relay> 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'
|
||||
|
||||
@@ -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,22 +128,24 @@ export const cleanMessage = (message: WAMessage, meId: string, meLid: string) =>
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeMessageJids = async (
|
||||
message: WAMessage,
|
||||
signalRepository: SignalRepositoryWithLIDStore,
|
||||
/**
|
||||
* 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<void> => {
|
||||
const resolveLidToPn = async (jid: string | undefined | null): Promise<string | undefined> => {
|
||||
): Promise<string | undefined> => {
|
||||
if (!jid) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (isLidUser(jid) || isHostedLidUser(jid)) {
|
||||
const pn = await signalRepository.lidMapping.getPNForLID(jid)
|
||||
if (isAnyLidUser(jid)) {
|
||||
const pn = await 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')
|
||||
logger?.debug({ lid: jid, pn }, 'Resolved LID to PN')
|
||||
}
|
||||
|
||||
return pn || jid
|
||||
@@ -149,18 +154,68 @@ export const normalizeMessageJids = async (
|
||||
return jid
|
||||
}
|
||||
|
||||
// Execute both lookups in parallel instead of sequentially to reduce latency
|
||||
/**
|
||||
* Normalizes a WAMessageKey by resolving LID→PN for remoteJid and participant.
|
||||
*/
|
||||
export const normalizeKeyLidToPn = async (
|
||||
key: WAMessageKey,
|
||||
lidMapping: LIDMappingStore,
|
||||
logger?: ILogger
|
||||
): Promise<void> => {
|
||||
const [resolvedRemoteJid, resolvedParticipant] = await Promise.all([
|
||||
resolveLidToPn(message.key.remoteJid),
|
||||
resolveLidToPn(message.key.participant)
|
||||
resolveLidToPn(key.remoteJid, lidMapping, logger),
|
||||
resolveLidToPn(key.participant, lidMapping, logger)
|
||||
])
|
||||
|
||||
if (resolvedRemoteJid) {
|
||||
message.key.remoteJid = resolvedRemoteJid
|
||||
key.remoteJid = resolvedRemoteJid
|
||||
}
|
||||
|
||||
if (resolvedParticipant) {
|
||||
message.key.participant = resolvedParticipant
|
||||
key.participant = resolvedParticipant
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeMessageJids = async (
|
||||
message: WAMessage,
|
||||
signalRepository: SignalRepositoryWithLIDStore,
|
||||
logger?: ILogger
|
||||
): Promise<void> => {
|
||||
const lidMapping = signalRepository.lidMapping
|
||||
const key = message.key
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,6 +589,12 @@ const processMessage = async (
|
||||
'CTWA: Successfully recovered message via placeholder resend'
|
||||
)
|
||||
|
||||
// Normalize LID→PN in PDO-recovered message key before emitting
|
||||
// eslint-disable-next-line max-depth
|
||||
if (webMessageInfo.key && signalRepository) {
|
||||
await normalizeKeyLidToPn(webMessageInfo.key as WAMessageKey, signalRepository.lidMapping, logger)
|
||||
}
|
||||
|
||||
// wait till another upsert event is available, don't want it to be part of the PDO response message
|
||||
// TODO: parse through proper message handling utilities (to add relevant key fields)
|
||||
ev.emit('messages.upsert', {
|
||||
@@ -674,9 +735,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 +784,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:
|
||||
|
||||
Reference in New Issue
Block a user