Async + Multi Cache (#1741)

* fix: handle invalid signatureKeyPublic types in sender-key-state

- added extra check to ensure signatureKeyPublic is either a base64 string or a Buffer
- fallback to empty buffer when unexpected object type is received
- prevents ERR_INVALID_ARG_TYPE error in Buffer.from

* adjusted based on  @jlucaso1 recommmendation

* fix: handle chain key objects for skmsg group message decryption

previously, some sender chain keys were stored or retrieved as plain objects
(e.g. { '0': 85, '1': 100, ... }) instead of Buffers. this caused
"ERR_INVALID_ARG_TYPE" during initial decryption of skmsg group messages.

this fixes any chain key object is converted to a proper Buffer
before being passed to SenderChainKey.

* fix: add more robust checks and fallbacks

* feat: async cache

feat: async caching

* fix: linting issues

* fix: mget logic

---------

Co-authored-by: αѕтяσχ11 <devastro0010@gmail.com>
This commit is contained in:
Martín Schere
2025-09-14 16:13:11 +02:00
committed by GitHub
parent f0cc173aa3
commit 8029446511
7 changed files with 70 additions and 50 deletions
-1
View File
@@ -10,7 +10,6 @@ export class SenderChainKey {
constructor(iteration: number, chainKey: Uint8Array | Buffer) { constructor(iteration: number, chainKey: Uint8Array | Buffer) {
this.iteration = iteration this.iteration = iteration
if (Buffer.isBuffer(chainKey)) { if (Buffer.isBuffer(chainKey)) {
// backported from @MartinSchere's PR
this.chainKey = chainKey this.chainKey = chainKey
} else if (chainKey instanceof Uint8Array) { } else if (chainKey instanceof Uint8Array) {
this.chainKey = Buffer.from(chainKey) this.chainKey = Buffer.from(chainKey)
+18 -18
View File
@@ -400,7 +400,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
} else { } else {
// Fallback to old system // Fallback to old system
const key = `${msgId}:${msgKey?.participant}` const key = `${msgId}:${msgKey?.participant}`
let retryCount = msgRetryCache.get<number>(key) || 0 let retryCount = (await msgRetryCache.get<number>(key)) || 0
if (retryCount >= maxMsgRetryCount) { if (retryCount >= maxMsgRetryCount) {
logger.debug({ retryCount, msgId }, 'reached retry limit, clearing') logger.debug({ retryCount, msgId }, 'reached retry limit, clearing')
msgRetryCache.del(key) msgRetryCache.del(key)
@@ -408,11 +408,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
} }
retryCount += 1 retryCount += 1
msgRetryCache.set(key, retryCount) await msgRetryCache.set(key, retryCount)
} }
const key = `${msgId}:${msgKey?.participant}` const key = `${msgId}:${msgKey?.participant}`
const retryCount = msgRetryCache.get<number>(key) || 1 const retryCount = (await msgRetryCache.get<number>(key)) || 1
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
const fromJid = node.attrs.from! const fromJid = node.attrs.from!
@@ -862,16 +862,16 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
return data instanceof Buffer ? data : Buffer.from(data) return data instanceof Buffer ? data : Buffer.from(data)
} }
const willSendMessageAgain = (id: string, participant: string) => { const willSendMessageAgain = async (id: string, participant: string) => {
const key = `${id}:${participant}` const key = `${id}:${participant}`
const retryCount = msgRetryCache.get<number>(key) || 0 const retryCount = (await msgRetryCache.get<number>(key)) || 0
return retryCount <= maxMsgRetryCount return retryCount < maxMsgRetryCount
} }
const updateSendMessageAgainCount = (id: string, participant: string) => { const updateSendMessageAgainCount = async (id: string, participant: string) => {
const key = `${id}:${participant}` const key = `${id}:${participant}`
const newValue = (msgRetryCache.get<number>(key) || 0) + 1 const newValue = ((await msgRetryCache.get<number>(key)) || 0) + 1
msgRetryCache.set(key, newValue) await msgRetryCache.set(key, newValue)
} }
const sendMessagesAgain = async (key: proto.IMessageKey, ids: string[], retryNode: BinaryNode) => { const sendMessagesAgain = async (key: proto.IMessageKey, ids: string[], retryNode: BinaryNode) => {
@@ -950,7 +950,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
for (const [i, msg] of msgs.entries()) { for (const [i, msg] of msgs.entries()) {
if (!ids[i]) continue if (!ids[i]) continue
if (msg && willSendMessageAgain(ids[i], participant)) { if (msg && (await willSendMessageAgain(ids[i], participant))) {
updateSendMessageAgainCount(ids[i], participant) updateSendMessageAgainCount(ids[i], participant)
const msgRelayOpts: MessageRelayOptions = { messageId: ids[i] } const msgRelayOpts: MessageRelayOptions = { messageId: ids[i] }
@@ -1039,7 +1039,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// correctly set who is asking for the retry // correctly set who is asking for the retry
key.participant = key.participant || attrs.from key.participant = key.participant || attrs.from
const retryNode = getBinaryNodeChild(node, 'retry') const retryNode = getBinaryNodeChild(node, 'retry')
if (ids[0] && key.participant && willSendMessageAgain(ids[0], key.participant)) { if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
if (key.fromMe) { if (key.fromMe) {
try { try {
updateSendMessageAgainCount(ids[0], key.participant) updateSendMessageAgainCount(ids[0], key.participant)
@@ -1127,8 +1127,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
logger.debug('received unavailable message, acked and requested resend from phone') logger.debug('received unavailable message, acked and requested resend from phone')
} else { } else {
if (placeholderResendCache.get(node.attrs.id!)) { if (await placeholderResendCache.get(node.attrs.id!)) {
placeholderResendCache.del(node.attrs.id!) await placeholderResendCache.del(node.attrs.id!)
} }
} }
@@ -1292,7 +1292,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
status = getCallStatusFromNode(infoChild) status = getCallStatusFromNode(infoChild)
if (isLidUser(from) && infoChild.tag === 'relaylatency') { if (isLidUser(from) && infoChild.tag === 'relaylatency') {
const verify = callOfferCache.get(callId) const verify = await callOfferCache.get(callId)
if (!verify) { if (!verify) {
status = 'offer' status = 'offer'
const callLid: WACallEvent = { const callLid: WACallEvent = {
@@ -1303,7 +1303,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
offline: !!attrs.offline, offline: !!attrs.offline,
status status
} }
callOfferCache.set(callId, callLid) await callOfferCache.set(callId, callLid)
} }
} }
@@ -1320,10 +1320,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
call.isVideo = !!getBinaryNodeChild(infoChild, 'video') call.isVideo = !!getBinaryNodeChild(infoChild, 'video')
call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'] call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid']
call.groupJid = infoChild.attrs['group-jid'] call.groupJid = infoChild.attrs['group-jid']
callOfferCache.set(call.id, call) await callOfferCache.set(call.id, call)
} }
const existingCall = callOfferCache.get<WACallEvent>(call.id) const existingCall = await callOfferCache.get<WACallEvent>(call.id)
// use existing call info to populate this event // use existing call info to populate this event
if (existingCall) { if (existingCall) {
@@ -1333,7 +1333,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// delete data once call has ended // delete data once call has ended
if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') { if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
callOfferCache.del(call.id) await callOfferCache.del(call.id)
} }
ev.emit('call', [call]) ev.emit('call', [call])
+35 -20
View File
@@ -245,30 +245,40 @@ export const makeMessagesSocket = (config: SocketConfig) => {
} }
const toFetch: string[] = [] const toFetch: string[] = []
// Deduplicate and normalize JIDs
jids = deduplicateLidPnJids(Array.from(new Set(jids))) jids = deduplicateLidPnJids(Array.from(new Set(jids)))
const jidsWithUser = jids
.map(jid => {
const decoded = jidDecode(jid)
const user = decoded?.user
const device = decoded?.device
const isExplicitDevice = typeof device === 'number' && device >= 0
for (let jid of jids) { if (isExplicitDevice && user) {
const decoded = jidDecode(jid) deviceResults.push({
const user = decoded?.user user,
const device = decoded?.device device,
const isExplicitDevice = typeof device === 'number' && device >= 0 wireJid: jid
})
return null
}
// Handle explicit device JIDs directly jid = jidNormalizedUser(jid)
if (isExplicitDevice && user) { return { jid, user }
deviceResults.push({ })
user, .filter(jid => jid !== null)
device,
wireJid: jid // Preserve exact JID format for wire protocol
})
continue
}
// For user JIDs, normalize and prepare for device enumeration let mgetDevices: undefined | Record<string, JidWithDevice[] | undefined>
jid = jidNormalizedUser(jid)
if (useCache && userDevicesCache.mget) {
const usersToFetch = jidsWithUser.map(j => j?.user).filter(Boolean) as string[]
mgetDevices = await userDevicesCache.mget(usersToFetch)
}
for (const { jid, user } of jidsWithUser) {
if (useCache) { if (useCache) {
const devices = userDevicesCache.get(user!) as JidWithDevice[] const devices =
mgetDevices?.[user!] ||
(userDevicesCache.mget ? undefined : ((await userDevicesCache.get(user!)) as JidWithDevice[]))
if (devices) { if (devices) {
const isLidJid = jid.includes('@lid') const isLidJid = jid.includes('@lid')
const devicesWithWire = devices.map(d => ({ const devicesWithWire = devices.map(d => ({
@@ -342,8 +352,13 @@ export const makeMessagesSocket = (config: SocketConfig) => {
} }
} }
for (const key in deviceMap) { if (userDevicesCache.mset) {
userDevicesCache.set(key, deviceMap[key]!) // if the cache supports mset, we can set all devices in one go
await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
} else {
for (const key in deviceMap) {
if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
}
} }
} }
+11 -5
View File
@@ -13,13 +13,19 @@ export type WABrowserDescription = [string, string, string]
export type CacheStore = { export type CacheStore = {
/** get a cached key and change the stats */ /** get a cached key and change the stats */
get<T>(key: string): T | undefined get<T>(key: string): Promise<T> | T | undefined
/** set a key in the cache */ /** set a key in the cache */
set<T>(key: string, value: T): void set<T>(key: string, value: T): Promise<void> | void | number | boolean
/** delete a key from the cache */ /** delete a key from the cache */
del(key: string): void del(key: string): void | Promise<void> | number | boolean
/** flush all data */ /** flush all data */
flushAll(): void flushAll(): void | Promise<void>
}
export type PossiblyExtendedCacheStore = CacheStore & {
mget?: <T>(keys: string[]) => Promise<Record<string, T | undefined>>
mset?: <T>(entries: { key: string; value: T }[]) => Promise<void> | void | number | boolean
mdel?: (keys: string[]) => void | Promise<void> | number | boolean
} }
export type PatchedMessageWithRecipientJID = proto.IMessage & { recipientJid?: string } export type PatchedMessageWithRecipientJID = proto.IMessage & { recipientJid?: string }
@@ -78,7 +84,7 @@ export type SocketConfig = {
* used to determine whether to retry a message or not */ * used to determine whether to retry a message or not */
msgRetryCounterCache?: CacheStore msgRetryCounterCache?: CacheStore
/** provide a cache to store a user's device list */ /** provide a cache to store a user's device list */
userDevicesCache?: CacheStore userDevicesCache?: PossiblyExtendedCacheStore
/** cache to store call offers */ /** cache to store call offers */
callOfferCache?: CacheStore callOfferCache?: CacheStore
/** cache to track placeholder resends */ /** cache to track placeholder resends */
+2 -2
View File
@@ -76,7 +76,7 @@ export function makeCacheableSignalKeyStore(
let keys = 0 let keys = 0
for (const type in data) { for (const type in data) {
for (const id in data[type as keyof SignalDataTypeMap]) { for (const id in data[type as keyof SignalDataTypeMap]) {
cache.set(getUniqueId(type, id), data[type as keyof SignalDataTypeMap]![id]!) await cache.set(getUniqueId(type, id), data[type as keyof SignalDataTypeMap]![id]!)
keys += 1 keys += 1
} }
} }
@@ -87,7 +87,7 @@ export function makeCacheableSignalKeyStore(
}) })
}, },
async clear() { async clear() {
cache.flushAll() await cache.flushAll()
await store.clear?.() await store.clear?.()
} }
} }
+3 -3
View File
@@ -152,7 +152,7 @@ export const prepareWAMessageMedia = async (
} }
if (cacheableKey) { if (cacheableKey) {
const mediaBuff = options.mediaCache!.get<Buffer>(cacheableKey) const mediaBuff = await options.mediaCache!.get<Buffer>(cacheableKey)
if (mediaBuff) { if (mediaBuff) {
logger?.debug({ cacheableKey }, 'got media cache hit') logger?.debug({ cacheableKey }, 'got media cache hit')
@@ -202,7 +202,7 @@ export const prepareWAMessageMedia = async (
if (cacheableKey) { if (cacheableKey) {
logger?.debug({ cacheableKey }, 'set cache') logger?.debug({ cacheableKey }, 'set cache')
options.mediaCache!.set(cacheableKey, WAProto.Message.encode(obj).finish()) await options.mediaCache!.set(cacheableKey, WAProto.Message.encode(obj).finish())
} }
return obj return obj
@@ -305,7 +305,7 @@ export const prepareWAMessageMedia = async (
if (cacheableKey) { if (cacheableKey) {
logger?.debug({ cacheableKey }, 'set cache') logger?.debug({ cacheableKey }, 'set cache')
options.mediaCache!.set(cacheableKey, WAProto.Message.encode(obj).finish()) await options.mediaCache!.set(cacheableKey, WAProto.Message.encode(obj).finish())
} }
return obj return obj
+1 -1
View File
@@ -264,7 +264,7 @@ const processMessage = async (
case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE: case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE:
const response = protocolMsg.peerDataOperationRequestResponseMessage! const response = protocolMsg.peerDataOperationRequestResponseMessage!
if (response) { if (response) {
placeholderResendCache?.del(response.stanzaId!) await placeholderResendCache?.del(response.stanzaId!)
// TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.). // TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.).
const { peerDataOperationResult } = response const { peerDataOperationResult } = response
for (const result of peerDataOperationResult!) { for (const result of peerDataOperationResult!) {