@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.
@@ -4,8 +4,7 @@ import * as Utils from '../Utils/index.js'
4
4
  import { proto } from '../../WAProto/index.js'
5
5
  import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js'
6
6
  import * as WABinary from '../WABinary/index.js'
7
- import { getUrlInfo, migrateIndexKey } from '../Utils/index.js'
8
- import { makeKeyedMutex } from '../Utils/make-mutex.js'
7
+ import { getUrlInfo, migrateIndexKey, getMessageReportingToken, shouldIncludeReportingToken, buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveTcTokenJid, resolveIssuanceJid, shouldSendNewTcToken, storeTcTokensFromIqResult, makeKeyedMutex, makeMutex } from '../Utils/index.js'
9
8
  import { USyncQuery, USyncUser } from '../WAUSync/index.js'
10
9
  import { makeNewsletterSocket } from './newsletter.js'
11
10
  import NexusHandler from './nexus-handler.js'
@@ -23,52 +22,11 @@ const {
23
22
  const {
24
23
  areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser,
25
24
  isJidBroadcast, isJidGroup, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET,
26
- getBinaryFilteredButtons, STORIES_JID, isJidUser, getButtonArgs, getButtonType
25
+ getBinaryFilteredButtons, STORIES_JID, isJidUser, getButtonArgs, getButtonType, isJidBot, isJidMetaAI
27
26
  } = WABinary
28
27
 
29
- const TC_TOKEN_BUCKET_DURATION = 604800 // 7 days
30
- const TC_TOKEN_NUM_BUCKETS = 4 // ~28-day rolling window
31
-
32
- const isTcTokenExpired = (timestamp) => {
33
- if (timestamp === null || timestamp === undefined) return true
34
- const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
35
- if (isNaN(ts)) return true
36
- const now = Math.floor(Date.now() / 1000)
37
- const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION)
38
- const cutoffBucket = currentBucket - (TC_TOKEN_NUM_BUCKETS - 1)
39
- return ts < (cutoffBucket * TC_TOKEN_BUCKET_DURATION)
40
- }
41
-
42
- const shouldSendNewTcToken = (senderTimestamp) => {
43
- if (senderTimestamp === undefined) return true
44
- const now = Math.floor(Date.now() / 1000)
45
- const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION)
46
- const senderBucket = Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION)
47
- return currentBucket > senderBucket
48
- }
49
-
50
- const resolveTcTokenJid = async (jid, getLIDForPN) => {
51
- if (isLidUser(jid)) return jid
52
- const lid = await getLIDForPN(jid)
53
- return lid ?? jid
54
- }
55
-
56
- const resolveIssuanceJid = async (jid, issueToLid, getLIDForPN, getPNForLID) => {
57
- if (issueToLid) {
58
- if (isLidUser(jid)) return jid
59
- return (await getLIDForPN(jid)) ?? jid
60
- }
61
- if (!isLidUser(jid)) return jid
62
- if (getPNForLID) return (await getPNForLID(jid)) ?? jid
63
- return jid
64
- }
65
-
66
28
  export const makeMessagesSocket = (config) => {
67
- const {
68
- logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview,
69
- options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata,
70
- enableRecentMessageCache, maxMsgRetryCount
71
- } = config
29
+ const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount, getMessage } = config
72
30
 
73
31
  const sock = makeNewsletterSocket(config)
74
32
  const {
@@ -77,7 +35,7 @@ export const makeMessagesSocket = (config) => {
77
35
  } = sock
78
36
 
79
37
  const userDevicesCache = config.userDevicesCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
80
- const peerSessionsCache = new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
38
+ const devicesMutex = makeMutex()
81
39
  const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null
82
40
  const encryptionMutex = makeKeyedMutex()
83
41
 
@@ -111,9 +69,19 @@ export const makeMessagesSocket = (config) => {
111
69
  const node = { tag: 'receipt', attrs: { id: messageIds[0] } }
112
70
  const isReadReceipt = type === 'read' || type === 'read-self'
113
71
  if (isReadReceipt) node.attrs.t = unixTimestampSeconds().toString()
72
+ if (isJidStatusBroadcast(jid) && !participant && getMessage) {
73
+ try {
74
+ const msg = await getMessage({ remoteJid: jid, id: messageIds[0], fromMe: false })
75
+ participant = msg?.key?.participant || msg?.participant || msg?.key?.remoteJid
76
+ logger.debug({ jid, resolvedParticipant: participant }, 'resolved status receipt participant from message store')
77
+ } catch (err) { logger.debug({ err, jid }, 'failed to resolve status receipt participant') }
78
+ }
114
79
  if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) {
115
80
  node.attrs.recipient = jid
116
81
  node.attrs.to = participant
82
+ } else if (isJidStatusBroadcast(jid) && participant) {
83
+ node.attrs.to = jid
84
+ node.attrs.participant = participant
117
85
  } else {
118
86
  node.attrs.to = jid
119
87
  if (participant) node.attrs.participant = participant
@@ -135,7 +103,9 @@ export const makeMessagesSocket = (config) => {
135
103
 
136
104
  const readMessages = async (keys) => {
137
105
  const privacySettings = await fetchPrivacySettings()
138
- await sendReceipts(keys, privacySettings.readreceipts === 'all' ? 'read' : 'read-self')
106
+ const hasStatusKey = keys.some(k => isJidStatusBroadcast(k.remoteJid))
107
+ const type = hasStatusKey ? 'read' : (privacySettings.readreceipts === 'all' ? 'read' : 'read-self')
108
+ await sendReceipts(keys, type)
139
109
  }
140
110
 
141
111
  const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
@@ -212,11 +182,13 @@ export const makeMessagesSocket = (config) => {
212
182
  }
213
183
  }
214
184
 
215
- if (userDevicesCache.mset) {
216
- await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
217
- } else {
218
- for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
219
- }
185
+ await devicesMutex.mutex(async () => {
186
+ if (userDevicesCache.mset) {
187
+ await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
188
+ } else {
189
+ for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
190
+ }
191
+ })
220
192
 
221
193
  // Persist device lists for session migration (capped at 500 users)
222
194
  const userDeviceUpdates = {}
@@ -243,16 +215,20 @@ export const makeMessagesSocket = (config) => {
243
215
  if (force) {
244
216
  jidsRequiringFetch = jids
245
217
  } else {
246
- // assertSessions
247
- const sessionBatch = await migrateIndexKey(authState.keys, 'session') // sessions live in index blob, not individual files
218
+ const sessionBatch = await migrateIndexKey(authState.keys, 'session')
248
219
  for (const jid of jids) {
249
220
  const signalId = signalRepository.jidToSignalProtocolAddress(jid)
250
221
  if (!sessionBatch[signalId]) jidsRequiringFetch.push(jid)
251
222
  }
252
223
  }
253
224
  if (jidsRequiringFetch.length) {
254
- logger.debug({ jidsRequiringFetch }, 'fetching sessions')
255
- const result = await query({ tag: 'iq', attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET }, content: [{ tag: 'key', attrs: {}, content: jidsRequiringFetch.map(jid => ({ tag: 'user', attrs: { jid } })) }] })
225
+ const wireJids = [
226
+ ...jidsRequiringFetch.filter(jid => isLidUser(jid) || isHostedLidUser(jid)),
227
+ ...((await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => isPnUser(jid) || isHostedPnUser(jid)))) || []).map(a => a.lid)
228
+ ]
229
+ const fetchTargets = wireJids.length ? wireJids : jidsRequiringFetch
230
+ logger.debug({ jidsRequiringFetch, fetchTargets }, 'fetching sessions')
231
+ const result = await query({ tag: 'iq', attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET }, content: [{ tag: 'key', attrs: {}, content: fetchTargets.map(jid => ({ tag: 'user', attrs: { jid, ...(force ? { reason: 'identity' } : {}) } })) }] })
256
232
  await parseAndInjectE2ESessions(result, signalRepository)
257
233
  didFetchNewSession = true
258
234
  }
@@ -433,6 +409,7 @@ export const makeMessagesSocket = (config) => {
433
409
  const meMsg = { deviceSentMessage: { destinationJid, message }, messageContextInfo: message.messageContextInfo }
434
410
  const extraAttrs = {}
435
411
  const messages = normalizeMessageContent(message)
412
+ const reportingMessage = messages
436
413
  const buttonType = getButtonType(messages)
437
414
 
438
415
  let hasDeviceFanoutFalse = false
@@ -454,7 +431,7 @@ export const makeMessagesSocket = (config) => {
454
431
  return
455
432
  }
456
433
 
457
- if (messages?.pinInChatMessage || messages?.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) {
434
+ if (messages?.pinInChatMessage || messages?.keepInChatMessage || (message.reactionMessage && !isStatus) || message.protocolMessage?.editedMessage) {
458
435
  extraAttrs['decrypt-fail'] = 'hide'
459
436
  }
460
437
 
@@ -497,9 +474,10 @@ export const makeMessagesSocket = (config) => {
497
474
  if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
498
475
 
499
476
  const bytes = encodeWAMessage(patched)
477
+ const bytesU8 = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
500
478
  const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
501
479
  const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
502
- const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
480
+ const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytesU8, meId: groupSenderIdentity })
503
481
 
504
482
  const senderKeyRecipients = devices
505
483
  .filter(d => !isHostedLidUser(d.jid) && !isHostedPnUser(d.jid) && d.device !== 99)
@@ -527,9 +505,10 @@ export const makeMessagesSocket = (config) => {
527
505
  if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
528
506
 
529
507
  const bytes = encodeWAMessage(patched)
508
+ const bytesU8 = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
530
509
  const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
531
510
  const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
532
- const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
511
+ const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytesU8, meId: groupSenderIdentity })
533
512
 
534
513
  const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
535
514
  await assertSessions([participant.jid])
@@ -657,48 +636,64 @@ export const makeMessagesSocket = (config) => {
657
636
  logger.debug({ jid }, 'adding device identity')
658
637
  }
659
638
 
660
- // TC token handling for 1:1 messages
661
639
  const isPeerMessage = additionalAttributes?.category === 'peer'
662
640
  const is1on1 = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage
663
641
  if (is1on1) {
664
642
  const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
665
643
  const tcTokenJid = await resolveTcTokenJid(destinationJid, getLIDForPN)
666
-
667
644
  const contactTcTokenData = await authState.keys.get('tctoken', [tcTokenJid])
668
645
  const existingEntry = contactTcTokenData[tcTokenJid]
669
646
  let tcTokenBuffer = existingEntry?.token
670
-
671
- // Clear expired tokens
672
647
  if (tcTokenBuffer?.length && isTcTokenExpired(existingEntry?.timestamp)) {
673
- logger.debug({ jid: destinationJid }, 'tctoken expired, clearing')
648
+ logger.debug({ jid: destinationJid, timestamp: existingEntry?.timestamp }, 'tctoken expired, clearing')
674
649
  tcTokenBuffer = undefined
675
- try {
676
- await authState.keys.set({ tctoken: { [tcTokenJid]: existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null } })
677
- } catch { }
650
+ const cleared = existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null
651
+ try { await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } }) } catch (err) { logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup') }
678
652
  }
679
-
680
- if (tcTokenBuffer?.length) {
681
- stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
682
- }
683
-
684
- // Fire-and-forget: issue our token to the contact after send
653
+ if (tcTokenBuffer?.length && sock.serverProps?.privacyTokenOn1to1) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
685
654
  const isProtocolMsg = !!normalizeMessageContent(message)?.protocolMessage
686
- if (!isProtocolMsg && shouldSendNewTcToken(existingEntry?.senderTimestamp) && !inFlightTcTokenIssuance.has(tcTokenJid)) {
655
+ const isBotOrPSA = isJidBot(destinationJid) || isJidMetaAI(destinationJid)
656
+ if (!isProtocolMsg && !isBotOrPSA && shouldSendNewTcToken(existingEntry?.senderTimestamp) && !inFlightTcTokenIssuance.has(tcTokenJid)) {
687
657
  inFlightTcTokenIssuance.add(tcTokenJid)
688
658
  const issueTimestamp = unixTimestampSeconds()
689
659
  const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
690
660
  const issueToLid = sock.serverProps?.lidTrustedTokenIssueToLid ?? false
691
661
  resolveIssuanceJid(destinationJid, issueToLid, getLIDForPN, getPNForLID)
692
662
  .then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
693
- .then(async () => {
663
+ .then(async (result) => {
664
+ await storeTcTokensFromIqResult({ result, fallbackJid: tcTokenJid, keys: authState.keys, getLIDForPN })
694
665
  const currentData = await authState.keys.get('tctoken', [tcTokenJid])
695
- const current = currentData[tcTokenJid]
696
- await authState.keys.set({ tctoken: { [tcTokenJid]: { ...current, senderTimestamp: issueTimestamp } } })
666
+ const currentEntry = currentData[tcTokenJid]
667
+ const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid])
668
+ await authState.keys.set({ tctoken: { [tcTokenJid]: { token: Buffer.alloc(0), ...currentEntry, senderTimestamp: issueTimestamp }, ...indexWrite } })
697
669
  })
698
670
  .catch(err => logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed'))
699
671
  .finally(() => inFlightTcTokenIssuance.delete(tcTokenJid))
700
672
  }
701
673
  }
674
+ if (
675
+ !isNewsletter &&
676
+ !isRetryResend &&
677
+ reportingMessage?.messageContextInfo?.messageSecret &&
678
+ shouldIncludeReportingToken(reportingMessage)
679
+ ) {
680
+ try {
681
+ const encoded = encodeWAMessage(reportingMessage)
682
+ const reportingKey = {
683
+ id: finalMsgId,
684
+ fromMe: true,
685
+ remoteJid: destinationJid,
686
+ participant: participant?.jid
687
+ }
688
+ const reportingNode = await getMessageReportingToken(encoded, reportingMessage, reportingKey)
689
+ if (reportingNode) {
690
+ stanza.content.push(reportingNode)
691
+ logger.trace({ jid }, 'added reporting token to message')
692
+ }
693
+ } catch (error) {
694
+ logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token')
695
+ }
696
+ }
702
697
 
703
698
  logger.debug({ msgId: finalMsgId }, `sending message to ${participants.length} devices`)
704
699
  await sendNode(stanza)
@@ -749,7 +744,9 @@ export const makeMessagesSocket = (config) => {
749
744
  content = { ...rest, interactiveMessage: interactive }
750
745
  }
751
746
 
752
- const messageType = nexus.detectType(content)
747
+ // never route status reactions through nexus
748
+ const isStatusJid = jid === 'status@broadcast'
749
+ const messageType = !isStatusJid && nexus.detectType(content)
753
750
  if (messageType) return await nexus.processMessage(content, jid, quoted)
754
751
 
755
752
  if (content.disappearingMessagesInChat && isJidGroup(jid)) {
@@ -760,35 +757,6 @@ export const makeMessagesSocket = (config) => {
760
757
  return
761
758
  }
762
759
 
763
- if (content.delete) {
764
- const deleteKey = content.delete
765
- if (!deleteKey.remoteJid || !deleteKey.id) {
766
- logger.error({ deleteKey }, 'Invalid delete key: missing remoteJid or id')
767
- throw new Boom('Delete key must have remoteJid and id', { statusCode: 400 })
768
- }
769
-
770
- const { server: deleteServer } = jidDecode(deleteKey.remoteJid)
771
- let deleteAddressingMode = deleteServer === 'lid' ? 'lid' : 'pn'
772
- if (isJidGroup(deleteKey.remoteJid)) {
773
- const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(deleteKey.remoteJid) : undefined
774
- deleteAddressingMode = groupData?.addressingMode || 'lid'
775
- }
776
-
777
- let normalizedParticipant = deleteKey.participant
778
- if (deleteKey.fromMe || isJidGroup(deleteKey.remoteJid)) {
779
- const senderJid = (deleteAddressingMode === 'lid' && meLid) ? meLid : meId
780
- normalizedParticipant = jidNormalizedUser(senderJid)
781
- }
782
-
783
- content.delete = {
784
- remoteJid: deleteKey.remoteJid,
785
- fromMe: deleteKey.fromMe === true || deleteKey.fromMe === 'true',
786
- id: deleteKey.id,
787
- ...(normalizedParticipant ? { participant: jidNormalizedUser(normalizedParticipant) } : {}),
788
- addressingMode: deleteAddressingMode
789
- }
790
- logger.debug({ jid, deleteKey: content.delete }, 'processing message deletion')
791
- }
792
760
 
793
761
  const fullMsg = await generateWAMessage(jid, content, {
794
762
  logger,
@@ -805,7 +773,9 @@ export const makeMessagesSocket = (config) => {
805
773
 
806
774
  const additionalAttributes = {}, additionalNodes = []
807
775
  if (content.delete) {
808
- additionalAttributes.edit = isJidGroup(content.delete.remoteJid) ? '8' : '7'
776
+ const fromMe = content.delete?.fromMe
777
+ const isGroupDelete = isJidGroup(content.delete?.remoteJid)
778
+ additionalAttributes.edit = (isGroupDelete && !fromMe) ? '8' : '7'
809
779
  } else if (content.edit) {
810
780
  additionalAttributes.edit = '1'
811
781
  } else if (content.pin) {
@@ -815,15 +785,6 @@ export const makeMessagesSocket = (config) => {
815
785
  if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
816
786
  if (content.ai) additionalNodes.push({ tag: 'bot', attrs: { biz_bot: '1' } })
817
787
 
818
- // Pre-fetch TC token for new DM contacts
819
- if (!isJidGroup(jid) && !isJidStatusBroadcast(jid) && !isJidBroadcast(jid) && !content.disappearingMessagesInChat) {
820
- const existingToken = await authState.keys.get('tctoken', [jid])
821
- if (!existingToken[jid]) {
822
- try { await getPrivacyTokens([jid]); logger.debug({ jid }, 'fetched tctoken for new contact') }
823
- catch (err) { logger.warn({ jid, err }, 'failed to fetch tctoken') }
824
- }
825
- }
826
-
827
788
  await relayMessage(jid, fullMsg.message, {
828
789
  messageId: fullMsg.key.id,
829
790
  useCachedGroupMetadata: options.useCachedGroupMetadata,
@@ -844,6 +805,7 @@ export const makeMessagesSocket = (config) => {
844
805
  sendReceipt, sendReceipts, nexus, readMessages, refreshMediaConn,
845
806
  waUploadToServer, fetchPrivacySettings, sendPeerDataOperationMessage,
846
807
  createParticipantNodes, getUSyncDevices, messageRetryManager, updateMemberLabel,
808
+ userDevicesCache, devicesMutex,
847
809
 
848
810
  updateMediaMessage: async (message) => {
849
811
  const content = assertMediaContent(message.message)