@nexustechpro/baileys 2.0.5 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/WAProto/index.js +22 -18
- package/lib/Defaults/baileys-version.json +1 -1
- package/lib/Defaults/index.js +7 -6
- package/lib/Signal/libsignal.js +65 -50
- package/lib/Socket/chats.js +64 -57
- package/lib/Socket/index.js +2 -3
- package/lib/Socket/messages-recv.js +227 -41
- package/lib/Socket/messages-send.js +79 -117
- package/lib/Socket/nexus-handler.js +325 -90
- package/lib/Socket/registration.js +50 -33
- package/lib/Socket/socket.js +232 -69
- package/lib/Types/Newsletter.js +37 -29
- package/lib/Types/State.js +43 -0
- package/lib/Utils/auth-utils.js +2 -2
- package/lib/Utils/chat-utils.js +48 -16
- package/lib/Utils/companion-reg-client-utils.js +34 -0
- package/lib/Utils/decode-wa-message.js +40 -8
- package/lib/Utils/generics.js +5 -7
- package/lib/Utils/index.js +4 -0
- package/lib/Utils/link-preview.js +10 -0
- package/lib/Utils/messages-media.js +426 -382
- package/lib/Utils/messages.js +602 -487
- package/lib/Utils/process-message.js +53 -35
- package/lib/Utils/reporting-utils.js +155 -0
- package/lib/Utils/signal.js +134 -104
- package/lib/Utils/sync-action-utils.js +33 -0
- package/lib/Utils/tc-token-utils.js +162 -0
- package/lib/WABinary/constants.js +6 -0
- package/lib/WABinary/index.js +1 -0
- package/lib/index.js +2 -3
- package/package.json +6 -4
|
@@ -4,17 +4,18 @@ import { randomBytes } from "crypto"
|
|
|
4
4
|
import { proto } from "../../WAProto/index.js"
|
|
5
5
|
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from "../Defaults/index.js"
|
|
6
6
|
import { WAMessageStatus, WAMessageStubType } from "../Types/index.js"
|
|
7
|
-
import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys,
|
|
7
|
+
import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, extractE2ESessionFromRetryReceipt, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey, ACCOUNT_RESTRICTED_TEXT, buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveTcTokenJid, resolveIssuanceJid, storeTcTokensFromIqResult, TC_TOKEN_INDEX_KEY } from "../Utils/index.js"
|
|
8
8
|
import { makeMutex } from "../Utils/make-mutex.js"
|
|
9
|
-
import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET, jidEncode } from "../WABinary/index.js"
|
|
9
|
+
import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, getBinaryNodeChildUInt, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET, jidEncode } from "../WABinary/index.js"
|
|
10
10
|
import { extractGroupMetadata } from "./groups.js"
|
|
11
11
|
import { makeMessagesSocket } from "./messages-send.js"
|
|
12
12
|
|
|
13
13
|
export const makeMessagesRecvSocket = (config) => {
|
|
14
14
|
const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config
|
|
15
15
|
const sock = makeMessagesSocket(config)
|
|
16
|
-
const { ev, authState, ws, processingMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager,
|
|
16
|
+
const { ev, authState, ws, processingMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager, registerSocketEndHandler, issuePrivacyTokens, fetchAccountReachoutTimelock } = sock
|
|
17
17
|
|
|
18
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
|
18
19
|
const messageMutex = makeMutex()
|
|
19
20
|
const notificationMutex = makeMutex()
|
|
20
21
|
const retryMutex = makeMutex()
|
|
@@ -109,18 +110,23 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
const sendMessageAck = async (
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
if (
|
|
118
|
-
if (
|
|
119
|
-
|
|
113
|
+
const sendMessageAck = async (node, errorCode) => {
|
|
114
|
+
const { tag, attrs, content } = node
|
|
115
|
+
const hasUnavailable = !!getBinaryNodeChild({ tag, attrs, content }, 'unavailable')
|
|
116
|
+
const stanza = { tag: 'ack', attrs: { id: attrs.id, to: attrs.from, class: tag } }
|
|
117
|
+
if (errorCode) stanza.attrs.error = errorCode.toString()
|
|
118
|
+
if (attrs.participant) stanza.attrs.participant = attrs.participant
|
|
119
|
+
if (attrs.recipient) stanza.attrs.recipient = attrs.recipient
|
|
120
|
+
// include type always when present (upstream), but also force-include on unavailable/error
|
|
121
|
+
if (attrs.type || hasUnavailable || errorCode) stanza.attrs.type = attrs.type
|
|
122
|
+
// include from for all message-class ACKs (upstream), not just unavailable
|
|
123
|
+
if (tag === 'message' && authState.creds.me?.id) stanza.attrs.from = authState.creds.me.id
|
|
124
|
+
logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, 'sent ack')
|
|
120
125
|
try {
|
|
121
126
|
await sendNode(stanza)
|
|
122
127
|
} catch (error) {
|
|
123
|
-
if (error?.output?.statusCode === 428 || error?.message?.includes(
|
|
128
|
+
if (error?.output?.statusCode === 428 || error?.message?.includes('Connection'))
|
|
129
|
+
logger.warn({ id: attrs.id, error: error?.message }, 'Failed to send ACK (connection closed) - message already received')
|
|
124
130
|
else throw error
|
|
125
131
|
}
|
|
126
132
|
}
|
|
@@ -263,20 +269,42 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
263
269
|
}, authState?.creds?.me?.id || "sendRetryRequest")
|
|
264
270
|
}
|
|
265
271
|
|
|
272
|
+
const reissueTcTokenAfterIdentityChange = (from) => {
|
|
273
|
+
void (async () => {
|
|
274
|
+
const normalizedJid = jidNormalizedUser(from)
|
|
275
|
+
const tcJid = await resolveTcTokenJid(normalizedJid, getLIDForPN)
|
|
276
|
+
const tcTokenData = await authState.keys.get('tctoken', [tcJid])
|
|
277
|
+
const senderTs = tcTokenData?.[tcJid]?.senderTimestamp
|
|
278
|
+
if (senderTs === null || senderTs === undefined || isTcTokenExpired(senderTs)) return
|
|
279
|
+
logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken')
|
|
280
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
|
|
281
|
+
const issueJid = await resolveIssuanceJid(normalizedJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID)
|
|
282
|
+
const result = await issuePrivacyTokens([issueJid], senderTs)
|
|
283
|
+
await storeTcTokensFromIqResult({ result, fallbackJid: tcJid, keys: authState.keys, getLIDForPN, onNewJidStored: trackTcTokenJid })
|
|
284
|
+
})().catch(err => logger.debug({ jid: from, err: err?.message }, 'failed to re-issue tctoken after identity change'))
|
|
285
|
+
}
|
|
286
|
+
|
|
266
287
|
const handleEncryptNotification = async (node) => {
|
|
267
288
|
const from = node.attrs.from
|
|
268
289
|
if (from === S_WHATSAPP_NET) {
|
|
290
|
+
const stanzaId = node.attrs.id
|
|
291
|
+
if (stanzaId && inFlightPreKeyLow.has(stanzaId)) return
|
|
269
292
|
const countChild = getBinaryNodeChild(node, "count")
|
|
270
293
|
const count = +countChild.attrs.value
|
|
271
294
|
const shouldUploadMorePreKeys = count < MIN_PREKEY_COUNT
|
|
272
295
|
logger.debug({ count, shouldUploadMorePreKeys }, "recv pre-key count")
|
|
273
|
-
if (shouldUploadMorePreKeys)
|
|
296
|
+
if (shouldUploadMorePreKeys) {
|
|
297
|
+
if (stanzaId) inFlightPreKeyLow.add(stanzaId)
|
|
298
|
+
try { await uploadPreKeys() } finally { if (stanzaId) inFlightPreKeyLow.delete(stanzaId) }
|
|
299
|
+
}
|
|
274
300
|
} else {
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
301
|
+
const result = await handleIdentityChange(node, { meId: authState.creds.me?.id, meLid: authState.creds.me?.lid, validateSession: signalRepository.validateSession, assertSessions, debounceCache: identityAssertDebounce, logger, onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange })
|
|
302
|
+
if (result.action === 'no_identity_node') logger.info({ node }, 'unknown encrypt notification')
|
|
277
303
|
}
|
|
278
304
|
}
|
|
279
305
|
|
|
306
|
+
const inFlightPreKeyLow = new Set()
|
|
307
|
+
|
|
280
308
|
const handleGroupNotification = (fullNode, child, msg) => {
|
|
281
309
|
const actingParticipantLid = fullNode.attrs.participant
|
|
282
310
|
const actingParticipantPn = fullNode.attrs.participant_pn
|
|
@@ -356,19 +384,57 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
356
384
|
|
|
357
385
|
const handlePrivacyTokenNotification = async (node) => {
|
|
358
386
|
const tokensNode = getBinaryNodeChild(node, "tokens")
|
|
359
|
-
const from = jidNormalizedUser(node.attrs.from)
|
|
360
387
|
if (!tokensNode) return
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
388
|
+
const from = jidNormalizedUser(node.attrs.from)
|
|
389
|
+
const senderLid = node.attrs.sender_lid && isLidUser(jidNormalizedUser(node.attrs.sender_lid)) ? jidNormalizedUser(node.attrs.sender_lid) : undefined
|
|
390
|
+
const fallbackJid = senderLid ?? (await resolveTcTokenJid(from, getLIDForPN))
|
|
391
|
+
logger.debug({ from, storageJid: fallbackJid }, 'processing privacy token notification')
|
|
392
|
+
await storeTcTokensFromIqResult({ result: node, fallbackJid, keys: authState.keys, getLIDForPN, onNewJidStored: trackTcTokenJid })
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const handleDevicesNotification = async (node) => {
|
|
396
|
+
const [child] = getAllBinaryNodeChildren(node)
|
|
397
|
+
const from = jidNormalizedUser(node.attrs.from)
|
|
398
|
+
if (!child) { logger.debug({ from }, 'devices notification missing child, skipping'); return }
|
|
399
|
+
const tag = child.tag
|
|
400
|
+
const deviceHash = child.attrs.device_hash
|
|
401
|
+
const devices = getBinaryNodeChildren(child, 'device')
|
|
402
|
+
if (areJidsSameUser(from, authState.creds.me.id) || areJidsSameUser(from, authState.creds.me.lid)) { const deviceJids = devices.map(d => d.attrs.jid); logger.info({ deviceJids }, 'got my own devices') }
|
|
403
|
+
if (!devices.length) { logger.debug({ from, tag }, 'no devices in notification, skipping'); return }
|
|
404
|
+
const decoded = []
|
|
405
|
+
for (const d of devices) {
|
|
406
|
+
const jid = d.attrs.jid
|
|
407
|
+
if (!jid) continue
|
|
408
|
+
const parts = jidDecode(jid)
|
|
409
|
+
if (!parts) { logger.debug({ jid }, 'failed to decode device jid, skipping'); continue }
|
|
410
|
+
decoded.push({ jid, user: parts.user, server: parts.server, device: parts.device })
|
|
371
411
|
}
|
|
412
|
+
if (!decoded.length) return
|
|
413
|
+
await sock.devicesMutex.mutex(async () => {
|
|
414
|
+
const byUser = new Map()
|
|
415
|
+
for (const d of decoded) { const list = byUser.get(d.user) || []; list.push(d); byUser.set(d.user, list) }
|
|
416
|
+
for (const [user, entries] of byUser) {
|
|
417
|
+
if (tag === 'update') { logger.debug({ user }, `${user}'s device list updated, dropping cached devices`); await sock.userDevicesCache?.del(user); continue }
|
|
418
|
+
if (tag === 'remove') await signalRepository.deleteSession(entries.map(e => e.jid))
|
|
419
|
+
const existingCache = (await sock.userDevicesCache?.get(user)) || []
|
|
420
|
+
if (!existingCache.length) { logger.debug({ user, tag }, 'device list not cached, deferring to USync refresh'); continue }
|
|
421
|
+
const affected = new Set(entries.map(e => e.device))
|
|
422
|
+
let updatedDevices
|
|
423
|
+
switch (tag) {
|
|
424
|
+
case 'add':
|
|
425
|
+
logger.info({ deviceHash, count: entries.length }, 'devices added')
|
|
426
|
+
updatedDevices = [...existingCache.filter(d => !affected.has(d.device)), ...entries.map(e => ({ user: e.user, server: e.server, device: e.device }))]
|
|
427
|
+
break
|
|
428
|
+
case 'remove':
|
|
429
|
+
logger.info({ deviceHash, count: entries.length }, 'devices removed')
|
|
430
|
+
updatedDevices = existingCache.filter(d => !affected.has(d.device))
|
|
431
|
+
break
|
|
432
|
+
default: logger.debug({ tag }, 'unknown device list change tag'); continue
|
|
433
|
+
}
|
|
434
|
+
if (updatedDevices.length === 0) await sock.userDevicesCache?.del(user)
|
|
435
|
+
else await sock.userDevicesCache?.set(user, updatedDevices)
|
|
436
|
+
}
|
|
437
|
+
})
|
|
372
438
|
}
|
|
373
439
|
|
|
374
440
|
const processNotification = async (node) => {
|
|
@@ -397,11 +463,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
397
463
|
await handleEncryptNotification(node)
|
|
398
464
|
break
|
|
399
465
|
case "devices":
|
|
400
|
-
|
|
401
|
-
if (areJidsSameUser(child.attrs.jid, authState.creds.me.id) || areJidsSameUser(child.attrs.lid, authState.creds.me.lid)) {
|
|
402
|
-
const deviceData = devices.map((d) => ({ id: d.attrs.jid, lid: d.attrs.lid }))
|
|
403
|
-
logger.info({ deviceData }, "my own devices changed")
|
|
404
|
-
}
|
|
466
|
+
try { await handleDevicesNotification(node) } catch (error) { logger.error({ error, node }, 'failed to handle devices notification') }
|
|
405
467
|
break
|
|
406
468
|
case "server_sync":
|
|
407
469
|
const update = getBinaryNodeChild(node, "collection")
|
|
@@ -462,6 +524,28 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
462
524
|
if (Object.keys(result).length) return result
|
|
463
525
|
}
|
|
464
526
|
|
|
527
|
+
const tcTokenKnownJids = new Set()
|
|
528
|
+
const tcTokenIndexLoaded = (async () => {
|
|
529
|
+
try { const jids = await readTcTokenIndex(authState.keys); for (const jid of jids) tcTokenKnownJids.add(jid); logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index') }
|
|
530
|
+
catch (err) { logger.warn({ err: err?.message }, 'failed to load tctoken index') }
|
|
531
|
+
})()
|
|
532
|
+
|
|
533
|
+
let tcTokenIndexTimer
|
|
534
|
+
async function flushTcTokenIndex() {
|
|
535
|
+
if (tcTokenIndexTimer) { clearTimeout(tcTokenIndexTimer); tcTokenIndexTimer = undefined }
|
|
536
|
+
const write = await buildMergedTcTokenIndexWrite(authState.keys, tcTokenKnownJids)
|
|
537
|
+
return authState.keys.set({ tctoken: write })
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function scheduleTcTokenIndexSave() {
|
|
541
|
+
if (tcTokenIndexTimer) clearTimeout(tcTokenIndexTimer)
|
|
542
|
+
tcTokenIndexTimer = setTimeout(() => { tcTokenIndexTimer = undefined; flushTcTokenIndex().catch(err => logger.warn({ err: err?.message }, 'failed to save tctoken index')) }, 5000)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function trackTcTokenJid(jid) {
|
|
546
|
+
if (jid && jid !== TC_TOKEN_INDEX_KEY && !tcTokenKnownJids.has(jid)) { tcTokenKnownJids.add(jid); scheduleTcTokenIndexSave() }
|
|
547
|
+
}
|
|
548
|
+
|
|
465
549
|
async function decipherLinkPublicKey(data) {
|
|
466
550
|
const buffer = toRequiredBuffer(data)
|
|
467
551
|
const salt = buffer.slice(0, 32)
|
|
@@ -488,10 +572,11 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
488
572
|
await msgRetryCache.set(key, newValue)
|
|
489
573
|
}
|
|
490
574
|
|
|
491
|
-
const sendMessagesAgain = async (key, ids, retryNode) => {
|
|
575
|
+
const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
|
|
492
576
|
const remoteJid = key.remoteJid
|
|
493
577
|
const participant = key.participant || remoteJid
|
|
494
578
|
const retryCount = +retryNode.attrs.count || 1
|
|
579
|
+
const msgId = ids[0]
|
|
495
580
|
const msgs = []
|
|
496
581
|
for (const id of ids) {
|
|
497
582
|
let msg
|
|
@@ -506,11 +591,35 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
506
591
|
msgs.push(msg)
|
|
507
592
|
}
|
|
508
593
|
const sendToAll = !jidDecode(participant)?.device
|
|
594
|
+
const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
|
|
595
|
+
let injectedFromBundle = false
|
|
596
|
+
const bundle = extractE2ESessionFromRetryReceipt(receiptNode)
|
|
597
|
+
if (bundle) {
|
|
598
|
+
try { await signalRepository.injectE2ESession({ jid: participant, session: bundle }); injectedFromBundle = true; logger.debug({ participant, retryCount }, 'injected session from retry receipt key bundle') }
|
|
599
|
+
catch (error) { logger.warn({ error, participant }, 'failed to inject session from retry receipt') }
|
|
600
|
+
}
|
|
601
|
+
if (!injectedFromBundle && typeof signalRepository.getSessionInfo === 'function') {
|
|
602
|
+
const receivedRegId = getBinaryNodeChildUInt(receiptNode, 'registration', 4)
|
|
603
|
+
if (typeof receivedRegId === 'number' && Number.isInteger(receivedRegId)) {
|
|
604
|
+
const info = await signalRepository.getSessionInfo(participant)
|
|
605
|
+
if (info && info.registrationId !== 0 && info.registrationId !== receivedRegId) { logger.info({ participant, stored: info.registrationId, received: receivedRegId }, 'reg id mismatch on retry without bundle, deleting session'); await authState.keys.set({ session: { [sessionId]: null } }) }
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const BASE_KEY_CHECK_RETRY = 2
|
|
609
|
+
if (msgId && messageRetryManager && typeof messageRetryManager.saveBaseKey === 'function') {
|
|
610
|
+
const info = typeof signalRepository.getSessionInfo === 'function' ? await signalRepository.getSessionInfo(participant) : undefined
|
|
611
|
+
if (info) {
|
|
612
|
+
if (retryCount === BASE_KEY_CHECK_RETRY) messageRetryManager.saveBaseKey(sessionId, msgId, info.baseKey)
|
|
613
|
+
else if (retryCount > BASE_KEY_CHECK_RETRY) {
|
|
614
|
+
if (messageRetryManager.hasSameBaseKey(sessionId, msgId, info.baseKey)) { logger.warn({ participant, retryCount }, 'base key collision on retry, forcing fresh session'); await authState.keys.set({ session: { [sessionId]: null } }) }
|
|
615
|
+
messageRetryManager.deleteBaseKey(sessionId, msgId)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
509
619
|
let shouldRecreateSession = false
|
|
510
620
|
let recreateReason = ""
|
|
511
|
-
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
|
|
621
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
|
|
512
622
|
try {
|
|
513
|
-
const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
|
|
514
623
|
const hasSession = await signalRepository.validateSession(participant)
|
|
515
624
|
const result = messageRetryManager.shouldRecreateSession(participant, retryCount, hasSession.exists)
|
|
516
625
|
shouldRecreateSession = result.recreate
|
|
@@ -518,9 +627,9 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
518
627
|
if (shouldRecreateSession) { logger.debug({ participant, retryCount, reason: recreateReason }, "recreating session for outgoing retry"); await authState.keys.set({ session: { [sessionId]: null } }) }
|
|
519
628
|
} catch (error) { logger.warn({ error, participant }, "failed to check session recreation for outgoing retry") }
|
|
520
629
|
}
|
|
521
|
-
await assertSessions([participant],
|
|
630
|
+
if (!injectedFromBundle) await assertSessions([participant], true)
|
|
522
631
|
if (isJidGroup(remoteJid)) await authState.keys.set({ "sender-key-memory": { [remoteJid]: null } })
|
|
523
|
-
logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason }, "preparing retry recp")
|
|
632
|
+
logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason, injectedFromBundle }, "preparing retry recp")
|
|
524
633
|
for (const [i, msg] of msgs.entries()) {
|
|
525
634
|
if (!ids[i]) continue
|
|
526
635
|
if (msg && (await willSendMessageAgain(ids[i], participant))) {
|
|
@@ -565,7 +674,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
565
674
|
try {
|
|
566
675
|
await updateSendMessageAgainCount(ids[0], key.participant)
|
|
567
676
|
logger.debug({ attrs, key }, "recv retry request")
|
|
568
|
-
await sendMessagesAgain(key, ids, retryNode)
|
|
677
|
+
await sendMessagesAgain(key, ids, retryNode, node)
|
|
569
678
|
} catch (error) { logger.error({ key, ids, trace: error instanceof Error ? error.stack : "Unknown error" }, "error in sending message again") }
|
|
570
679
|
} else logger.info({ attrs, key }, "recv retry for not fromMe message")
|
|
571
680
|
} else {
|
|
@@ -681,7 +790,14 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
681
790
|
cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid)
|
|
682
791
|
await upsertMessage(msg, node.attrs.offline ? "append" : "notify")
|
|
683
792
|
})
|
|
684
|
-
} catch (error) {
|
|
793
|
+
} catch (error) {
|
|
794
|
+
const isClosed = error?.message?.includes('Connection Closed') || error?.output?.statusCode === 428
|
|
795
|
+
if (isClosed) {
|
|
796
|
+
logger.debug({ msg: error?.message }, "Connection closed while handling message")
|
|
797
|
+
} else {
|
|
798
|
+
logger.error({ error, stack: error?.stack, msg: error?.message || String(error), node: binaryNodeToString(node) }, "error in handling message")
|
|
799
|
+
}
|
|
800
|
+
}
|
|
685
801
|
}
|
|
686
802
|
|
|
687
803
|
const handleCall = async (node) => {
|
|
@@ -700,11 +816,38 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
700
816
|
await sendMessageAck(node)
|
|
701
817
|
}
|
|
702
818
|
|
|
819
|
+
const inFlight463Recoveries = new Set()
|
|
820
|
+
|
|
703
821
|
const handleBadAck = async ({ attrs }) => {
|
|
704
822
|
const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id }
|
|
705
823
|
if (attrs.error) {
|
|
706
|
-
|
|
707
|
-
|
|
824
|
+
const isReachoutTimelocked = attrs.error === String(NACK_REASONS.SenderReachoutTimelocked)
|
|
825
|
+
if (attrs.error === SERVER_ERROR_CODES.MessageAccountRestriction) {
|
|
826
|
+
logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted or missing tctoken for contact')
|
|
827
|
+
const ackFrom = attrs.from
|
|
828
|
+
if (ackFrom && !inFlight463Recoveries.has(ackFrom)) {
|
|
829
|
+
inFlight463Recoveries.add(ackFrom)
|
|
830
|
+
void (async () => {
|
|
831
|
+
try {
|
|
832
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
|
|
833
|
+
const tcStorageJid = await resolveTcTokenJid(ackFrom, getLIDForPN)
|
|
834
|
+
const issueJid = await resolveIssuanceJid(ackFrom, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID)
|
|
835
|
+
const result = await issuePrivacyTokens([issueJid], unixTimestampSeconds())
|
|
836
|
+
await storeTcTokensFromIqResult({ result, fallbackJid: tcStorageJid, keys: authState.keys, getLIDForPN, onNewJidStored: trackTcTokenJid })
|
|
837
|
+
logger.debug({ from: ackFrom }, 'completed 463 token recovery issuance')
|
|
838
|
+
} catch (err) { logger.debug({ from: ackFrom, err: err?.message }, 'failed 463 token recovery issuance') }
|
|
839
|
+
finally { inFlight463Recoveries.delete(ackFrom) }
|
|
840
|
+
})()
|
|
841
|
+
}
|
|
842
|
+
} else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
|
|
843
|
+
logger.warn({ msgId: attrs.id, from: attrs.from }, 'smax-invalid (479): stale device session or malformed addressing')
|
|
844
|
+
} else if (isReachoutTimelocked) {
|
|
845
|
+
await fetchAccountReachoutTimelock().catch(err => logger.warn({ err }, 'failed to fetch reachout timelock'))
|
|
846
|
+
logger.warn({ attrs }, 'received error in ack')
|
|
847
|
+
} else {
|
|
848
|
+
logger.warn({ attrs }, 'received error in ack')
|
|
849
|
+
}
|
|
850
|
+
ev.emit("messages.update", [{ key, update: { status: WAMessageStatus.ERROR, messageStubParameters: isReachoutTimelocked ? [attrs.error, ACCOUNT_RESTRICTED_TEXT] : [attrs.error] } }])
|
|
708
851
|
}
|
|
709
852
|
}
|
|
710
853
|
|
|
@@ -770,8 +913,51 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
770
913
|
}
|
|
771
914
|
})
|
|
772
915
|
|
|
773
|
-
|
|
916
|
+
let lastTcTokenPruneTs = 0
|
|
917
|
+
|
|
918
|
+
async function pruneExpiredTcTokens() {
|
|
919
|
+
try {
|
|
920
|
+
await tcTokenIndexLoaded
|
|
921
|
+
const persisted = await readTcTokenIndex(authState.keys)
|
|
922
|
+
const allJids = new Set(tcTokenKnownJids)
|
|
923
|
+
for (const jid of persisted) allJids.add(jid)
|
|
924
|
+
if (!allJids.size) return
|
|
925
|
+
const jids = [...allJids]
|
|
926
|
+
const allTokens = await authState.keys.get('tctoken', jids)
|
|
927
|
+
const writes = {}, survivors = new Set()
|
|
928
|
+
let mutated = 0
|
|
929
|
+
for (const jid of jids) {
|
|
930
|
+
const entry = allTokens[jid]
|
|
931
|
+
if (!entry) { mutated++; continue }
|
|
932
|
+
const hasPeerToken = !!entry.token?.length
|
|
933
|
+
const peerTokenExpired = hasPeerToken && isTcTokenExpired(entry.timestamp)
|
|
934
|
+
const hasSenderTs = entry.senderTimestamp !== undefined
|
|
935
|
+
const senderTsExpired = hasSenderTs && isTcTokenExpired(entry.senderTimestamp)
|
|
936
|
+
const keepPeerToken = hasPeerToken && !peerTokenExpired
|
|
937
|
+
const keepSenderTs = hasSenderTs && !senderTsExpired
|
|
938
|
+
if (!keepPeerToken && !keepSenderTs) { writes[jid] = null; mutated++ }
|
|
939
|
+
else if (peerTokenExpired && keepSenderTs) { writes[jid] = { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp }; survivors.add(jid); mutated++ }
|
|
940
|
+
else survivors.add(jid)
|
|
941
|
+
}
|
|
942
|
+
if (mutated === 0) return
|
|
943
|
+
await authState.keys.set({ tctoken: { ...writes, [TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...survivors])) } } })
|
|
944
|
+
tcTokenKnownJids.clear()
|
|
945
|
+
for (const jid of survivors) tcTokenKnownJids.add(jid)
|
|
946
|
+
logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens')
|
|
947
|
+
} catch (err) { logger.warn({ err: err?.message }, 'failed to prune expired tctokens') }
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
ev.on("connection.update", ({ isOnline, connection }) => {
|
|
774
951
|
if (typeof isOnline !== "undefined") { sendActiveReceipts = isOnline; logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`) }
|
|
952
|
+
if (connection === 'close' && tcTokenIndexTimer) { clearTimeout(tcTokenIndexTimer); tcTokenIndexTimer = undefined; try { void Promise.resolve(flushTcTokenIndex()).catch(() => { }) } catch { } }
|
|
953
|
+
if (isOnline) { const now = Date.now(); const DAY_MS = 24 * 60 * 60 * 1000; if (now - lastTcTokenPruneTs >= DAY_MS) { lastTcTokenPruneTs = now; void pruneExpiredTcTokens() } }
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
registerSocketEndHandler(() => {
|
|
957
|
+
if (!config.msgRetryCounterCache && msgRetryCache.close) msgRetryCache.close()
|
|
958
|
+
if (!config.callOfferCache && callOfferCache.close) callOfferCache.close()
|
|
959
|
+
identityAssertDebounce.close()
|
|
960
|
+
sendActiveReceipts = false
|
|
775
961
|
})
|
|
776
962
|
|
|
777
963
|
return { ...sock, sendMessageAck, sendRetryRequest, rejectCall, offerCall, fetchMessageHistory, requestPlaceholderResend, messageRetryManager }
|