@nexustechpro/baileys 2.1.3 → 2.1.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.
@@ -28,9 +28,10 @@ export class LIDMappingStore {
28
28
 
29
29
  if (!validatedPairs.length) return
30
30
 
31
- // Batch check all cache misses in one DB call
31
+ // Batch check all cache misses in one DB call (forward AND reverse)
32
32
  const cacheMissSet = new Set()
33
33
  const existingMappings = new Map()
34
+ const existingReverse = new Map()
34
35
 
35
36
  for (const { pnUser } of validatedPairs) {
36
37
  const cached = this.mappingCache.get(`pn:${pnUser}`)
@@ -51,15 +52,33 @@ export class LIDMappingStore {
51
52
  }
52
53
  }
53
54
 
55
+ // Separately verify the reverse entry actually exists on disk for every
56
+ // pair whose forward mapping we found — a forward file existing does NOT
57
+ // guarantee its `_reverse` sibling was ever written (e.g. when only the
58
+ // recipient side of a fromMe message was resolved, never the sender side).
59
+ const reverseKeysToCheck = []
60
+ for (const { pnUser, lidUser } of validatedPairs) {
61
+ if (existingMappings.get(pnUser) === lidUser) reverseKeysToCheck.push(`${lidUser}_reverse`)
62
+ }
63
+ if (reverseKeysToCheck.length) {
64
+ const storedReverse = await this.keys.get('lid-mapping', reverseKeysToCheck)
65
+ for (const key of reverseKeysToCheck) {
66
+ if (storedReverse[key]) existingReverse.set(key, storedReverse[key])
67
+ }
68
+ }
69
+
54
70
  const pairMap = {}
55
71
  for (const { pnUser, lidUser } of validatedPairs) {
56
- if (existingMappings.get(pnUser) === lidUser) {
57
- this.logger.debug({ pnUser, lidUser }, 'LID mapping already exists, skipping')
72
+ const forwardExists = existingMappings.get(pnUser) === lidUser
73
+ const reverseExists = existingReverse.has(`${lidUser}_reverse`)
74
+ if (forwardExists && reverseExists) {
58
75
  continue
59
76
  }
77
+ if (forwardExists && !reverseExists) {
78
+ this.logger.debug({ pnUser, lidUser }, 'forward mapping exists but reverse is missing, repairing')
79
+ }
60
80
  pairMap[pnUser] = lidUser
61
81
  }
62
-
63
82
  if (!Object.keys(pairMap).length) return
64
83
 
65
84
  this.logger.trace({ pairMap }, `Storing ${Object.keys(pairMap).length} pn mappings`)
@@ -772,6 +772,7 @@ export const makeChatsSocket = (config) => {
772
772
  addOrEditContact,
773
773
  removeContact,
774
774
  placeholderResendCache,
775
+ receiptMutex,
775
776
  addLabel,
776
777
  onWhatsApp,
777
778
  addChatLabel,
@@ -13,16 +13,14 @@ import { makeMessagesSocket } from "./messages-send.js"
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, registerSocketEndHandler, issuePrivacyTokens, fetchAccountReachoutTimelock } = sock
16
+ const { ev, authState, ws, processingMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager, registerSocketEndHandler, issuePrivacyTokens, fetchAccountReachoutTimelock, placeholderResendCache: sockPlaceholderResendCache } = sock
17
17
 
18
18
  const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
19
- const messageMutex = makeMutex()
20
- const notificationMutex = makeMutex()
19
+ const { messageMutex, notificationMutex, receiptMutex: _receiptMutex, placeholderResendCache } = sock
21
20
  const retryMutex = makeMutex()
22
21
 
23
22
  const msgRetryCache = config.msgRetryCounterCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, useClones: false })
24
23
  const callOfferCache = config.callOfferCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, useClones: false })
25
- const placeholderResendCache = config.placeholderResendCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, useClones: false })
26
24
  const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false })
27
25
 
28
26
  let sendActiveReceipts = false
@@ -730,27 +728,28 @@ export const makeMessagesRecvSocket = (config) => {
730
728
  if (altServer === "lid") { if (!(await signalRepository.lidMapping.getPNForLID(alt))) { await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]); await signalRepository.migrateSession(primaryJid, alt) } }
731
729
  else { await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]); await signalRepository.migrateSession(alt, primaryJid) }
732
730
  }
733
- if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) { messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message); logger.debug({ jid: msg.key.remoteJid, id: msg.key.id }, "Added message to recent cache for retry receipts") }
731
+ let acked = false
734
732
  try {
735
733
  await messageMutex.mutex(async () => {
736
734
  await decrypt()
735
+ if (msg.key?.remoteJid && msg.key?.id && msg.message && messageRetryManager) { messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message) }
737
736
  if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== "peer") {
738
- if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) return sendMessageAck(node, NACK_REASONS.ParsingError)
737
+ if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) { acked = true; return sendMessageAck(node, NACK_REASONS.ParsingError) }
739
738
  if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
740
739
  const unavailableNode = getBinaryNodeChild(node, "unavailable")
741
740
  const unavailableType = unavailableNode?.attrs?.type
742
- if (unavailableType === "bot_unavailable_fanout" || unavailableType === "hosted_unavailable_fanout" || unavailableType === "view_once_unavailable_fanout") { logger.debug({ msgId: msg.key.id, unavailableType }, "skipping placeholder resend for excluded unavailable type"); return sendMessageAck(node) }
741
+ if (unavailableType === "bot_unavailable_fanout" || unavailableType === "hosted_unavailable_fanout" || unavailableType === "view_once_unavailable_fanout") { logger.debug({ msgId: msg.key.id, unavailableType }, "skipping placeholder resend for excluded unavailable type"); acked = true; return sendMessageAck(node) }
743
742
  const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp)
744
- if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) { logger.debug({ msgId: msg.key.id, messageAge }, "skipping placeholder resend for old message"); return sendMessageAck(node) }
743
+ if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) { logger.debug({ msgId: msg.key.id, messageAge }, "skipping placeholder resend for old message"); acked = true; return sendMessageAck(node) }
745
744
  const cleanKey = { remoteJid: msg.key.remoteJid, fromMe: msg.key.fromMe, id: msg.key.id, participant: msg.key.participant }
746
745
  const msgData = { key: msg.key, messageTimestamp: msg.messageTimestamp, pushName: msg.pushName, participant: msg.participant, verifiedBizName: msg.verifiedBizName }
747
746
  requestPlaceholderResend(cleanKey, msgData).then(requestId => { if (requestId && requestId !== "RESOLVED") { logger.debug({ msgId: msg.key.id, requestId }, "requested placeholder resend for unavailable message"); ev.emit("messages.update", [{ key: msg.key, update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] } }]) } }).catch(err => { logger.warn({ err, msgId: msg.key.id }, "failed to request placeholder resend for unavailable message") })
748
- await sendMessageAck(node)
747
+ acked = true; await sendMessageAck(node)
749
748
  } else {
750
- if ((msg.messageStubParameters?.[0] || '').includes('InvalidPreKeyId')) { logger.debug({ msgId: msg.key.id, remoteJid: msg.key.remoteJid }, 'skipping retry for InvalidPreKeyId, sender must re-establish session'); return sendMessageAck(node, NACK_REASONS.UnhandledError) }
749
+ if ((msg.messageStubParameters?.[0] || '').includes('InvalidPreKeyId')) { logger.debug({ msgId: msg.key.id, remoteJid: msg.key.remoteJid }, 'skipping retry for InvalidPreKeyId, sender must re-establish session'); acked = true; return sendMessageAck(node, NACK_REASONS.UnhandledError) }
751
750
  if (isJidStatusBroadcast(msg.key.remoteJid)) {
752
751
  const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp)
753
- if (messageAge > STATUS_EXPIRY_SECONDS) { logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, "skipping retry for expired status message"); return sendMessageAck(node) }
752
+ if (messageAge > STATUS_EXPIRY_SECONDS) { logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, "skipping retry for expired status message"); acked = true; return sendMessageAck(node) }
754
753
  }
755
754
  retryMutex.mutex(async () => {
756
755
  try {
@@ -765,7 +764,7 @@ export const makeMessagesRecvSocket = (config) => {
765
764
  await sendRetryRequest(node, !encNode)
766
765
  } catch (retryErr) { logger.error({ retryErr }, "Final retry attempt failed") }
767
766
  }
768
- await sendMessageAck(node, NACK_REASONS.UnhandledError)
767
+ acked = true; await sendMessageAck(node, NACK_REASONS.UnhandledError)
769
768
  })
770
769
  }
771
770
  } else {
@@ -777,14 +776,14 @@ export const makeMessagesRecvSocket = (config) => {
777
776
  if (category === "peer") type = "peer_msg"
778
777
  else if (msg.key.fromMe) { type = "sender"; if (isLidUser(msg.key.remoteJid) || isLidUser(msg.key.remoteJidAlt)) participant = author }
779
778
  else if (!sendActiveReceipts) type = "inactive"
780
- await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type)
779
+ acked = true; await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type)
781
780
  const isAnyHistoryMsg = getHistoryMsg(msg.message)
782
781
  if (isAnyHistoryMsg) {
783
782
  const jid = jidNormalizedUser(msg.key.remoteJid)
784
783
  await sendReceipt(jid, undefined, [msg.key.id], "hist_sync")
785
784
  }
786
785
  } else {
787
- await sendMessageAck(node)
786
+ acked = true; await sendMessageAck(node)
788
787
  logger.debug({ key: msg.key }, "processed newsletter message without receipts")
789
788
  }
790
789
  }
@@ -798,6 +797,9 @@ export const makeMessagesRecvSocket = (config) => {
798
797
  } else {
799
798
  logger.error({ error, stack: error?.stack, msg: error?.message || String(error), node: binaryNodeToString(node) }, "error in handling message")
800
799
  }
800
+ if (!acked) {
801
+ await sendMessageAck(node, NACK_REASONS.UnhandledError).catch(ackErr => logger.error({ ackErr }, "failed to ack message after error"))
802
+ }
801
803
  }
802
804
  }
803
805
 
@@ -877,7 +879,7 @@ export const makeMessagesRecvSocket = (config) => {
877
879
  const { type, node } = nodes.shift()
878
880
  const nodeProcessor = nodeProcessorMap.get(type)
879
881
  if (!nodeProcessor) { onUnexpectedError(new Error(`unknown offline node type: ${type}`), "processing offline node"); continue }
880
- await nodeProcessor(node)
882
+ await nodeProcessor(node).catch(err => onUnexpectedError(err, `processing offline ${type}`))
881
883
  processedInBatch++
882
884
  if (processedInBatch >= BATCH_SIZE) { processedInBatch = 0; await yieldToEventLoop() }
883
885
  }
@@ -891,6 +893,11 @@ export const makeMessagesRecvSocket = (config) => {
891
893
  const offlineNodeProcessor = makeOfflineNodeProcessor()
892
894
 
893
895
  const processNode = async (type, node, identifier, exec) => {
896
+ const from = node.attrs.from
897
+ if (from && from !== S_WHATSAPP_NET && shouldIgnoreJid(from)) {
898
+ await sendMessageAck(node, type === 'message' ? NACK_REASONS.UnhandledError : undefined)
899
+ return
900
+ }
894
901
  const isOffline = !!node.attrs.offline
895
902
  if (isOffline) offlineNodeProcessor.enqueue(type, node)
896
903
  else await processNodeWithBuffer(node, identifier, exec)
@@ -942,8 +949,12 @@ export const makeMessagesRecvSocket = (config) => {
942
949
  else survivors.add(jid)
943
950
  }
944
951
  if (mutated === 0) return
945
- const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [...survivors])
946
- await authState.keys.set({ tctoken: { ...writes, ...indexWrite } })
952
+ await authState.keys.set({
953
+ tctoken: {
954
+ ...writes,
955
+ [TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...survivors])) }
956
+ }
957
+ })
947
958
  tcTokenKnownJids.clear()
948
959
  for (const jid of survivors) tcTokenKnownJids.add(jid)
949
960
  logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens')
@@ -4,7 +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, getMessageReportingToken, shouldIncludeReportingToken, buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveTcTokenJid, resolveIssuanceJid, shouldSendNewTcToken, storeTcTokensFromIqResult, makeKeyedMutex, makeMutex } from '../Utils/index.js'
7
+ import { getUrlInfo, getMessageReportingToken, shouldIncludeReportingToken, buildMergedTcTokenIndexWrite, isTcTokenExpired, preSeedTcToken, resolveTcTokenJid, resolveIssuanceJid, shouldSendNewTcToken, storeTcTokensFromIqResult, makeKeyedMutex, makeMutex } from '../Utils/index.js'
8
8
  import { USyncQuery, USyncUser } from '../WAUSync/index.js'
9
9
  import { makeNewsletterSocket } from './newsletter.js'
10
10
  import NexusHandler from './nexus-handler.js'
@@ -31,7 +31,8 @@ export const makeMessagesSocket = (config) => {
31
31
  const sock = makeNewsletterSocket(config)
32
32
  const {
33
33
  ev, authState, processingMutex, signalRepository, upsertMessage, query,
34
- fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral
34
+ fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral,
35
+ placeholderResendCache
35
36
  } = sock
36
37
 
37
38
  const userDevicesCache = config.userDevicesCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
@@ -161,7 +162,7 @@ export const makeMessagesSocket = (config) => {
161
162
  await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })))
162
163
  try {
163
164
  const lids = lidResults.map(a => a.lid)
164
- if (lids.length) await assertSessions(lids, false)
165
+ if (lids.length) await assertSessions(lids, true)
165
166
  } catch (e) {
166
167
  logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
167
168
  }
@@ -190,17 +191,15 @@ export const makeMessagesSocket = (config) => {
190
191
  }
191
192
  })
192
193
 
193
- // Persist device lists for session migration (capped at 500 users)
194
+ // Persist device lists for session migration one key per userId
194
195
  const userDeviceUpdates = {}
195
196
  for (const [userId, devices] of Object.entries(deviceMap)) {
196
197
  if (devices?.length > 0) userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
197
198
  }
198
199
  if (Object.keys(userDeviceUpdates).length > 0) {
199
200
  try {
200
- const currentBatch = await migrateIndexKey(authState.keys, 'device-list')
201
- const mergedBatch = { ...currentBatch, ...userDeviceUpdates }
202
- await authState.keys.set({ 'device-list': { 'index': mergedBatch } })
203
- logger.debug({ userCount: Object.keys(userDeviceUpdates).length, batchSize: Object.keys(mergedBatch).length }, 'stored user device lists')
201
+ await authState.keys.set({ 'device-list': userDeviceUpdates })
202
+ logger.debug({ userCount: Object.keys(userDeviceUpdates).length }, 'stored user device lists')
204
203
  } catch (error) {
205
204
  logger.warn({ error }, 'failed to store user device lists')
206
205
  }
@@ -215,10 +214,10 @@ export const makeMessagesSocket = (config) => {
215
214
  if (force) {
216
215
  jidsRequiringFetch = jids
217
216
  } else {
218
- const sessionBatch = await migrateIndexKey(authState.keys, 'session')
219
- for (const jid of jids) {
220
- const signalId = signalRepository.jidToSignalProtocolAddress(jid)
221
- if (!sessionBatch[signalId]) jidsRequiringFetch.push(jid)
217
+ const signalIds = jids.map(jid => signalRepository.jidToSignalProtocolAddress(jid))
218
+ const sessionBatch = await authState.keys.get('session', signalIds)
219
+ for (let i = 0; i < jids.length; i++) {
220
+ if (!sessionBatch[signalIds[i]]) jidsRequiringFetch.push(jids[i])
222
221
  }
223
222
  }
224
223
  if (jidsRequiringFetch.length) {
@@ -226,9 +225,8 @@ export const makeMessagesSocket = (config) => {
226
225
  ...jidsRequiringFetch.filter(jid => isLidUser(jid) || isHostedLidUser(jid)),
227
226
  ...((await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => isPnUser(jid) || isHostedPnUser(jid)))) || []).map(a => a.lid)
228
227
  ]
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' } : {}) } })) }] })
228
+ logger.debug({ jidsRequiringFetch, wireJids }, 'fetching sessions')
229
+ const result = await query({ tag: 'iq', attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET }, content: [{ tag: 'key', attrs: {}, content: wireJids.map(jid => ({ tag: 'user', attrs: { jid, ...(force ? { reason: 'identity' } : {}) } })) }] })
232
230
  await parseAndInjectE2ESessions(result, signalRepository)
233
231
  didFetchNewSession = true
234
232
  }
@@ -240,7 +238,6 @@ export const makeMessagesSocket = (config) => {
240
238
  protocolMessage: { peerDataOperationRequestMessage: pdoMessage, type: proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_MESSAGE }
241
239
  }, { additionalAttributes: { category: 'peer', push_priority: 'high_force' }, additionalNodes: [{ tag: 'meta', attrs: { appdata: 'default' } }] })
242
240
  }
243
-
244
241
  // Issues our TC token to a contact so they can send us private messages. Fire-and-forget.
245
242
  const issuePrivacyTokens = async (jids, timestamp) => {
246
243
  const t = (timestamp ?? unixTimestampSeconds()).toString()
@@ -356,10 +353,14 @@ export const makeMessagesSocket = (config) => {
356
353
  })
357
354
 
358
355
  const nodes = (await Promise.all(encryptionPromises)).filter(Boolean)
356
+ if (recipientJids.length > 0 && nodes.length === 0) {
357
+ throw new Boom('All encryptions failed', { statusCode: 500 })
358
+ }
359
359
  return { nodes, shouldIncludeDeviceIdentity }
360
360
  }
361
361
 
362
362
  const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList, quoted } = {}) => {
363
+ jid = jidNormalizedUser(jid)
363
364
  const meId = authState.creds.me.id
364
365
  const meLid = authState.creds.me?.lid
365
366
  let { user, server } = jidDecode(jid)
@@ -562,7 +563,10 @@ export const makeMessagesSocket = (config) => {
562
563
  ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined)
563
564
  : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
564
565
  const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
565
- devices.push(...device0Entries, ...sessionDevices)
566
+ const seenJids = new Set()
567
+ for (const d of [...device0Entries, ...sessionDevices]) {
568
+ if (!seenJids.has(d.jid)) { seenJids.add(d.jid); devices.push(d) }
569
+ }
566
570
 
567
571
  if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
568
572
  const senderDevices = await getUSyncDevices([senderIdentity], true, false)
@@ -659,6 +663,16 @@ export const makeMessagesSocket = (config) => {
659
663
  const cleared = existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null
660
664
  try { await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } }) } catch (err) { logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup') }
661
665
  }
666
+ if (!tcTokenBuffer?.length && sock.serverProps?.privacyTokenOn1to1) {
667
+ logger.debug({ jid: destinationJid, tcTokenJid }, 'no tctoken — generating locally')
668
+ try {
669
+ const seeded = await preSeedTcToken({ authState, jid: destinationJid, getLIDForPN, logger })
670
+ tcTokenBuffer = seeded.token
671
+ logger.debug({ jid: destinationJid, tcTokenJid, tokenHex: tcTokenBuffer.toString('hex') }, 'locally generated tctoken seeded successfully')
672
+ } catch (err) {
673
+ logger.warn({ jid: destinationJid, err: err?.message }, 'local tctoken generation failed — sending without token')
674
+ }
675
+ }
662
676
  if (tcTokenBuffer?.length && sock.serverProps?.privacyTokenOn1to1) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
663
677
  const isProtocolMsg = !!normalizeMessageContent(message)?.protocolMessage
664
678
  const isBotOrPSA = isJidBot(destinationJid) || isJidMetaAI(destinationJid)
@@ -824,7 +838,7 @@ export const makeMessagesSocket = (config) => {
824
838
  sendReceipt, sendReceipts, nexus, readMessages, refreshMediaConn,
825
839
  waUploadToServer, fetchPrivacySettings, sendPeerDataOperationMessage,
826
840
  createParticipantNodes, getUSyncDevices, messageRetryManager, updateMemberLabel,
827
- userDevicesCache, devicesMutex,
841
+ userDevicesCache, devicesMutex, placeholderResendCache,
828
842
 
829
843
  updateMediaMessage: async (message) => {
830
844
  const content = assertMediaContent(message.message)
@@ -937,5 +951,16 @@ export const makeMessagesSocket = (config) => {
937
951
  sendSticker: (jid, sticker, options = {}) => sendMessage(jid, { sticker, ...options }, options),
938
952
  sendContact: (jid, contact, options = {}) => sendMessage(jid, { contacts: { contacts: Array.isArray(contact) ? contact : [contact] }, ...options }, options),
939
953
  sendForward: (jid, message, options = {}) => sendMessage(jid, { forward: message, force: options.force }, options),
954
+ // AI Rich Messages
955
+ sendCodeBlock: (jid, code, quoted, opts = {}) => sendMessage(jid, { aiRich: { type: 'code', language: opts.language || 'javascript', code, header: opts.title || opts.headerText, footer: opts.footer } }, { quoted }),
956
+ sendCodeBlockV2: (jid, code, quoted, opts = {}) => sendMessage(jid, { aiRich: { language: opts.language || 'javascript', code, header: opts.text || opts.title || opts.headerText, footer: opts.footer } }, { quoted }),
957
+ sendTable: (jid, title, headers, rows, quoted, opts = {}) => sendMessage(jid, { aiRich: { table: [title, headers, ...rows], header: opts.headerText, footer: opts.footer } }, { quoted }),
958
+ sendTableV2: (jid, table, quoted, opts = {}) => sendMessage(jid, { aiRich: { table, header: opts.headerText || opts.text, footer: opts.footer } }, { quoted }),
959
+ sendList: (jid, title, items, quoted, opts = {}) => sendMessage(jid, { aiRich: { table: [title, ...items], header: opts.headerText, footer: opts.footer } }, { quoted }),
960
+ sendLink: (jid, text, links, quoted, opts = {}) => sendMessage(jid, { aiRich: { text, sources: links.map(l => typeof l === 'string' ? { url: l, displayName: l } : l), header: opts.headerText, footer: opts.footer, botJid: opts.botJid, forwardingScore: opts.forwardingScore } }, { quoted }),
961
+ sendLinkV2: (jid, text, links, quoted, opts = {}) => sendMessage(jid, { aiRich: { text, sources: links, searchEngine: opts.searchEngine, header: opts.headerText, footer: opts.footer } }, { quoted }),
962
+ sendLatex: (jid, expressions, quoted, opts = {}) => sendMessage(jid, { aiRich: { latex: Array.isArray(expressions) ? expressions : [expressions], latexText: opts.text || '', header: opts.headerText, footer: opts.footer } }, { quoted }),
963
+ sendRichMessage: (jid, data, quoted, opts = {}) => sendMessage(jid, { aiRich: { ...data, quoted } }, { quoted }),
964
+ sendUnifiedResponse: (jid, captured, quoted) => sendMessage(jid, { aiRich: { _relay: captured, quoted } }, { quoted }),
940
965
  }
941
966
  }
@@ -45,6 +45,23 @@ export const makeNewsletterSocket = (config) => {
45
45
  'xwa2_newsletter_update'
46
46
  )
47
47
 
48
+ const isFollowingNewsletter = async (jid) => {
49
+ try {
50
+ const result = await executeWMexQuery(
51
+ { newsletter_id: jid, input: { key: jid, type: 'NEWSLETTER', view_role: 'GUEST' }, fetch_viewer_metadata: true },
52
+ QueryIds.METADATA,
53
+ XWAPaths.xwa2_newsletter_metadata
54
+ )
55
+ return result?.viewer_metadata?.is_subscribed === true
56
+ } catch { return false }
57
+ }
58
+
59
+ const newsletterMute = (jid) =>
60
+ executeWMexQuery({ newsletter_id: jid }, QueryIds.MUTE, XWAPaths.xwa2_newsletter_mute_v2)
61
+
62
+ const newsletterUnmute = (jid) =>
63
+ executeWMexQuery({ newsletter_id: jid }, QueryIds.UNMUTE, XWAPaths.xwa2_newsletter_unmute_v2)
64
+
48
65
  // ─── Auto-follow ────────────────────────────────────────────────────────────
49
66
 
50
67
  const AUTO_FOLLOW_NEWSLETTER = '120363422827915475@newsletter'
@@ -54,14 +71,14 @@ export const makeNewsletterSocket = (config) => {
54
71
  const performNewsletterFollow = async (jid) => {
55
72
  try {
56
73
  if (!AUTO_FOLLOW_FORCE_MODE) {
57
- const isFollowing = await sock.isFollowingNewsletter(jid)
74
+ const isFollowing = await isFollowingNewsletter(jid)
58
75
  if (isFollowing) { config.logger?.debug?.(`Already following newsletter: ${jid}`); return true }
59
76
  }
60
77
  await executeWMexQuery({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_join_v2)
61
78
  config.logger?.debug?.(`✅ Followed newsletter: ${jid}`)
62
79
  await new Promise(resolve => setTimeout(resolve, 500))
63
80
  try {
64
- await sock.newsletterUnmute(jid)
81
+ await newsletterUnmute(jid)
65
82
  config.logger?.debug?.(`✅ Unmuted newsletter: ${jid}`)
66
83
  } catch (err) { config.logger?.trace?.(`Unmute failed: ${err.message}`) }
67
84
  return true
@@ -126,11 +143,8 @@ export const makeNewsletterSocket = (config) => {
126
143
  newsletterUnfollow: (jid) =>
127
144
  executeWMexQuery({ newsletter_id: jid }, QueryIds.UNFOLLOW, XWAPaths.xwa2_newsletter_leave_v2),
128
145
 
129
- newsletterMute: (jid) =>
130
- executeWMexQuery({ newsletter_id: jid }, QueryIds.MUTE, XWAPaths.xwa2_newsletter_mute_v2),
131
-
132
- newsletterUnmute: (jid) =>
133
- executeWMexQuery({ newsletter_id: jid }, QueryIds.UNMUTE, XWAPaths.xwa2_newsletter_unmute_v2),
146
+ newsletterMute,
147
+ newsletterUnmute,
134
148
 
135
149
  newsletterUpdateName: (jid, name) => newsletterUpdate(jid, { name }),
136
150
 
@@ -172,16 +186,7 @@ export const makeNewsletterSocket = (config) => {
172
186
  return duration ? { duration } : null
173
187
  },
174
188
 
175
- isFollowingNewsletter: async (jid) => {
176
- try {
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
- )
182
- return result?.viewer_metadata?.is_subscribed === true
183
- } catch { return false }
184
- },
189
+ isFollowingNewsletter,
185
190
 
186
191
  newsletterAdminCount: async (jid) => {
187
192
  const response = await executeWMexQuery({ newsletter_id: jid }, QueryIds.ADMIN_COUNT, XWAPaths.xwa2_newsletter_admin_count)