Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba1c1b0fdb | |||
| 876af2e96c | |||
| 5bb18da6bf | |||
| 52d8ddae32 | |||
| 8c8c5a312e | |||
| 52756fb50d | |||
| bd8465d9b8 | |||
| 6c52b01ea7 |
@@ -188,10 +188,12 @@ export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[]
|
|||||||
/** 120s timeout for history sync stall detection, same as WA Web's handleChunkProgress / restartPausedTimer (g = 120) */
|
/** 120s timeout for history sync stall detection, same as WA Web's handleChunkProgress / restartPausedTimer (g = 120) */
|
||||||
export const HISTORY_SYNC_PAUSED_TIMEOUT_MS = 120_000
|
export const HISTORY_SYNC_PAUSED_TIMEOUT_MS = 120_000
|
||||||
|
|
||||||
export const MIN_PREKEY_COUNT = 5
|
// Replenishment threshold: when server count drops below this, top-up back to INITIAL_PREKEY_COUNT
|
||||||
|
export const MIN_PREKEY_COUNT = 200
|
||||||
|
|
||||||
// Moderate prekey count (upstream uses 812, reduced to balance rate limiting and availability)
|
// Initial pool size matching WA Business (CDP IDB capture: prekey-store = 812 on registration)
|
||||||
export const INITIAL_PREKEY_COUNT = 200
|
// Rounded to 800 for cleanliness; replenishment always tops up to this value
|
||||||
|
export const INITIAL_PREKEY_COUNT = 800
|
||||||
|
|
||||||
export const UPLOAD_TIMEOUT = 30000 // 30 seconds
|
export const UPLOAD_TIMEOUT = 30000 // 30 seconds
|
||||||
// Moderate upload interval to balance rate limiting and responsiveness (was 5000)
|
// Moderate upload interval to balance rate limiting and responsiveness (was 5000)
|
||||||
|
|||||||
+29
-40
@@ -7,6 +7,7 @@ import { proto } from '../../WAProto/index.js'
|
|||||||
import {
|
import {
|
||||||
DEFAULT_CACHE_TTLS,
|
DEFAULT_CACHE_TTLS,
|
||||||
DEFAULT_SESSION_CLEANUP_CONFIG,
|
DEFAULT_SESSION_CLEANUP_CONFIG,
|
||||||
|
INITIAL_PREKEY_COUNT,
|
||||||
KEY_BUNDLE_TYPE,
|
KEY_BUNDLE_TYPE,
|
||||||
MIN_PREKEY_COUNT,
|
MIN_PREKEY_COUNT,
|
||||||
PLACEHOLDER_MAX_AGE_SECONDS,
|
PLACEHOLDER_MAX_AGE_SECONDS,
|
||||||
@@ -1212,7 +1213,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
return await query(stanza)
|
return await query(stanza)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendRetryRequest = async (node: BinaryNode, forceIncludeKeys = false) => {
|
const sendRetryRequest = async (node: BinaryNode, forceIncludeKeys = false, decryptionError?: string) => {
|
||||||
const { fullMessage } = decodeMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '')
|
const { fullMessage } = decodeMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '')
|
||||||
const { key: msgKey } = fullMessage
|
const { key: msgKey } = fullMessage
|
||||||
const msgId = msgKey.id!
|
const msgId = msgKey.id!
|
||||||
@@ -1310,39 +1311,27 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
|
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
|
||||||
const fromJid = node.attrs.from!
|
const fromJid = node.attrs.from!
|
||||||
|
|
||||||
// Check if we should recreate the session
|
// Derive the Signal error code from the actual decryption failure message.
|
||||||
let shouldRecreateSession = false
|
// This is sent in the retry receipt so the peer (even another InfiniteAPI instance)
|
||||||
let recreateReason = ''
|
// knows the exact reason and can recreate the session immediately instead of waiting
|
||||||
|
// for the 1-hour timeout fallback.
|
||||||
if (enableAutoSessionRecreation && messageRetryManager && retryCount >= 1) {
|
//
|
||||||
try {
|
// Codes mirror RetryReason enum in message-retry-manager.ts:
|
||||||
// Check if we have a session with this JID
|
// 0 = UnknownError | 1 = NoSession | 2 = InvalidKey
|
||||||
const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
|
// 3 = InvalidKeyId | 7 = BadMac (= SignalErrorInvalidMessage/InvalidCipherKey)
|
||||||
const hasSession = await signalRepository.validateSession(fromJid)
|
//
|
||||||
|
// NOTE: We do NOT delete the session here (receiver side). The Signal Protocol
|
||||||
// Extract error code from retry node if present (for MAC error detection)
|
// recovers automatically when the sender's pkmsg arrives — it overwrites the
|
||||||
const retryNode = getBinaryNodeChild(node, 'retry')
|
// corrupted session. Deleting prematurely creates a race window where no session
|
||||||
const errorAttr = retryNode?.attrs?.error
|
// exists, which can cause "No Session" errors on concurrent messages.
|
||||||
const errorCode = messageRetryManager.parseRetryErrorCode(errorAttr)
|
const retryErrorCode = (() => {
|
||||||
|
if (!decryptionError) return 0
|
||||||
const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists, errorCode)
|
if (/bad\s*mac/i.test(decryptionError)) return 7 // SignalErrorBadMac
|
||||||
shouldRecreateSession = result.recreate
|
if (/no\s*session/i.test(decryptionError)) return 1 // SignalErrorNoSession
|
||||||
recreateReason = result.reason
|
if (/pre\s*key/i.test(decryptionError)) return 3 // SignalErrorInvalidKeyId
|
||||||
|
if (/invalid\s*key/i.test(decryptionError)) return 2 // SignalErrorInvalidKey
|
||||||
if (shouldRecreateSession) {
|
return 0
|
||||||
logger.debug({ fromJid, retryCount, reason: recreateReason, errorCode }, 'recreating session for retry')
|
})()
|
||||||
// Delete existing session to force recreation
|
|
||||||
// CRITICAL: Use same transaction key as encrypt/decrypt operations to prevent race
|
|
||||||
// Using meId ensures this delete serializes with sendMessage() and other session operations
|
|
||||||
await authState.keys.transaction(async () => {
|
|
||||||
await authState.keys.set({ session: { [sessionId]: null } })
|
|
||||||
}, authState.creds.me?.id || 'session-operation')
|
|
||||||
forceIncludeKeys = true
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn({ error, fromJid }, 'failed to check session recreation')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retryCount <= 2) {
|
if (retryCount <= 2) {
|
||||||
// Use new retry manager for phone requests if available
|
// Use new retry manager for phone requests if available
|
||||||
@@ -1383,8 +1372,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
id: node.attrs.id!,
|
id: node.attrs.id!,
|
||||||
t: node.attrs.t!,
|
t: node.attrs.t!,
|
||||||
v: '1',
|
v: '1',
|
||||||
// ADD ERROR FIELD
|
error: retryErrorCode.toString()
|
||||||
error: '0'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1403,7 +1391,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
receipt.attrs.participant = node.attrs.participant
|
receipt.attrs.participant = node.attrs.participant
|
||||||
}
|
}
|
||||||
|
|
||||||
if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
|
if (retryCount > 1 || forceIncludeKeys) {
|
||||||
const { update, preKeys } = await getNextPreKeys(authState, 1)
|
const { update, preKeys } = await getNextPreKeys(authState, 1)
|
||||||
|
|
||||||
const [keyId] = Object.keys(preKeys)
|
const [keyId] = Object.keys(preKeys)
|
||||||
@@ -1440,7 +1428,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
logger.debug({ count, shouldUploadMorePreKeys }, 'recv pre-key count')
|
logger.debug({ count, shouldUploadMorePreKeys }, 'recv pre-key count')
|
||||||
if (shouldUploadMorePreKeys) {
|
if (shouldUploadMorePreKeys) {
|
||||||
await uploadPreKeys()
|
// Top-up back to INITIAL_PREKEY_COUNT so the pool is always restored to full size
|
||||||
|
await uploadPreKeys(Math.max(1, INITIAL_PREKEY_COUNT - count))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const result = await handleIdentityChange(node, {
|
const result = await handleIdentityChange(node, {
|
||||||
@@ -2505,7 +2494,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const encNode = getBinaryNodeChild(node, 'enc')
|
const encNode = getBinaryNodeChild(node, 'enc')
|
||||||
await sendRetryRequest(node, !encNode)
|
await sendRetryRequest(node, !encNode, errorMessage)
|
||||||
if (retryRequestDelayMs) {
|
if (retryRequestDelayMs) {
|
||||||
await delay(retryRequestDelayMs)
|
await delay(retryRequestDelayMs)
|
||||||
}
|
}
|
||||||
@@ -2514,7 +2503,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
// Still attempt retry even if pre-key upload failed
|
// Still attempt retry even if pre-key upload failed
|
||||||
try {
|
try {
|
||||||
const encNode = getBinaryNodeChild(node, 'enc')
|
const encNode = getBinaryNodeChild(node, 'enc')
|
||||||
await sendRetryRequest(node, !encNode)
|
await sendRetryRequest(node, !encNode, errorMessage)
|
||||||
} catch (retryErr) {
|
} catch (retryErr) {
|
||||||
logger.error({ retryErr }, 'Failed to send retry after error handling')
|
logger.error({ retryErr }, 'Failed to send retry after error handling')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
AlbumMessageOptions,
|
AlbumMessageOptions,
|
||||||
AlbumSendResult,
|
AlbumSendResult,
|
||||||
AnyMessageContent,
|
AnyMessageContent,
|
||||||
|
LIDMapping,
|
||||||
MediaConnInfo,
|
MediaConnInfo,
|
||||||
MessageReceiptType,
|
MessageReceiptType,
|
||||||
MessageRelayOptions,
|
MessageRelayOptions,
|
||||||
@@ -338,6 +339,33 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4th LID→PN source: device-list entries sharing the same raw_id
|
||||||
|
// WA Business uses this for accounts where HistorySync sends zero phoneNumberToLidMappings
|
||||||
|
const allEntries = [...result.list, ...result.sideList]
|
||||||
|
const rawIdMap = new Map<number, { pn?: string; lid?: string }>()
|
||||||
|
for (const item of allEntries) {
|
||||||
|
if (typeof item.rawId !== 'number' || isNaN(item.rawId)) continue
|
||||||
|
const decoded = jidDecode(item.id)
|
||||||
|
if (!decoded) continue
|
||||||
|
const entry = rawIdMap.get(item.rawId) || {}
|
||||||
|
if (decoded.server === 'lid' || decoded.server === 'hosted.lid') {
|
||||||
|
entry.lid = item.id
|
||||||
|
} else if (decoded.server === 's.whatsapp.net' || decoded.server === 'c.us') {
|
||||||
|
entry.pn = item.id
|
||||||
|
}
|
||||||
|
rawIdMap.set(item.rawId, entry)
|
||||||
|
}
|
||||||
|
const rawIdMappings: LIDMapping[] = []
|
||||||
|
for (const { pn, lid } of rawIdMap.values()) {
|
||||||
|
if (pn && lid) {
|
||||||
|
rawIdMappings.push({ lid: jidNormalizedUser(lid), pn: jidNormalizedUser(pn) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rawIdMappings.length > 0) {
|
||||||
|
await signalRepository.lidMapping.storeLIDPNMappings(rawIdMappings)
|
||||||
|
logger.debug({ count: rawIdMappings.length }, 'stored LID-PN mappings from raw_id pairing')
|
||||||
|
}
|
||||||
|
|
||||||
const meId = authState.creds.me?.id
|
const meId = authState.creds.me?.id
|
||||||
if (!meId) throw new Boom('Not authenticated', { statusCode: 401 })
|
if (!meId) throw new Boom('Not authenticated', { statusCode: 401 })
|
||||||
const meLid = authState.creds.me?.lid || ''
|
const meLid = authState.creds.me?.lid || ''
|
||||||
|
|||||||
+28
-8
@@ -701,10 +701,12 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent multiple concurrent uploads
|
// Prevent multiple concurrent uploads — if one is already running, wait for it and return:
|
||||||
|
// the concurrent upload already replenished the pool, so there is nothing left to do.
|
||||||
if (uploadPreKeysPromise) {
|
if (uploadPreKeysPromise) {
|
||||||
logger.debug('Pre-key upload already in progress, waiting for completion')
|
logger.debug('Pre-key upload already in progress, waiting for completion')
|
||||||
await uploadPreKeysPromise
|
await uploadPreKeysPromise
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadLogic = async () => {
|
const uploadLogic = async () => {
|
||||||
@@ -784,14 +786,15 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
try {
|
try {
|
||||||
let count = 0
|
let count = 0
|
||||||
const preKeyCount = await getAvailablePreKeysOnServer()
|
const preKeyCount = await getAvailablePreKeysOnServer()
|
||||||
if (preKeyCount === 0) count = INITIAL_PREKEY_COUNT
|
// How many to upload: top-up to INITIAL_PREKEY_COUNT from whatever remains on server
|
||||||
else count = MIN_PREKEY_COUNT
|
count = Math.max(0, INITIAL_PREKEY_COUNT - preKeyCount)
|
||||||
const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists()
|
const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists()
|
||||||
|
|
||||||
logger.info(`${preKeyCount} pre-keys found on server`)
|
logger.info(`${preKeyCount} pre-keys found on server`)
|
||||||
logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`)
|
logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`)
|
||||||
|
|
||||||
const lowServerCount = preKeyCount <= count
|
// Trigger upload when below the replenishment threshold, not when count < topUp amount
|
||||||
|
const lowServerCount = preKeyCount < MIN_PREKEY_COUNT
|
||||||
const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0
|
const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0
|
||||||
|
|
||||||
const shouldUpload = lowServerCount || missingCurrentPreKey
|
const shouldUpload = lowServerCount || missingCurrentPreKey
|
||||||
@@ -1151,7 +1154,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
// Decrement active connections
|
// Decrement active connections
|
||||||
decrementActiveConnections()
|
decrementActiveConnections()
|
||||||
|
|
||||||
clearInterval(keepAliveReq)
|
clearTimeout(keepAliveReq)
|
||||||
clearTimeout(qrTimer)
|
clearTimeout(qrTimer)
|
||||||
|
|
||||||
// Clear offline-buffer safety timer so its callback cannot call ev.flush()
|
// Clear offline-buffer safety timer so its callback cannot call ev.flush()
|
||||||
@@ -1262,8 +1265,16 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const startKeepAliveRequest = () =>
|
const startKeepAliveRequest = () => {
|
||||||
(keepAliveReq = setInterval(() => {
|
// Use recursive setTimeout with ±15% jitter to match WA Desktop behaviour
|
||||||
|
// (WA Business Desktop: ~25-30s intervals with natural variance)
|
||||||
|
const scheduleNextKeepAlive = () => {
|
||||||
|
const jitter = keepAliveIntervalMs * 0.15
|
||||||
|
const delay = keepAliveIntervalMs + Math.floor((Math.random() * 2 - 1) * jitter)
|
||||||
|
keepAliveReq = setTimeout(onKeepAliveTick, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeepAliveTick = () => {
|
||||||
if (!lastDateRecv) {
|
if (!lastDateRecv) {
|
||||||
lastDateRecv = new Date()
|
lastDateRecv = new Date()
|
||||||
}
|
}
|
||||||
@@ -1275,6 +1286,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
*/
|
*/
|
||||||
if (diff > keepAliveIntervalMs + 5000) {
|
if (diff > keepAliveIntervalMs + 5000) {
|
||||||
void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
|
void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
|
||||||
|
return // connection closing — do not reschedule
|
||||||
} else if (ws.isOpen) {
|
} else if (ws.isOpen) {
|
||||||
// Send keep-alive ping via sendNode() (fire-and-forget) instead of query().
|
// Send keep-alive ping via sendNode() (fire-and-forget) instead of query().
|
||||||
// query() wraps the ping in the query circuit breaker — when that breaker is
|
// query() wraps the ping in the query circuit breaker — when that breaker is
|
||||||
@@ -1297,7 +1309,15 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
} else {
|
} else {
|
||||||
logger.warn('keep alive called when WS not open')
|
logger.warn('keep alive called when WS not open')
|
||||||
}
|
}
|
||||||
}, keepAliveIntervalMs))
|
|
||||||
|
// Do not reschedule once shutdown has started (closed set by end() on any concurrent path)
|
||||||
|
if (!closed) {
|
||||||
|
scheduleNextKeepAlive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleNextKeepAlive()
|
||||||
|
}
|
||||||
/** i have no idea why this exists. pls enlighten me */
|
/** i have no idea why this exists. pls enlighten me */
|
||||||
const sendPassiveIq = (tag: 'passive' | 'active') =>
|
const sendPassiveIq = (tag: 'passive' | 'active') =>
|
||||||
query({
|
query({
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export type BaileysEventMap = {
|
|||||||
chats: Chat[]
|
chats: Chat[]
|
||||||
contacts: Contact[]
|
contacts: Contact[]
|
||||||
messages: WAMessage[]
|
messages: WAMessage[]
|
||||||
|
/** Past participants for group chats (people who left/were removed). userJid is always a phone number (PN), never a LID. */
|
||||||
|
pastParticipants?: proto.IPastParticipants[] | null
|
||||||
isLatest?: boolean
|
isLatest?: boolean
|
||||||
progress?: number | null
|
progress?: number | null
|
||||||
syncType?: proto.HistorySync.HistorySyncType | null
|
syncType?: proto.HistorySync.HistorySyncType | null
|
||||||
@@ -185,6 +187,8 @@ export type BufferedEventData = {
|
|||||||
chats: { [jid: string]: Chat }
|
chats: { [jid: string]: Chat }
|
||||||
contacts: { [jid: string]: Contact }
|
contacts: { [jid: string]: Contact }
|
||||||
messages: { [uqId: string]: WAMessage }
|
messages: { [uqId: string]: WAMessage }
|
||||||
|
/** Keyed by groupJid for O(1) deduplication across chunks */
|
||||||
|
pastParticipants: { [groupJid: string]: proto.IPastParticipant[] }
|
||||||
empty: boolean
|
empty: boolean
|
||||||
isLatest: boolean
|
isLatest: boolean
|
||||||
progress?: number | null
|
progress?: number | null
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
|
import { proto } from '../../WAProto/index.js'
|
||||||
import type {
|
import type {
|
||||||
BaileysEvent,
|
BaileysEvent,
|
||||||
BaileysEventEmitter,
|
BaileysEventEmitter,
|
||||||
@@ -890,6 +891,7 @@ const makeBufferData = (): BufferedEventData => {
|
|||||||
chats: {},
|
chats: {},
|
||||||
messages: {},
|
messages: {},
|
||||||
contacts: {},
|
contacts: {},
|
||||||
|
pastParticipants: {},
|
||||||
isLatest: false,
|
isLatest: false,
|
||||||
empty: true
|
empty: true
|
||||||
},
|
},
|
||||||
@@ -967,6 +969,28 @@ function append<E extends BufferableEvent>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge pastParticipants with deduplication by groupJid and by userJid within each group.
|
||||||
|
// Multiple HistorySync chunks can carry the same group -- we merge rather than concatenate
|
||||||
|
// to avoid duplicate entries in the final event delivered to the consumer.
|
||||||
|
for (const group of (eventData.pastParticipants ?? []) as proto.IPastParticipants[]) {
|
||||||
|
const groupJid = group.groupJid
|
||||||
|
if (!groupJid) continue
|
||||||
|
|
||||||
|
if (!data.historySets.pastParticipants[groupJid]) {
|
||||||
|
data.historySets.pastParticipants[groupJid] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = data.historySets.pastParticipants[groupJid]
|
||||||
|
const seenJids = new Set(existing.map(p => p.userJid).filter(Boolean))
|
||||||
|
|
||||||
|
for (const participant of (group.pastParticipants ?? []) as proto.IPastParticipant[]) {
|
||||||
|
if (participant.userJid && !seenJids.has(participant.userJid)) {
|
||||||
|
existing.push(participant)
|
||||||
|
seenJids.add(participant.userJid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data.historySets.empty = false
|
data.historySets.empty = false
|
||||||
data.historySets.syncType = eventData.syncType
|
data.historySets.syncType = eventData.syncType
|
||||||
data.historySets.progress = eventData.progress
|
data.historySets.progress = eventData.progress
|
||||||
@@ -1292,6 +1316,10 @@ function consolidateEvents(data: BufferedEventData) {
|
|||||||
chats: Object.values(data.historySets.chats),
|
chats: Object.values(data.historySets.chats),
|
||||||
messages: Object.values(data.historySets.messages),
|
messages: Object.values(data.historySets.messages),
|
||||||
contacts: Object.values(data.historySets.contacts),
|
contacts: Object.values(data.historySets.contacts),
|
||||||
|
// Convert dedup map back to array. Each entry has groupJid + participants (all unique userJids).
|
||||||
|
pastParticipants: Object.entries(data.historySets.pastParticipants).map(
|
||||||
|
([groupJid, participants]) => ({ groupJid, pastParticipants: participants })
|
||||||
|
),
|
||||||
syncType: data.historySets.syncType,
|
syncType: data.historySets.syncType,
|
||||||
progress: data.historySets.progress,
|
progress: data.historySets.progress,
|
||||||
isLatest: data.historySets.isLatest,
|
isLatest: data.historySets.isLatest,
|
||||||
|
|||||||
+34
-2
@@ -15,7 +15,8 @@ import {
|
|||||||
import { toNumber } from './generics'
|
import { toNumber } from './generics'
|
||||||
import type { ILogger } from './logger.js'
|
import type { ILogger } from './logger.js'
|
||||||
import { normalizeMessageContent } from './messages'
|
import { normalizeMessageContent } from './messages'
|
||||||
import { downloadContentFromMessage } from './messages-media'
|
import { DEFAULT_ORIGIN } from '../Defaults'
|
||||||
|
import { downloadContentFromMessage, getUrlFromDirectPath } from './messages-media'
|
||||||
|
|
||||||
const inflatePromise = promisify(inflate)
|
const inflatePromise = promisify(inflate)
|
||||||
|
|
||||||
@@ -374,10 +375,25 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
|
|||||||
// Convert Map back to array for return
|
// Convert Map back to array for return
|
||||||
const lidPnMappings = Array.from(lidPnMap.values())
|
const lidPnMappings = Array.from(lidPnMap.values())
|
||||||
|
|
||||||
|
// Normalize pastParticipants: resolve LID userJids → PN so the consumer always
|
||||||
|
// receives a phone number, never an opaque LID identifier.
|
||||||
|
// Uses the lidPnMap built above (populated from phoneNumberToLidMappings + conversations).
|
||||||
|
// If a LID cannot be resolved, the original value is kept rather than dropping the participant.
|
||||||
|
const pastParticipants: proto.IPastParticipants[] = (item.pastParticipants ?? []).map(group => ({
|
||||||
|
groupJid: group.groupJid,
|
||||||
|
pastParticipants: (group.pastParticipants ?? []).map(participant => {
|
||||||
|
const userJid = participant.userJid
|
||||||
|
if (!userJid || !isAnyLidUser(userJid)) return participant
|
||||||
|
const mapping = lidPnMap.get(jidNormalizedUser(userJid))
|
||||||
|
return mapping?.pn ? { ...participant, userJid: mapping.pn } : participant
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chats,
|
chats,
|
||||||
contacts,
|
contacts,
|
||||||
messages,
|
messages,
|
||||||
|
pastParticipants,
|
||||||
lidPnMappings,
|
lidPnMappings,
|
||||||
syncType: item.syncType,
|
syncType: item.syncType,
|
||||||
progress: item.progress
|
progress: item.progress
|
||||||
@@ -408,7 +424,23 @@ export const downloadAndProcessHistorySyncNotification = async (
|
|||||||
historyMsg = await downloadHistory(msg, options)
|
historyMsg = await downloadHistory(msg, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
return processHistoryMessage(historyMsg, logger)
|
const result = processHistoryMessage(historyMsg, logger)
|
||||||
|
|
||||||
|
// Mirror WA Desktop behaviour: DELETE the CDN blob only after processing succeeds.
|
||||||
|
// Doing this earlier (e.g. inside downloadHistory) risks permanent history loss if
|
||||||
|
// processing throws — the server copy would be gone and retry after reconnect would fail.
|
||||||
|
if (msg.directPath) {
|
||||||
|
const cdnUrl = getUrlFromDirectPath(msg.directPath)
|
||||||
|
fetch(cdnUrl, {
|
||||||
|
...options,
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { ...((options as RequestInit).headers ?? {}), Origin: DEFAULT_ORIGIN }
|
||||||
|
}).catch(() => {
|
||||||
|
// non-fatal — server will expire it anyway
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from './Protocols'
|
} from './Protocols'
|
||||||
import { USyncUser } from './USyncUser'
|
import { USyncUser } from './USyncUser'
|
||||||
|
|
||||||
export type USyncQueryResultList = { [protocol: string]: unknown; id: string }
|
export type USyncQueryResultList = { [protocol: string]: unknown; id: string; rawId?: number }
|
||||||
|
|
||||||
export type USyncQueryResult = {
|
export type USyncQueryResult = {
|
||||||
list: USyncQueryResultList[]
|
list: USyncQueryResultList[]
|
||||||
@@ -68,10 +68,8 @@ export class USyncQuery {
|
|||||||
//TODO: see if there are any errors in the result node
|
//TODO: see if there are any errors in the result node
|
||||||
//const resultNode = getBinaryNodeChild(usyncNode, 'result')
|
//const resultNode = getBinaryNodeChild(usyncNode, 'result')
|
||||||
|
|
||||||
const listNode = usyncNode ? getBinaryNodeChild(usyncNode, 'list') : undefined
|
const parseNodeList = (content: BinaryNode[]): USyncQueryResultList[] =>
|
||||||
|
content.reduce((acc: USyncQueryResultList[], node) => {
|
||||||
if (listNode?.content && Array.isArray(listNode.content)) {
|
|
||||||
queryResult.list = listNode.content.reduce((acc: USyncQueryResultList[], node) => {
|
|
||||||
const id = node?.attrs.jid
|
const id = node?.attrs.jid
|
||||||
if (id) {
|
if (id) {
|
||||||
const data = Array.isArray(node?.content)
|
const data = Array.isArray(node?.content)
|
||||||
@@ -89,15 +87,24 @@ export class USyncQuery {
|
|||||||
.filter(([, b]) => b !== null) as [string, unknown][]
|
.filter(([, b]) => b !== null) as [string, unknown][]
|
||||||
)
|
)
|
||||||
: {}
|
: {}
|
||||||
acc.push({ ...data, id })
|
const rawIdAttr = node?.attrs?.['raw_id']
|
||||||
|
const rawId = rawIdAttr !== undefined ? Number(rawIdAttr) : undefined
|
||||||
|
acc.push({ ...data, id, ...(rawId !== undefined && !isNaN(rawId) ? { rawId } : {}) })
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const listNode = usyncNode ? getBinaryNodeChild(usyncNode, 'list') : undefined
|
||||||
|
if (listNode?.content && Array.isArray(listNode.content)) {
|
||||||
|
queryResult.list = parseNodeList(listNode.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sideListNode = usyncNode ? getBinaryNodeChild(usyncNode, 'side_list') : undefined
|
||||||
|
if (sideListNode?.content && Array.isArray(sideListNode.content)) {
|
||||||
|
queryResult.sideList = parseNodeList(sideListNode.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: implement side list
|
|
||||||
//const sideListNode = getBinaryNodeChild(usyncNode, 'side_list')
|
|
||||||
return queryResult
|
return queryResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user