feat(signal): better parity with whatsapp web (#1967)

This commit is contained in:
vini
2025-11-19 10:20:49 -03:00
committed by GitHub
parent 3be21f8b99
commit a70e4da1c4
3 changed files with 78 additions and 11 deletions
+15 -3
View File
@@ -108,6 +108,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
useClones: false useClones: false
}) })
// Debounce identity-change session refreshes per JID to avoid bursts
const identityAssertDebounce = new NodeCache<boolean>({ stdTTL: 5, useClones: false })
let sendActiveReceipts = false let sendActiveReceipts = false
const fetchMessageHistory = async ( const fetchMessageHistory = async (
@@ -546,8 +549,17 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const identityNode = getBinaryNodeChild(node, 'identity') const identityNode = getBinaryNodeChild(node, 'identity')
if (identityNode) { if (identityNode) {
logger.info({ jid: from }, 'identity changed') logger.info({ jid: from }, 'identity changed')
// not handling right now if (identityAssertDebounce.get(from!)) {
// signal will override new identity anyway logger.debug({ jid: from }, 'skipping identity assert (debounced)')
return
}
identityAssertDebounce.set(from!, true)
try {
await assertSessions([from!], true)
} catch (error) {
logger.warn({ error, jid: from }, 'failed to assert sessions after identity change')
}
} else { } else {
logger.info({ node }, 'unknown encrypt notification') logger.info({ node }, 'unknown encrypt notification')
} }
@@ -1007,7 +1019,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
} }
} }
await assertSessions([participant]) await assertSessions([participant], true)
if (isJidGroup(remoteJid)) { if (isJidGroup(remoteJid)) {
await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } }) await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } })
+18 -7
View File
@@ -299,6 +299,16 @@ export const makeMessagesSocket = (config: SocketConfig) => {
if (lidResults.length > 0) { if (lidResults.length > 0) {
logger.trace('Storing LID maps from device call') logger.trace('Storing LID maps from device call')
await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid as string, pn: a.id }))) await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid as string, pn: a.id })))
// Force-refresh sessions for newly mapped LIDs to align identity addressing
try {
const lids = lidResults.map(a => a.lid as string)
if (lids.length) {
await assertSessions(lids, true)
}
} catch (e) {
logger.warn({ e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
}
} }
const extracted = extractDeviceJids( const extracted = extractDeviceJids(
@@ -373,7 +383,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
return deviceResults return deviceResults
} }
const assertSessions = async (jids: string[]) => { const assertSessions = async (jids: string[], force?: boolean) => {
let didFetchNewSession = false let didFetchNewSession = false
const uniqueJids = [...new Set(jids)] // Deduplicate JIDs const uniqueJids = [...new Set(jids)] // Deduplicate JIDs
const jidsRequiringFetch: string[] = [] const jidsRequiringFetch: string[] = []
@@ -385,14 +395,14 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const signalId = signalRepository.jidToSignalProtocolAddress(jid) const signalId = signalRepository.jidToSignalProtocolAddress(jid)
const cachedSession = peerSessionsCache.get(signalId) const cachedSession = peerSessionsCache.get(signalId)
if (cachedSession !== undefined) { if (cachedSession !== undefined) {
if (cachedSession) { if (cachedSession && !force) {
continue // Session exists in cache continue // Session exists in cache
} }
} else { } else {
const sessionValidation = await signalRepository.validateSession(jid) const sessionValidation = await signalRepository.validateSession(jid)
const hasSession = sessionValidation.exists const hasSession = sessionValidation.exists
peerSessionsCache.set(signalId, hasSession) peerSessionsCache.set(signalId, hasSession)
if (hasSession) { if (hasSession && !force) {
continue continue
} }
} }
@@ -423,10 +433,11 @@ export const makeMessagesSocket = (config: SocketConfig) => {
{ {
tag: 'key', tag: 'key',
attrs: {}, attrs: {},
content: wireJids.map(jid => ({ content: wireJids.map(jid => {
tag: 'user', const attrs: { [key: string]: string } = { jid }
attrs: { jid } if (force) attrs.reason = 'identity'
})) return { tag: 'user', attrs }
})
} }
] ]
}) })
+45 -1
View File
@@ -30,7 +30,9 @@ import {
getNextPreKeysNode, getNextPreKeysNode,
makeEventBuffer, makeEventBuffer,
makeNoiseHandler, makeNoiseHandler,
promiseTimeout promiseTimeout,
signedKeyPair,
xmppSignedPreKey
} from '../Utils' } from '../Utils'
import { getPlatformId } from '../Utils/browser-utils' import { getPlatformId } from '../Utils/browser-utils'
import { import {
@@ -204,6 +206,39 @@ export const makeSocket = (config: SocketConfig) => {
return result return result
} }
// Validate current key-bundle on server; on failure, trigger pre-key upload and rethrow
const digestKeyBundle = async (): Promise<void> => {
const res = await query({
tag: 'iq',
attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'encrypt' },
content: [{ tag: 'digest', attrs: {} }]
})
const digestNode = getBinaryNodeChild(res, 'digest')
if (!digestNode) {
await uploadPreKeys()
throw new Error('encrypt/get digest returned no digest node')
}
}
// Rotate our signed pre-key on server; on failure, run digest as fallback and rethrow
const rotateSignedPreKey = async (): Promise<void> => {
const newId = (creds.signedPreKey.keyId || 0) + 1
const skey = await signedKeyPair(creds.signedIdentityKey, newId)
await query({
tag: 'iq',
attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'encrypt' },
content: [
{
tag: 'rotate',
attrs: {},
content: [xmppSignedPreKey(skey)]
}
]
})
// Persist new signed pre-key in creds
ev.emit('creds.update', { signedPreKey: skey })
}
const executeUSyncQuery = async (usyncQuery: USyncQuery) => { const executeUSyncQuery = async (usyncQuery: USyncQuery) => {
if (usyncQuery.protocols.length === 0) { if (usyncQuery.protocols.length === 0) {
throw new Boom('USyncQuery must have at least one protocol') throw new Boom('USyncQuery must have at least one protocol')
@@ -869,6 +904,13 @@ export const makeSocket = (config: SocketConfig) => {
try { try {
await uploadPreKeysToServerIfRequired() await uploadPreKeysToServerIfRequired()
await sendPassiveIq('active') await sendPassiveIq('active')
// After successful login, validate our key-bundle against server
try {
await digestKeyBundle()
} catch (e) {
logger.warn({ e }, 'failed to run digest after login')
}
} catch (err) { } catch (err) {
logger.warn({ err }, 'failed to send initial passive iq') logger.warn({ err }, 'failed to send initial passive iq')
} }
@@ -1007,6 +1049,8 @@ export const makeSocket = (config: SocketConfig) => {
onUnexpectedError, onUnexpectedError,
uploadPreKeys, uploadPreKeys,
uploadPreKeysToServerIfRequired, uploadPreKeysToServerIfRequired,
digestKeyBundle,
rotateSignedPreKey,
requestPairingCode, requestPairingCode,
wamBuffer: publicWAMBuffer, wamBuffer: publicWAMBuffer,
/** Waits for the connection to WA to reach a state */ /** Waits for the connection to WA to reach a state */