@nexustechpro/baileys 2.1.2 → 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
@@ -527,7 +525,7 @@ export const makeMessagesRecvSocket = (config) => {
527
525
 
528
526
  const tcTokenKnownJids = new Set()
529
527
  const tcTokenIndexLoaded = (async () => {
530
- try { const jids = await readTcTokenIndex(authState.keys); for (const jid of jids) tcTokenKnownJids.add(jid); logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index') }
528
+ try { const jids = await readTcTokenIndex(authState.keys); for (const jid of jids) tcTokenKnownJids.add(jid); if (tcTokenKnownJids.size > 0) logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index') }
531
529
  catch (err) { logger.warn({ err: err?.message }, 'failed to load tctoken index') }
532
530
  })()
533
531
 
@@ -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,7 +949,12 @@ export const makeMessagesRecvSocket = (config) => {
942
949
  else survivors.add(jid)
943
950
  }
944
951
  if (mutated === 0) return
945
- await authState.keys.set({ tctoken: { ...writes, [TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...survivors])) } } })
952
+ await authState.keys.set({
953
+ tctoken: {
954
+ ...writes,
955
+ [TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...survivors])) }
956
+ }
957
+ })
946
958
  tcTokenKnownJids.clear()
947
959
  for (const jid of survivors) tcTokenKnownJids.add(jid)
948
960
  logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens')
@@ -962,5 +974,5 @@ export const makeMessagesRecvSocket = (config) => {
962
974
  sendActiveReceipts = false
963
975
  })
964
976
 
965
- return { ...sock, sendMessageAck, sendRetryRequest, rejectCall, offerCall, fetchMessageHistory, requestPlaceholderResend, messageRetryManager }
977
+ return { ...sock, sendMessageAck, sendRetryRequest, rejectCall, offerCall, fetchMessageHistory, requestPlaceholderResend, messageRetryManager, tcTokenIndexLoaded }
966
978
  }
@@ -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)
@@ -367,13 +368,6 @@ export const makeMessagesSocket = (config) => {
367
368
  const isStatus = jid === 'status@broadcast'
368
369
  let isLid = server === 'lid'
369
370
  const isNewsletter = server === 'newsletter'
370
- if (isLid && !isGroup && !isStatus) {
371
- const pn = await signalRepository.lidMapping.getPNForLID(jid)
372
- if (pn) {
373
- const resolvedJid = pn.replace(/:\d+(?=@)/, ''); logger.debug({ from: jid, to: resolvedJid }, 'relayMessage: resolved 1:1 LID to PN for wire protocol')
374
- jid = resolvedJid; const decoded = jidDecode(jid); user = decoded.user; server = decoded.server; isLid = false
375
- }
376
- }
377
371
  let activeSender = meId
378
372
  let groupAddressingMode = 'pn'
379
373
  if (isGroup && !isStatus) {
@@ -542,6 +536,14 @@ export const makeMessagesSocket = (config) => {
542
536
  if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
543
537
 
544
538
  const { user: ownUser } = jidDecode(ownId)
539
+ const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
540
+ devices.push({ user, device: 0, jid: jidEncode(user, targetUserServer, 0) })
541
+
542
+ if (user !== ownUser) {
543
+ const ownUserServer = isLid ? 'lid' : 's.whatsapp.net'
544
+ const ownUserForAddressing = isLid && meLid ? jidDecode(meLid).user : jidDecode(meId).user
545
+ devices.push({ user: ownUserForAddressing, device: 0, jid: jidEncode(ownUserForAddressing, ownUserServer, 0) })
546
+ }
545
547
 
546
548
  if (!participant) {
547
549
  const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
@@ -561,7 +563,10 @@ export const makeMessagesSocket = (config) => {
561
563
  ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined)
562
564
  : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
563
565
  const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
564
- 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
+ }
565
570
 
566
571
  if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
567
572
  const senderDevices = await getUSyncDevices([senderIdentity], true, false)
@@ -570,22 +575,16 @@ export const makeMessagesSocket = (config) => {
570
575
  }
571
576
  }
572
577
  }
573
-
574
578
  const allRecipients = [], meRecipients = [], otherRecipients = []
575
579
  const { user: mePnUser } = jidDecode(meId)
576
580
  const { user: meLidUser } = meLid ? jidDecode(meLid) : { user: null }
577
-
578
- for (const { user, jid } of devices) {
579
- const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
580
- if (isExactSenderDevice) continue
581
- const isMe = user === mePnUser || user === meLidUser
582
- if (isMe) meRecipients.push(jid)
583
- else otherRecipients.push(jid)
584
- allRecipients.push(jid)
581
+ for (const { user: devUser, jid: devJid } of devices) {
582
+ const isOwnUser = devUser === mePnUser || devUser === meLidUser
583
+ if (isOwnUser) meRecipients.push(devJid)
584
+ else otherRecipients.push(devJid)
585
+ allRecipients.push(devJid)
585
586
  }
586
-
587
587
  await assertSessions(allRecipients)
588
-
589
588
  const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
590
589
  createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
591
590
  createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
@@ -613,7 +612,7 @@ export const makeMessagesSocket = (config) => {
613
612
  id: finalMsgId,
614
613
  to: destinationJid,
615
614
  type: getMessageType(message),
616
- ...(isLid || (isGroup && groupAddressingMode === 'lid') ? { addressing_mode: 'lid' } : {}),
615
+ ...((isGroup && groupAddressingMode === 'lid') ? { addressing_mode: 'lid' } : {}),
617
616
  ...(hasDeviceFanoutFalse ? { device_fanout: 'false' } : {}),
618
617
  ...(additionalAttributes || {})
619
618
  },
@@ -664,6 +663,16 @@ export const makeMessagesSocket = (config) => {
664
663
  const cleared = existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null
665
664
  try { await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } }) } catch (err) { logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup') }
666
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
+ }
667
676
  if (tcTokenBuffer?.length && sock.serverProps?.privacyTokenOn1to1) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
668
677
  const isProtocolMsg = !!normalizeMessageContent(message)?.protocolMessage
669
678
  const isBotOrPSA = isJidBot(destinationJid) || isJidMetaAI(destinationJid)
@@ -829,7 +838,7 @@ export const makeMessagesSocket = (config) => {
829
838
  sendReceipt, sendReceipts, nexus, readMessages, refreshMediaConn,
830
839
  waUploadToServer, fetchPrivacySettings, sendPeerDataOperationMessage,
831
840
  createParticipantNodes, getUSyncDevices, messageRetryManager, updateMemberLabel,
832
- userDevicesCache, devicesMutex,
841
+ userDevicesCache, devicesMutex, placeholderResendCache,
833
842
 
834
843
  updateMediaMessage: async (message) => {
835
844
  const content = assertMediaContent(message.message)
@@ -942,5 +951,16 @@ export const makeMessagesSocket = (config) => {
942
951
  sendSticker: (jid, sticker, options = {}) => sendMessage(jid, { sticker, ...options }, options),
943
952
  sendContact: (jid, contact, options = {}) => sendMessage(jid, { contacts: { contacts: Array.isArray(contact) ? contact : [contact] }, ...options }, options),
944
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 }),
945
965
  }
946
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)