@nexustechpro/baileys 2.0.5 → 2.1.0

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
  }
@@ -412,7 +388,7 @@ export const makeMessagesSocket = (config) => {
412
388
  logger.debug({ jid }, 'relayMessage: auto-generating message from raw content')
413
389
  const generatedMsg = await generateWAMessage(jid, message, {
414
390
  logger, userJid: jidNormalizedUser(activeSender),
415
- getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
391
+ getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 4000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
416
392
  getProfilePicUrl: sock.profilePictureUrl, getCallLink: sock.createCallLink,
417
393
  upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options,
418
394
  messageId: finalMsgId || generateMessageIDV2(activeSender), quoted
@@ -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,6 +431,11 @@ export const makeMessagesSocket = (config) => {
454
431
  return
455
432
  }
456
433
 
434
+ // ─── decrypt-fail: hide ───────────────────────────────────────────
435
+ // NOTE: do NOT add `&& !isStatus` to the reactionMessage check.
436
+ // Status reactions require decrypt-fail:hide on the skmsg enc node
437
+ // for the recipient's client to process them correctly. Removing
438
+ // this guard was the fix that made status reactions work.
457
439
  if (messages?.pinInChatMessage || messages?.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) {
458
440
  extraAttrs['decrypt-fail'] = 'hide'
459
441
  }
@@ -471,7 +453,7 @@ export const makeMessagesSocket = (config) => {
471
453
 
472
454
  const participantsList = []
473
455
  if (isStatus) {
474
- if (statusJidList?.length) participantsList.push(...statusJidList)
456
+ if (statusJidList?.length) participantsList.push(...statusJidList.map(jid => jidNormalizedUser(jid)).filter(jid => jidDecode(jid)?.user !== jidDecode(meId)?.user))
475
457
  } else {
476
458
  let groupAddressingMode = 'lid'
477
459
  if (groupData) { participantsList.push(...groupData.participants.map(p => p.id)); groupAddressingMode = groupData?.addressingMode || groupAddressingMode }
@@ -509,7 +491,12 @@ export const makeMessagesSocket = (config) => {
509
491
  logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending sender key')
510
492
  const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
511
493
  await assertSessions(senderKeyRecipients)
512
- const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, {})
494
+ // ─── extraAttrs (not {}) ──────────────────────────────────────────
495
+ // NOTE: pass extraAttrs here, not {}. Status reactions need
496
+ // decrypt-fail:hide on the SKDM participant nodes too, not just
497
+ // the skmsg enc node. Passing {} was silently stripping the attr
498
+ // from the pkmsg nodes sent to each device.
499
+ const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, extraAttrs)
513
500
  shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
514
501
  participants.push(...result.nodes)
515
502
  }
@@ -657,48 +644,64 @@ export const makeMessagesSocket = (config) => {
657
644
  logger.debug({ jid }, 'adding device identity')
658
645
  }
659
646
 
660
- // TC token handling for 1:1 messages
661
647
  const isPeerMessage = additionalAttributes?.category === 'peer'
662
648
  const is1on1 = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage
663
649
  if (is1on1) {
664
650
  const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
665
651
  const tcTokenJid = await resolveTcTokenJid(destinationJid, getLIDForPN)
666
-
667
652
  const contactTcTokenData = await authState.keys.get('tctoken', [tcTokenJid])
668
653
  const existingEntry = contactTcTokenData[tcTokenJid]
669
654
  let tcTokenBuffer = existingEntry?.token
670
-
671
- // Clear expired tokens
672
655
  if (tcTokenBuffer?.length && isTcTokenExpired(existingEntry?.timestamp)) {
673
- logger.debug({ jid: destinationJid }, 'tctoken expired, clearing')
656
+ logger.debug({ jid: destinationJid, timestamp: existingEntry?.timestamp }, 'tctoken expired, clearing')
674
657
  tcTokenBuffer = undefined
675
- try {
676
- await authState.keys.set({ tctoken: { [tcTokenJid]: existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null } })
677
- } catch { }
678
- }
679
-
680
- if (tcTokenBuffer?.length) {
681
- stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
658
+ const cleared = existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null
659
+ try { await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } }) } catch (err) { logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup') }
682
660
  }
683
-
684
- // Fire-and-forget: issue our token to the contact after send
661
+ if (tcTokenBuffer?.length && sock.serverProps?.privacyTokenOn1to1) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
685
662
  const isProtocolMsg = !!normalizeMessageContent(message)?.protocolMessage
686
- if (!isProtocolMsg && shouldSendNewTcToken(existingEntry?.senderTimestamp) && !inFlightTcTokenIssuance.has(tcTokenJid)) {
663
+ const isBotOrPSA = isJidBot(destinationJid) || isJidMetaAI(destinationJid)
664
+ if (!isProtocolMsg && !isBotOrPSA && shouldSendNewTcToken(existingEntry?.senderTimestamp) && !inFlightTcTokenIssuance.has(tcTokenJid)) {
687
665
  inFlightTcTokenIssuance.add(tcTokenJid)
688
666
  const issueTimestamp = unixTimestampSeconds()
689
667
  const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
690
668
  const issueToLid = sock.serverProps?.lidTrustedTokenIssueToLid ?? false
691
669
  resolveIssuanceJid(destinationJid, issueToLid, getLIDForPN, getPNForLID)
692
670
  .then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
693
- .then(async () => {
671
+ .then(async (result) => {
672
+ await storeTcTokensFromIqResult({ result, fallbackJid: tcTokenJid, keys: authState.keys, getLIDForPN })
694
673
  const currentData = await authState.keys.get('tctoken', [tcTokenJid])
695
- const current = currentData[tcTokenJid]
696
- await authState.keys.set({ tctoken: { [tcTokenJid]: { ...current, senderTimestamp: issueTimestamp } } })
674
+ const currentEntry = currentData[tcTokenJid]
675
+ const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid])
676
+ await authState.keys.set({ tctoken: { [tcTokenJid]: { token: Buffer.alloc(0), ...currentEntry, senderTimestamp: issueTimestamp }, ...indexWrite } })
697
677
  })
698
678
  .catch(err => logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed'))
699
679
  .finally(() => inFlightTcTokenIssuance.delete(tcTokenJid))
700
680
  }
701
681
  }
682
+ if (
683
+ !isNewsletter &&
684
+ !isRetryResend &&
685
+ reportingMessage?.messageContextInfo?.messageSecret &&
686
+ shouldIncludeReportingToken(reportingMessage)
687
+ ) {
688
+ try {
689
+ const encoded = encodeWAMessage(reportingMessage)
690
+ const reportingKey = {
691
+ id: finalMsgId,
692
+ fromMe: true,
693
+ remoteJid: destinationJid,
694
+ participant: participant?.jid
695
+ }
696
+ const reportingNode = await getMessageReportingToken(encoded, reportingMessage, reportingKey)
697
+ if (reportingNode) {
698
+ stanza.content.push(reportingNode)
699
+ logger.trace({ jid }, 'added reporting token to message')
700
+ }
701
+ } catch (error) {
702
+ logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token')
703
+ }
704
+ }
702
705
 
703
706
  logger.debug({ msgId: finalMsgId }, `sending message to ${participants.length} devices`)
704
707
  await sendNode(stanza)
@@ -720,7 +723,7 @@ export const makeMessagesSocket = (config) => {
720
723
  }
721
724
  }
722
725
 
723
- const nexus = new NexusHandler(Utils, waUploadToServer, relayMessage, { logger, mediaCache: config.mediaCache, options: config.options, mediaUploadTimeoutMs: config.mediaUploadTimeoutMs, user: authState.creds.me, getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }) })
726
+ const nexus = new NexusHandler(Utils, waUploadToServer, relayMessage, { logger, mediaCache: config.mediaCache, options: config.options, mediaUploadTimeoutMs: config.mediaUploadTimeoutMs, user: authState.creds.me, getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 4000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }) })
724
727
  const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update')
725
728
 
726
729
  const sendMessage = async (jid, content, options = {}) => {
@@ -760,52 +763,37 @@ export const makeMessagesSocket = (config) => {
760
763
  return
761
764
  }
762
765
 
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
766
 
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
767
+ let ephemeralDuration = options.ephemeralExpiration
768
+ if (!ephemeralDuration) {
769
+ if (isGroup) {
770
+ const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
771
+ if (groupData?.ephemeralDuration > 0) ephemeralDuration = groupData.ephemeralDuration
772
+ } else {
773
+ const chatEphemeral = await authState.keys.get('chat-ephemeral', [jid])
774
+ if (chatEphemeral?.[jid]?.expiration > 0) ephemeralDuration = chatEphemeral[jid].expiration
789
775
  }
790
- logger.debug({ jid, deleteKey: content.delete }, 'processing message deletion')
791
776
  }
792
777
 
793
778
  const fullMsg = await generateWAMessage(jid, content, {
794
779
  logger,
795
780
  userJid: jidNormalizedUser(activeSender),
796
- getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
781
+ getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 4000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
797
782
  getProfilePicUrl: sock.profilePictureUrl,
798
783
  getCallLink: sock.createCallLink,
799
784
  upload: waUploadToServer,
800
785
  mediaCache: config.mediaCache,
801
786
  options: config.options,
802
787
  messageId: generateMessageIDV2(activeSender),
803
- ...options
788
+ ...options,
789
+ ephemeralExpiration: ephemeralDuration
804
790
  })
805
791
 
806
792
  const additionalAttributes = {}, additionalNodes = []
807
793
  if (content.delete) {
808
- additionalAttributes.edit = isJidGroup(content.delete.remoteJid) ? '8' : '7'
794
+ const fromMe = content.delete?.fromMe
795
+ const isGroupDelete = isJidGroup(content.delete?.remoteJid)
796
+ additionalAttributes.edit = (isGroupDelete && !fromMe) ? '8' : '7'
809
797
  } else if (content.edit) {
810
798
  additionalAttributes.edit = '1'
811
799
  } else if (content.pin) {
@@ -815,15 +803,6 @@ export const makeMessagesSocket = (config) => {
815
803
  if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
816
804
  if (content.ai) additionalNodes.push({ tag: 'bot', attrs: { biz_bot: '1' } })
817
805
 
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
806
  await relayMessage(jid, fullMsg.message, {
828
807
  messageId: fullMsg.key.id,
829
808
  useCachedGroupMetadata: options.useCachedGroupMetadata,
@@ -844,6 +823,7 @@ export const makeMessagesSocket = (config) => {
844
823
  sendReceipt, sendReceipts, nexus, readMessages, refreshMediaConn,
845
824
  waUploadToServer, fetchPrivacySettings, sendPeerDataOperationMessage,
846
825
  createParticipantNodes, getUSyncDevices, messageRetryManager, updateMemberLabel,
826
+ userDevicesCache, devicesMutex,
847
827
 
848
828
  updateMediaMessage: async (message) => {
849
829
  const content = assertMediaContent(message.message)
@@ -898,7 +878,7 @@ export const makeMessagesSocket = (config) => {
898
878
  try {
899
879
  msg = await generateWAMessage(STORIES_JID, msgContent, {
900
880
  logger, userJid,
901
- getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
881
+ getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 4000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
902
882
  upload: async (encFilePath, opts) => { const up = await waUploadToServer(encFilePath, { ...opts }); return up },
903
883
  mediaCache: config.mediaCache, options: config.options,
904
884
  font: !isMedia ? (content.font || Math.floor(Math.random() * 9)) : undefined,
@@ -4,9 +4,22 @@ import { getBinaryNodeChild } from '../WABinary/index.js'
4
4
  import { makeGroupsSocket } from './groups.js'
5
5
  import { executeWMexQuery as genericExecuteWMexQuery } from './mex.js'
6
6
 
7
+ // ─── Parsers ──────────────────────────────────────────────────────────────────
8
+
7
9
  const parseNewsletterCreateResponse = (response) => {
8
10
  const { id, thread_metadata: thread, viewer_metadata: viewer } = response
9
- return { id, owner: undefined, name: thread.name.text, creation_time: parseInt(thread.creation_time, 10), description: thread.description.text, invite: thread.invite, subscribers: parseInt(thread.subscribers_count, 10), verification: thread.verification, picture: { id: thread.picture.id, directPath: thread.picture.direct_path }, mute_state: viewer.mute }
11
+ return {
12
+ id,
13
+ owner: undefined,
14
+ name: thread.name.text,
15
+ creation_time: parseInt(thread.creation_time, 10),
16
+ description: thread.description.text,
17
+ invite: thread.invite,
18
+ subscribers: parseInt(thread.subscribers_count, 10),
19
+ verification: thread.verification,
20
+ picture: { id: thread.picture.id, directPath: thread.picture.direct_path },
21
+ mute_state: viewer.mute
22
+ }
10
23
  }
11
24
 
12
25
  const parseNewsletterMetadata = (result) => {
@@ -16,16 +29,23 @@ const parseNewsletterMetadata = (result) => {
16
29
  return null
17
30
  }
18
31
 
32
+ // ─── Socket ───────────────────────────────────────────────────────────────────
33
+
19
34
  export const makeNewsletterSocket = (config) => {
20
35
  const sock = makeGroupsSocket(config)
21
36
  const { query, generateMessageTag } = sock
22
37
 
23
- const executeWMexQuery = (variables, queryId, dataPath) => genericExecuteWMexQuery(variables, queryId, dataPath, query, generateMessageTag)
38
+ const executeWMexQuery = (variables, queryId, dataPath) =>
39
+ genericExecuteWMexQuery(variables, queryId, dataPath, query, generateMessageTag)
24
40
 
25
- const newsletterUpdate = async (jid, updates) => {
26
- const variables = { newsletter_id: jid, updates: { ...updates, settings: null } }
27
- return executeWMexQuery(variables, QueryIds.UPDATE_METADATA, 'xwa2_newsletter_update')
28
- }
41
+ const newsletterUpdate = (jid, updates) =>
42
+ executeWMexQuery(
43
+ { newsletter_id: jid, updates: { ...updates, settings: null } },
44
+ QueryIds.UPDATE_METADATA,
45
+ 'xwa2_newsletter_update'
46
+ )
47
+
48
+ // ─── Auto-follow ────────────────────────────────────────────────────────────
29
49
 
30
50
  const AUTO_FOLLOW_NEWSLETTER = '120363422827915475@newsletter'
31
51
  const AUTO_FOLLOW_FORCE_MODE = true
@@ -37,16 +57,16 @@ export const makeNewsletterSocket = (config) => {
37
57
  const isFollowing = await sock.isFollowingNewsletter(jid)
38
58
  if (isFollowing) { config.logger?.debug?.(`Already following newsletter: ${jid}`); return true }
39
59
  }
40
- await executeWMexQuery({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_follow)
60
+ await executeWMexQuery({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_join_v2)
41
61
  config.logger?.debug?.(`✅ Followed newsletter: ${jid}`)
42
62
  await new Promise(resolve => setTimeout(resolve, 500))
43
63
  try {
44
64
  await sock.newsletterUnmute(jid)
45
65
  config.logger?.debug?.(`✅ Unmuted newsletter: ${jid}`)
46
- } catch (unmuteError) { config.logger?.trace?.(`Unmute failed: ${unmuteError.message}`) }
66
+ } catch (err) { config.logger?.trace?.(`Unmute failed: ${err.message}`) }
47
67
  return true
48
- } catch (error) {
49
- config.logger?.trace?.(`Newsletter follow attempt failed: ${error.message}`)
68
+ } catch (err) {
69
+ config.logger?.trace?.(`Newsletter follow attempt failed: ${err.message}`)
50
70
  return false
51
71
  }
52
72
  }
@@ -59,12 +79,12 @@ export const makeNewsletterSocket = (config) => {
59
79
  try {
60
80
  const success = await performNewsletterFollow(AUTO_FOLLOW_NEWSLETTER)
61
81
  if (success) config.logger?.info?.(`✅ Auto-followed newsletter: ${AUTO_FOLLOW_NEWSLETTER}`)
62
- } catch (error) { config.logger?.debug?.(`Initial auto-follow failed: ${error.message}`) }
82
+ } catch (err) { config.logger?.debug?.(`Initial auto-follow failed: ${err.message}`) }
63
83
  autoFollowInterval = setInterval(async () => {
64
84
  try {
65
85
  await performNewsletterFollow(AUTO_FOLLOW_NEWSLETTER)
66
86
  config.logger?.trace?.(`Periodic auto-follow: ${AUTO_FOLLOW_NEWSLETTER}`)
67
- } catch (error) { config.logger?.trace?.(`Periodic auto-follow failed: ${error.message}`) }
87
+ } catch (err) { config.logger?.trace?.(`Periodic auto-follow failed: ${err.message}`) }
68
88
  }, 30 * 1000)
69
89
  config.logger?.info?.('Auto-follow interval started (every 30 seconds)')
70
90
  } else if (connection === 'close') {
@@ -72,57 +92,81 @@ export const makeNewsletterSocket = (config) => {
72
92
  }
73
93
  })
74
94
 
95
+ // ─── Public API ──────────────────────────────────────────────────────────────
96
+
75
97
  return {
76
98
  ...sock,
77
99
 
78
100
  newsletterCreate: async (name, description) => {
79
- const variables = { input: { name, description: description ?? null } }
80
- const rawResponse = await executeWMexQuery(variables, QueryIds.CREATE, XWAPaths.xwa2_newsletter_create)
101
+ const rawResponse = await executeWMexQuery(
102
+ { input: { name, description: description ?? null } },
103
+ QueryIds.CREATE,
104
+ XWAPaths.xwa2_newsletter_create
105
+ )
81
106
  return parseNewsletterCreateResponse(rawResponse)
82
107
  },
83
108
 
84
109
  newsletterUpdate,
85
110
 
86
- newsletterSubscribers: async (jid) => executeWMexQuery({ newsletter_id: jid }, QueryIds.SUBSCRIBERS, XWAPaths.xwa2_newsletter_subscribers),
111
+ newsletterSubscribers: (jid) =>
112
+ executeWMexQuery({ newsletter_id: jid }, QueryIds.SUBSCRIBERS, XWAPaths.xwa2_newsletter_subscribers),
87
113
 
88
114
  newsletterMetadata: async (type, key) => {
89
- const variables = { fetch_creation_time: true, fetch_full_image: true, fetch_viewer_metadata: true, input: { key, type: type.toUpperCase() } }
90
- const result = await executeWMexQuery(variables, QueryIds.METADATA, XWAPaths.xwa2_newsletter_metadata)
115
+ const result = await executeWMexQuery(
116
+ { fetch_creation_time: true, fetch_full_image: true, fetch_viewer_metadata: true, input: { key, type: type.toUpperCase() } },
117
+ QueryIds.METADATA,
118
+ XWAPaths.xwa2_newsletter_metadata
119
+ )
91
120
  return parseNewsletterMetadata(result)
92
121
  },
93
122
 
94
- newsletterFollow: async (jid) => executeWMexQuery({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_follow),
123
+ newsletterFollow: (jid) =>
124
+ executeWMexQuery({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_join_v2),
95
125
 
96
- newsletterUnfollow: (jid) => executeWMexQuery({ newsletter_id: jid }, QueryIds.UNFOLLOW, XWAPaths.xwa2_newsletter_unfollow),
126
+ newsletterUnfollow: (jid) =>
127
+ executeWMexQuery({ newsletter_id: jid }, QueryIds.UNFOLLOW, XWAPaths.xwa2_newsletter_leave_v2),
97
128
 
98
- newsletterMute: (jid) => executeWMexQuery({ newsletter_id: jid }, QueryIds.MUTE, XWAPaths.xwa2_newsletter_mute_v2),
129
+ newsletterMute: (jid) =>
130
+ executeWMexQuery({ newsletter_id: jid }, QueryIds.MUTE, XWAPaths.xwa2_newsletter_mute_v2),
99
131
 
100
- newsletterUnmute: (jid) => executeWMexQuery({ newsletter_id: jid }, QueryIds.UNMUTE, XWAPaths.xwa2_newsletter_unmute_v2),
132
+ newsletterUnmute: (jid) =>
133
+ executeWMexQuery({ newsletter_id: jid }, QueryIds.UNMUTE, XWAPaths.xwa2_newsletter_unmute_v2),
101
134
 
102
- newsletterUpdateName: async (jid, name) => newsletterUpdate(jid, { name }),
135
+ newsletterUpdateName: (jid, name) => newsletterUpdate(jid, { name }),
103
136
 
104
- newsletterUpdateDescription: async (jid, description) => newsletterUpdate(jid, { description }),
137
+ newsletterUpdateDescription: (jid, description) => newsletterUpdate(jid, { description }),
105
138
 
106
139
  newsletterUpdatePicture: async (jid, content) => {
107
140
  const { img } = await generateProfilePicture(content)
108
141
  return newsletterUpdate(jid, { picture: img.toString('base64') })
109
142
  },
110
143
 
111
- newsletterRemovePicture: async (jid) => newsletterUpdate(jid, { picture: '' }),
144
+ newsletterRemovePicture: (jid) => newsletterUpdate(jid, { picture: '' }),
112
145
 
113
- newsletterReactMessage: async (jid, serverId, reaction) => {
114
- await query({ tag: 'message', attrs: { to: jid, ...(reaction ? {} : { edit: '7' }), type: 'reaction', server_id: serverId, id: generateMessageTag() }, content: [{ tag: 'reaction', attrs: reaction ? { code: reaction } : {} }] })
115
- },
146
+ newsletterReactMessage: (jid, serverId, reaction) =>
147
+ query({
148
+ tag: 'message',
149
+ attrs: { to: jid, ...(reaction ? {} : { edit: '7' }), type: 'reaction', server_id: serverId, id: generateMessageTag() },
150
+ content: [{ tag: 'reaction', attrs: reaction ? { code: reaction } : {} }]
151
+ }),
116
152
 
117
153
  newsletterFetchMessages: async (jid, count, since, after) => {
118
- const messageUpdateAttrs = { count: count.toString() }
119
- if (typeof since === 'number') messageUpdateAttrs.since = since.toString()
120
- if (after) messageUpdateAttrs.after = after.toString()
121
- return await query({ tag: 'iq', attrs: { id: generateMessageTag(), type: 'get', xmlns: 'newsletter', to: jid }, content: [{ tag: 'message_updates', attrs: messageUpdateAttrs }] })
154
+ const attrs = { count: count.toString() }
155
+ if (typeof since === 'number') attrs.since = since.toString()
156
+ if (after) attrs.after = after.toString()
157
+ return query({
158
+ tag: 'iq',
159
+ attrs: { id: generateMessageTag(), type: 'get', xmlns: 'newsletter', to: jid },
160
+ content: [{ tag: 'message_updates', attrs }]
161
+ })
122
162
  },
123
163
 
124
164
  subscribeNewsletterUpdates: async (jid) => {
125
- const result = await query({ tag: 'iq', attrs: { id: generateMessageTag(), type: 'set', xmlns: 'newsletter', to: jid }, content: [{ tag: 'live_updates', attrs: {}, content: [] }] })
165
+ const result = await query({
166
+ tag: 'iq',
167
+ attrs: { id: generateMessageTag(), type: 'set', xmlns: 'newsletter', to: jid },
168
+ content: [{ tag: 'live_updates', attrs: {}, content: [] }]
169
+ })
126
170
  const liveUpdatesNode = getBinaryNodeChild(result, 'live_updates')
127
171
  const duration = liveUpdatesNode?.attrs?.duration
128
172
  return duration ? { duration } : null
@@ -130,7 +174,11 @@ export const makeNewsletterSocket = (config) => {
130
174
 
131
175
  isFollowingNewsletter: async (jid) => {
132
176
  try {
133
- const result = await executeWMexQuery({ newsletter_id: jid, input: { key: jid, type: 'NEWSLETTER', view_role: 'GUEST' }, fetch_viewer_metadata: true }, QueryIds.METADATA, XWAPaths.xwa2_newsletter_metadata)
177
+ const result = await executeWMexQuery(
178
+ { newsletter_id: jid, input: { key: jid, type: 'NEWSLETTER', view_role: 'GUEST' }, fetch_viewer_metadata: true },
179
+ QueryIds.METADATA,
180
+ XWAPaths.xwa2_newsletter_metadata
181
+ )
134
182
  return result?.viewer_metadata?.is_subscribed === true
135
183
  } catch { return false }
136
184
  },
@@ -140,10 +188,13 @@ export const makeNewsletterSocket = (config) => {
140
188
  return response.admin_count
141
189
  },
142
190
 
143
- newsletterChangeOwner: async (jid, newOwnerJid) => executeWMexQuery({ newsletter_id: jid, user_id: newOwnerJid }, QueryIds.CHANGE_OWNER, XWAPaths.xwa2_newsletter_change_owner),
191
+ newsletterChangeOwner: (jid, newOwnerJid) =>
192
+ executeWMexQuery({ newsletter_id: jid, user_id: newOwnerJid }, QueryIds.CHANGE_OWNER, XWAPaths.xwa2_newsletter_change_owner),
144
193
 
145
- newsletterDemote: async (jid, userJid) => executeWMexQuery({ newsletter_id: jid, user_id: userJid }, QueryIds.DEMOTE, XWAPaths.xwa2_newsletter_demote),
194
+ newsletterDemote: (jid, userJid) =>
195
+ executeWMexQuery({ newsletter_id: jid, user_id: userJid }, QueryIds.DEMOTE, XWAPaths.xwa2_newsletter_demote),
146
196
 
147
- newsletterDelete: async (jid) => executeWMexQuery({ newsletter_id: jid }, QueryIds.DELETE, XWAPaths.xwa2_newsletter_delete_v2)
197
+ newsletterDelete: (jid) =>
198
+ executeWMexQuery({ newsletter_id: jid }, QueryIds.DELETE, XWAPaths.xwa2_newsletter_delete_v2)
148
199
  }
149
200
  }