@nexustechpro/baileys 2.0.2 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +924 -1299
  3. package/WAProto/index.js +22 -18
  4. package/lib/Defaults/baileys-version.json +6 -2
  5. package/lib/Defaults/index.js +173 -172
  6. package/lib/Signal/libsignal.js +395 -292
  7. package/lib/Signal/lid-mapping.js +264 -171
  8. package/lib/Socket/Client/index.js +2 -2
  9. package/lib/Socket/Client/types.js +10 -10
  10. package/lib/Socket/Client/websocket.js +45 -310
  11. package/lib/Socket/business.js +375 -375
  12. package/lib/Socket/chats.js +916 -963
  13. package/lib/Socket/communities.js +430 -430
  14. package/lib/Socket/groups.js +342 -342
  15. package/lib/Socket/index.js +21 -22
  16. package/lib/Socket/messages-recv.js +963 -743
  17. package/lib/Socket/messages-send.js +273 -321
  18. package/lib/Socket/mex.js +50 -50
  19. package/lib/Socket/newsletter.js +148 -148
  20. package/lib/Socket/nexus-handler.js +296 -247
  21. package/lib/Socket/registration.js +50 -33
  22. package/lib/Socket/socket.js +872 -1201
  23. package/lib/Store/index.js +5 -5
  24. package/lib/Store/make-cache-manager-store.js +81 -81
  25. package/lib/Store/make-in-memory-store.js +416 -416
  26. package/lib/Store/make-ordered-dictionary.js +81 -81
  27. package/lib/Store/object-repository.js +30 -30
  28. package/lib/Types/Auth.js +1 -1
  29. package/lib/Types/Bussines.js +1 -1
  30. package/lib/Types/Call.js +1 -1
  31. package/lib/Types/Chat.js +7 -7
  32. package/lib/Types/Contact.js +1 -1
  33. package/lib/Types/Events.js +1 -1
  34. package/lib/Types/GroupMetadata.js +1 -1
  35. package/lib/Types/Label.js +24 -24
  36. package/lib/Types/LabelAssociation.js +6 -6
  37. package/lib/Types/Message.js +10 -10
  38. package/lib/Types/Newsletter.js +37 -29
  39. package/lib/Types/Product.js +1 -1
  40. package/lib/Types/Signal.js +1 -1
  41. package/lib/Types/Socket.js +2 -2
  42. package/lib/Types/State.js +55 -12
  43. package/lib/Types/USync.js +1 -1
  44. package/lib/Types/index.js +25 -25
  45. package/lib/Utils/auth-utils.js +264 -256
  46. package/lib/Utils/baileys-event-stream.js +55 -55
  47. package/lib/Utils/browser-utils.js +27 -27
  48. package/lib/Utils/business.js +228 -230
  49. package/lib/Utils/chat-utils.js +726 -764
  50. package/lib/Utils/companion-reg-client-utils.js +34 -0
  51. package/lib/Utils/crypto.js +109 -135
  52. package/lib/Utils/decode-wa-message.js +342 -314
  53. package/lib/Utils/event-buffer.js +547 -547
  54. package/lib/Utils/generics.js +295 -297
  55. package/lib/Utils/history.js +91 -83
  56. package/lib/Utils/index.js +25 -20
  57. package/lib/Utils/key-store.js +17 -0
  58. package/lib/Utils/link-preview.js +107 -98
  59. package/lib/Utils/logger.js +2 -2
  60. package/lib/Utils/lt-hash.js +47 -47
  61. package/lib/Utils/make-mutex.js +39 -39
  62. package/lib/Utils/message-retry-manager.js +148 -148
  63. package/lib/Utils/messages-media.js +579 -535
  64. package/lib/Utils/messages.js +821 -706
  65. package/lib/Utils/noise-handler.js +255 -255
  66. package/lib/Utils/pre-key-manager.js +105 -105
  67. package/lib/Utils/process-message.js +430 -412
  68. package/lib/Utils/reporting-utils.js +155 -0
  69. package/lib/Utils/signal.js +191 -159
  70. package/lib/Utils/sync-action-utils.js +33 -0
  71. package/lib/Utils/tc-token-utils.js +162 -0
  72. package/lib/Utils/use-multi-file-auth-state.js +120 -120
  73. package/lib/Utils/validate-connection.js +194 -194
  74. package/lib/WABinary/constants.js +1306 -1300
  75. package/lib/WABinary/decode.js +237 -237
  76. package/lib/WABinary/encode.js +232 -232
  77. package/lib/WABinary/generic-utils.js +252 -211
  78. package/lib/WABinary/index.js +6 -5
  79. package/lib/WABinary/jid-utils.js +279 -95
  80. package/lib/WABinary/types.js +1 -1
  81. package/lib/WAM/BinaryInfo.js +9 -9
  82. package/lib/WAM/constants.js +22852 -22852
  83. package/lib/WAM/encode.js +149 -149
  84. package/lib/WAM/index.js +3 -3
  85. package/lib/WAUSync/Protocols/USyncContactProtocol.js +28 -28
  86. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +53 -53
  87. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -26
  88. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -37
  89. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -50
  90. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -28
  91. package/lib/WAUSync/Protocols/index.js +4 -4
  92. package/lib/WAUSync/USyncQuery.js +93 -93
  93. package/lib/WAUSync/USyncUser.js +22 -22
  94. package/lib/WAUSync/index.js +3 -3
  95. package/lib/index.js +65 -66
  96. package/package.json +172 -143
  97. package/lib/Signal/Group/ciphertext-message.js +0 -12
  98. package/lib/Signal/Group/group-session-builder.js +0 -30
  99. package/lib/Signal/Group/group_cipher.js +0 -100
  100. package/lib/Signal/Group/index.js +0 -12
  101. package/lib/Signal/Group/keyhelper.js +0 -18
  102. package/lib/Signal/Group/sender-chain-key.js +0 -26
  103. package/lib/Signal/Group/sender-key-distribution-message.js +0 -63
  104. package/lib/Signal/Group/sender-key-message.js +0 -66
  105. package/lib/Signal/Group/sender-key-name.js +0 -48
  106. package/lib/Signal/Group/sender-key-record.js +0 -41
  107. package/lib/Signal/Group/sender-key-state.js +0 -84
  108. package/lib/Signal/Group/sender-message-key.js +0 -26
@@ -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 } from '../Utils/link-preview.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'
@@ -22,16 +21,12 @@ const {
22
21
 
23
22
  const {
24
23
  areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser,
25
- isJidGroup, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET,
26
- getBinaryFilteredButtons, STORIES_JID, isJidUser, getButtonArgs, getButtonType
24
+ isJidBroadcast, isJidGroup, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET,
25
+ getBinaryFilteredButtons, STORIES_JID, isJidUser, getButtonArgs, getButtonType, isJidBot, isJidMetaAI
27
26
  } = WABinary
28
27
 
29
28
  export const makeMessagesSocket = (config) => {
30
- const {
31
- logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview,
32
- options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata,
33
- enableRecentMessageCache, maxMsgRetryCount
34
- } = config
29
+ const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount, getMessage } = config
35
30
 
36
31
  const sock = makeNewsletterSocket(config)
37
32
  const {
@@ -40,16 +35,15 @@ export const makeMessagesSocket = (config) => {
40
35
  } = sock
41
36
 
42
37
  const userDevicesCache = config.userDevicesCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
43
- const peerSessionsCache = new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
38
+ const devicesMutex = makeMutex()
44
39
  const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null
45
40
  const encryptionMutex = makeKeyedMutex()
41
+
42
+ // Prevents duplicate TC token IQ requests from concurrent sends
43
+ const inFlightTcTokenIssuance = new Set()
44
+
46
45
  let mediaConn
47
46
 
48
- // ─────────────────────────────────────────────
49
- // MEDIA CONNECTION
50
- // Fetches and caches the WhatsApp media upload connection.
51
- // Refreshes automatically when TTL expires or forced.
52
- // ─────────────────────────────────────────────
53
47
  const refreshMediaConn = async (forceGet = false) => {
54
48
  const media = await mediaConn
55
49
  if (!media || forceGet || Date.now() - media.fetchDate.getTime() > media.ttl * 1000) {
@@ -68,18 +62,26 @@ export const makeMessagesSocket = (config) => {
68
62
  return mediaConn
69
63
  }
70
64
 
71
- // ─────────────────────────────────────────────
72
- // RECEIPTS
73
- // Sends read receipts and delivery confirmations.
74
- // ─────────────────────────────────────────────
65
+ const waUploadToServer = getWAUploadToServer(config, refreshMediaConn)
66
+
75
67
  const sendReceipt = async (jid, participant, messageIds, type) => {
76
68
  if (!messageIds?.length) throw new Boom('missing ids in receipt')
77
69
  const node = { tag: 'receipt', attrs: { id: messageIds[0] } }
78
70
  const isReadReceipt = type === 'read' || type === 'read-self'
79
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
+ }
80
79
  if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) {
81
80
  node.attrs.recipient = jid
82
81
  node.attrs.to = participant
82
+ } else if (isJidStatusBroadcast(jid) && participant) {
83
+ node.attrs.to = jid
84
+ node.attrs.participant = participant
83
85
  } else {
84
86
  node.attrs.to = jid
85
87
  if (participant) node.attrs.participant = participant
@@ -101,14 +103,11 @@ export const makeMessagesSocket = (config) => {
101
103
 
102
104
  const readMessages = async (keys) => {
103
105
  const privacySettings = await fetchPrivacySettings()
104
- 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)
105
109
  }
106
110
 
107
- // ─────────────────────────────────────────────
108
- // DEVICE & SESSION MANAGEMENT
109
- // Fetches participant devices via USync and manages
110
- // Signal protocol sessions for encryption.
111
- // ─────────────────────────────────────────────
112
111
  const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
113
112
  const deviceResults = []
114
113
  if (!useCache) logger.debug('not using cache for devices')
@@ -117,7 +116,6 @@ export const makeMessagesSocket = (config) => {
117
116
  const decoded = jidDecode(jid)
118
117
  const user = decoded?.user
119
118
  const device = decoded?.device
120
- // Already has a device number — push directly
121
119
  if (typeof device === 'number' && device >= 0 && user) { deviceResults.push({ user, device, jid }); return null }
122
120
  return { jid: jidNormalizedUser(jid), user }
123
121
  }).filter(Boolean)
@@ -144,7 +142,6 @@ export const makeMessagesSocket = (config) => {
144
142
 
145
143
  if (!toFetch.length) return deviceResults
146
144
 
147
- // Track which JIDs are LID-based so we can encode them correctly
148
145
  const requestedLidUsers = new Set()
149
146
  for (const jid of toFetch) {
150
147
  if (isLidUser(jid) || isHostedLidUser(jid)) {
@@ -153,23 +150,21 @@ export const makeMessagesSocket = (config) => {
153
150
  }
154
151
  }
155
152
 
156
- const query = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
157
- for (const jid of toFetch) query.withUser(new USyncUser().withId(jid))
153
+ const usyncQuery = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
154
+ for (const jid of toFetch) usyncQuery.withUser(new USyncUser().withId(jid))
158
155
 
159
- const result = await sock.executeUSyncQuery(query)
156
+ const result = await sock.executeUSyncQuery(usyncQuery)
160
157
  if (result) {
161
158
  const lidResults = result.list.filter(a => !!a.lid)
162
159
  if (lidResults.length > 0) {
163
160
  logger.trace('Storing LID maps from device call')
164
161
  await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })))
165
- }
166
-
167
- // Pre-assert sessions for newly discovered LID users
168
- try {
169
- const lids = lidResults.map(a => a.lid)
170
- if (lids.length) await assertSessions(lids, false)
171
- } catch (e) {
172
- logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
162
+ try {
163
+ const lids = lidResults.map(a => a.lid)
164
+ if (lids.length) await assertSessions(lids, false)
165
+ } catch (e) {
166
+ logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
167
+ }
173
168
  }
174
169
 
175
170
  const extracted = extractDeviceJids(result?.list, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices)
@@ -187,27 +182,25 @@ export const makeMessagesSocket = (config) => {
187
182
  }
188
183
  }
189
184
 
190
- // Cache results
191
- if (userDevicesCache.mset) {
192
- await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
193
- } else {
194
- for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
195
- }
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
+ })
196
192
 
197
- // Persist device lists for session migration support (capped at 500 users)
193
+ // Persist device lists for session migration (capped at 500 users)
198
194
  const userDeviceUpdates = {}
199
195
  for (const [userId, devices] of Object.entries(deviceMap)) {
200
196
  if (devices?.length > 0) userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
201
197
  }
202
198
  if (Object.keys(userDeviceUpdates).length > 0) {
203
199
  try {
204
- const existingData = await authState.keys.get('device-list', ['_index'])
205
- const currentBatch = existingData?.['_index'] || {}
200
+ const currentBatch = await migrateIndexKey(authState.keys, 'device-list')
206
201
  const mergedBatch = { ...currentBatch, ...userDeviceUpdates }
207
- const trimmedBatch = {}
208
- Object.keys(mergedBatch).sort().slice(-500).forEach(userId => { trimmedBatch[userId] = mergedBatch[userId] })
209
- await authState.keys.set({ 'device-list': { '_index': trimmedBatch } })
210
- logger.debug({ userCount: Object.keys(userDeviceUpdates).length, batchSize: Object.keys(trimmedBatch).length }, 'stored user device lists')
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')
211
204
  } catch (error) {
212
205
  logger.warn({ error }, 'failed to store user device lists')
213
206
  }
@@ -217,47 +210,30 @@ export const makeMessagesSocket = (config) => {
217
210
  }
218
211
 
219
212
  const assertSessions = async (jids, force) => {
220
- const uniqueJids = [...new Set(jids)]
221
- const jidsRequiringFetch = []
222
-
223
- for (const jid of uniqueJids) {
224
- const signalId = signalRepository.jidToSignalProtocolAddress(jid)
225
- const cachedSession = peerSessionsCache.get(signalId)
226
- if (cachedSession !== undefined) {
227
- if (cachedSession && !force) continue
228
- } else {
229
- const sessionValidation = await signalRepository.validateSession(jid)
230
- peerSessionsCache.set(signalId, sessionValidation.exists)
231
- if (sessionValidation.exists && !force) continue
213
+ let didFetchNewSession = false
214
+ let jidsRequiringFetch = []
215
+ if (force) {
216
+ jidsRequiringFetch = jids
217
+ } 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)
232
222
  }
233
- jidsRequiringFetch.push(jid)
234
223
  }
235
-
236
- if (!jidsRequiringFetch.length) return false
237
-
238
- // Resolve LID JIDs for wire-level session fetch
239
- const wireJids = [
240
- ...jidsRequiringFetch.filter(jid => isLidUser(jid) || isHostedLidUser(jid)),
241
- ...(await signalRepository.lidMapping.getLIDsForPNs(
242
- jidsRequiringFetch.filter(jid => isPnUser(jid) || isHostedPnUser(jid))
243
- ) || []).map(a => a.lid)
244
- ]
245
-
246
- logger.debug({ jidsRequiringFetch, wireJids }, 'fetching sessions')
247
- const result = await query({
248
- tag: 'iq',
249
- attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET },
250
- content: [{ tag: 'key', attrs: {}, content: wireJids.map(jid => { const attrs = { jid }; if (force) attrs.reason = 'identity'; return { tag: 'user', attrs } }) }]
251
- })
252
- await parseAndInjectE2ESessions(result, signalRepository)
253
- for (const wireJid of wireJids) peerSessionsCache.set(signalRepository.jidToSignalProtocolAddress(wireJid), true)
254
- return true
224
+ if (jidsRequiringFetch.length) {
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' } : {}) } })) }] })
232
+ await parseAndInjectE2ESessions(result, signalRepository)
233
+ didFetchNewSession = true
234
+ }
235
+ return didFetchNewSession
255
236
  }
256
-
257
- // ─────────────────────────────────────────────
258
- // PEER DATA OPERATIONS
259
- // Used for history sync requests and placeholder resends.
260
- // ─────────────────────────────────────────────
261
237
  const sendPeerDataOperationMessage = async (pdoMessage) => {
262
238
  if (!authState.creds.me?.id) throw new Boom('Not authenticated')
263
239
  return await relayMessage(jidNormalizedUser(authState.creds.me.id), {
@@ -265,18 +241,24 @@ export const makeMessagesSocket = (config) => {
265
241
  }, { additionalAttributes: { category: 'peer', push_priority: 'high_force' }, additionalNodes: [{ tag: 'meta', attrs: { appdata: 'default' } }] })
266
242
  }
267
243
 
268
- // ─────────────────────────────────────────────
269
- // TC TOKEN (Trusted Contact Token)
270
- // Used for end-to-end privacy verification with contacts.
271
- // ─────────────────────────────────────────────
272
- const TOKEN_EXPIRY_TTL = 24 * 60 * 60 // 24 hours in seconds
273
-
274
- const isTokenExpired = (tokenData) => {
275
- if (!tokenData || !tokenData.timestamp) return true
276
- return unixTimestampSeconds() - Number(tokenData.timestamp) > TOKEN_EXPIRY_TTL
244
+ // Issues our TC token to a contact so they can send us private messages. Fire-and-forget.
245
+ const issuePrivacyTokens = async (jids, timestamp) => {
246
+ const t = (timestamp ?? unixTimestampSeconds()).toString()
247
+ return query({
248
+ tag: 'iq',
249
+ attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
250
+ content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }]
251
+ })
277
252
  }
278
253
 
279
- const parseTCTokens = (result) => {
254
+ // Fetches TC tokens from the server for the given JIDs and stores them locally.
255
+ const getPrivacyTokens = async (jids) => {
256
+ const t = unixTimestampSeconds().toString()
257
+ const result = await query({
258
+ tag: 'iq',
259
+ attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
260
+ content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }]
261
+ })
280
262
  const tokens = {}
281
263
  const tokenList = getBinaryNodeChild(result, 'tokens')
282
264
  if (tokenList) {
@@ -285,25 +267,10 @@ export const makeMessagesSocket = (config) => {
285
267
  if (jid && content) tokens[jid] = { token: content, timestamp: Number(unixTimestampSeconds()) }
286
268
  }
287
269
  }
288
- return tokens
289
- }
290
-
291
- const getPrivacyTokens = async (jids) => {
292
- const t = unixTimestampSeconds().toString()
293
- const result = await query({
294
- tag: 'iq',
295
- attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
296
- content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }]
297
- })
298
- const tokens = parseTCTokens(result)
299
270
  if (Object.keys(tokens).length > 0) await authState.keys.set({ 'tctoken': tokens })
300
271
  return tokens
301
272
  }
302
273
 
303
- // ─────────────────────────────────────────────
304
- // GROUP MEMBER LABEL
305
- // Sets a custom label for a member in a group.
306
- // ─────────────────────────────────────────────
307
274
  const updateMemberLabel = (jid, memberLabel) => {
308
275
  if (!memberLabel || typeof memberLabel !== 'string') throw new Error('Member label must be a non-empty string')
309
276
  if (!isJidGroup(jid)) throw new Error('Member labels can only be set in groups')
@@ -315,20 +282,19 @@ export const makeMessagesSocket = (config) => {
315
282
  }, { additionalNodes: [{ tag: 'meta', attrs: { tag_reason: 'user_update', appdata: 'member_tag' }, content: undefined }] })
316
283
  }
317
284
 
318
- // ─────────────────────────────────────────────
319
- // MESSAGE TYPE DETECTION
320
- // Classifies messages for proper stanza type attribute.
321
- // ─────────────────────────────────────────────
322
285
  const getMessageType = (msg) => {
323
286
  const message = normalizeMessageContent(msg)
287
+ if (!message) return 'text'
324
288
  if (message.pollCreationMessage || message.pollCreationMessageV2 || message.pollCreationMessageV3) return 'poll'
325
- if (message.reactionMessage) return 'reaction'
289
+ if (message.reactionMessage || message.encReactionMessage) return 'reaction'
326
290
  if (message.eventMessage) return 'event'
327
291
  if (getMediaType(message)) return 'media'
328
292
  return 'text'
329
293
  }
330
294
 
331
295
  const getMediaType = (message) => {
296
+ const inner = message.viewOnceMessage?.message || message.viewOnceMessageV2?.message || message.viewOnceMessageV2Extension?.message
297
+ if (inner) return getMediaType(inner)
332
298
  if (message.imageMessage) return 'image'
333
299
  if (message.stickerMessage) return message.stickerMessage.isLottie ? '1p_sticker' : message.stickerMessage.isAvatar ? 'avatar_sticker' : 'sticker'
334
300
  if (message.videoMessage) return message.videoMessage.gifPlayback ? 'gif' : 'video'
@@ -352,11 +318,6 @@ export const makeMessagesSocket = (config) => {
352
318
  if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) return 'url'
353
319
  }
354
320
 
355
- // ─────────────────────────────────────────────
356
- // PARTICIPANT NODE CREATION
357
- // Encrypts a message for each recipient JID and
358
- // wraps it in a binary <to> node for the stanza.
359
- // ─────────────────────────────────────────────
360
321
  const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
361
322
  if (!recipientJids.length) return { nodes: [], shouldIncludeDeviceIdentity: false }
362
323
 
@@ -373,7 +334,7 @@ export const makeMessagesSocket = (config) => {
373
334
  if (!jid) return null
374
335
  let msgToEncrypt = patchedMessage
375
336
 
376
- // Use Device Sent Message (DSM) for own linked devices so they can read the message
337
+ // Use DSM for own linked devices so they can read the message
377
338
  if (dsmMessage) {
378
339
  const { user: targetUser } = jidDecode(jid)
379
340
  const { user: ownPnUser } = jidDecode(meId)
@@ -389,7 +350,7 @@ export const makeMessagesSocket = (config) => {
389
350
  return { tag: 'to', attrs: { jid }, content: [{ tag: 'enc', attrs: { v: '2', type, ...(extraAttrs || {}) }, content: ciphertext }] }
390
351
  })
391
352
  } catch (err) {
392
- logger.error({ jid, err }, 'Failed to encrypt for recipient')
353
+ logger.warn({ jid, err: err?.message || err }, 'Failed to encrypt for recipient — no session, will retry on next interaction')
393
354
  return null
394
355
  }
395
356
  })
@@ -398,17 +359,6 @@ export const makeMessagesSocket = (config) => {
398
359
  return { nodes, shouldIncludeDeviceIdentity }
399
360
  }
400
361
 
401
- // ─────────────────────────────────────────────
402
- // RELAY MESSAGE
403
- // Core message sending function. Handles groups, DMs,
404
- // newsletters, status broadcasts, and retries.
405
- //
406
- // Key design decisions:
407
- // - senderKeyMap always starts empty (Promise.resolve({})) so
408
- // SKDM is always sent fresh — prevents "waiting for message"
409
- // errors caused by stale persisted sender key memory.
410
- // - LID vs PN identity is resolved per group's addressingMode.
411
- // ─────────────────────────────────────────────
412
362
  const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList, quoted } = {}) => {
413
363
  const meId = authState.creds.me.id
414
364
  const meLid = authState.creds.me?.lid
@@ -418,7 +368,6 @@ export const makeMessagesSocket = (config) => {
418
368
  const isLid = server === 'lid'
419
369
  const isNewsletter = server === 'newsletter'
420
370
 
421
- // Choose sender identity (PN or LID) based on destination
422
371
  let activeSender = meId
423
372
  let groupAddressingMode = 'pn'
424
373
  if (isGroup && !isStatus) {
@@ -460,6 +409,7 @@ export const makeMessagesSocket = (config) => {
460
409
  const meMsg = { deviceSentMessage: { destinationJid, message }, messageContextInfo: message.messageContextInfo }
461
410
  const extraAttrs = {}
462
411
  const messages = normalizeMessageContent(message)
412
+ const reportingMessage = messages
463
413
  const buttonType = getButtonType(messages)
464
414
 
465
415
  let hasDeviceFanoutFalse = false
@@ -473,7 +423,6 @@ export const makeMessagesSocket = (config) => {
473
423
  const mediaType = getMediaType(message)
474
424
  if (mediaType) extraAttrs.mediatype = mediaType
475
425
 
476
- // ── Newsletter: plaintext encoding, no encryption ──
477
426
  if (isNewsletter) {
478
427
  const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
479
428
  binaryNodeContent.push({ tag: 'plaintext', attrs: {}, content: encodeNewsletterMessage(patched) })
@@ -482,11 +431,10 @@ export const makeMessagesSocket = (config) => {
482
431
  return
483
432
  }
484
433
 
485
- if (messages.pinInChatMessage || messages.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) {
434
+ if (messages?.pinInChatMessage || messages?.keepInChatMessage || (message.reactionMessage && !isStatus) || message.protocolMessage?.editedMessage) {
486
435
  extraAttrs['decrypt-fail'] = 'hide'
487
436
  }
488
437
 
489
- // ── Group / Status: sender key (SKDM + skmsg) ──
490
438
  if ((isGroup || isStatus) && !isRetryResend) {
491
439
  const [groupData] = await Promise.all([
492
440
  (async () => {
@@ -495,10 +443,9 @@ export const makeMessagesSocket = (config) => {
495
443
  else if (!isStatus) groupData = await groupMetadata(jid)
496
444
  return groupData
497
445
  })(),
498
- Promise.resolve({}) // senderKeyMap intentionally always empty — forces fresh SKDM every send
446
+ Promise.resolve({}) // senderKeyMap always empty — forces fresh SKDM every send
499
447
  ])
500
448
 
501
- // Build participant list
502
449
  const participantsList = []
503
450
  if (isStatus) {
504
451
  if (statusJidList?.length) participantsList.push(...statusJidList)
@@ -515,9 +462,7 @@ export const makeMessagesSocket = (config) => {
515
462
  const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
516
463
  devices.push(...additionalDevices)
517
464
 
518
- // Ensure Device 0 (primary phone) is always included for every participant.
519
- // USync sometimes omits Device 0 for LID groups to save bandwidth,
520
- // which would cause recipients to miss the SKDM on first send.
465
+ // Force Device 0 inclusion USync sometimes omits it for LID groups
521
466
  for (const pJid of participantsList) {
522
467
  const decoded = jidDecode(pJid)
523
468
  if (decoded?.user && !devices.some(d => d.user === decoded.user && d.device === 0)) {
@@ -529,11 +474,11 @@ export const makeMessagesSocket = (config) => {
529
474
  if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
530
475
 
531
476
  const bytes = encodeWAMessage(patched)
477
+ const bytesU8 = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
532
478
  const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
533
479
  const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
534
- const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
480
+ const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytesU8, meId: groupSenderIdentity })
535
481
 
536
- // Send SKDM to ALL devices every time (senderKeyMap is always {})
537
482
  const senderKeyRecipients = devices
538
483
  .filter(d => !isHostedLidUser(d.jid) && !isHostedPnUser(d.jid) && d.device !== 99)
539
484
  .map(d => d.jid)
@@ -549,7 +494,6 @@ export const makeMessagesSocket = (config) => {
549
494
 
550
495
  binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg', ...extraAttrs }, content: ciphertext })
551
496
 
552
- // ── Group Retry: direct pairwise re-encrypt for specific participant ──
553
497
  } else if ((isGroup || isStatus) && isRetryResend) {
554
498
  const groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
555
499
  if (!groupData && !isStatus) await groupMetadata(jid)
@@ -561,21 +505,24 @@ export const makeMessagesSocket = (config) => {
561
505
  if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
562
506
 
563
507
  const bytes = encodeWAMessage(patched)
508
+ const bytesU8 = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
564
509
  const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
565
510
  const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
566
- const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
511
+ const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytesU8, meId: groupSenderIdentity })
567
512
 
568
- // Send fresh SKDM directly to the requesting participant
569
513
  const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
570
514
  await assertSessions([participant.jid])
571
515
  const skResult = await createParticipantNodes([participant.jid], senderKeyMsg, {})
572
516
  shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || skResult.shouldIncludeDeviceIdentity
573
517
  participants.push(...skResult.nodes)
574
518
 
575
- const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({ data: bytes, jid: participant?.jid })
519
+ // For retry resend, encrypt directly to the requesting participant
520
+ const isParticipantLid = isLidUser(participant.jid)
521
+ const isMe = areJidsSameUser(participant.jid, isParticipantLid ? meLid : meId)
522
+ const encodedMsg = isMe ? encodeWAMessage({ deviceSentMessage: { destinationJid, message } }) : encodeWAMessage(message)
523
+ const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({ data: encodedMsg, jid: participant.jid })
576
524
  binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type, count: participant.count.toString() }, content: encryptedContent })
577
525
 
578
- // ── DM / LID: standard pairwise encryption ──
579
526
  } else {
580
527
  let ownId = meId
581
528
  if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
@@ -593,7 +540,6 @@ export const makeMessagesSocket = (config) => {
593
540
  }
594
541
 
595
542
  if (additionalAttributes?.category !== 'peer') {
596
- // Preserve Device 0 entries before USync refetch which may omit them
597
543
  const device0Entries = devices.filter(d => d.device === 0)
598
544
  const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
599
545
  devices.length = 0
@@ -603,7 +549,6 @@ export const makeMessagesSocket = (config) => {
603
549
  const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
604
550
  devices.push(...device0Entries, ...sessionDevices)
605
551
 
606
- // Explicitly fetch sender's linked devices if not returned by USync
607
552
  if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
608
553
  const senderDevices = await getUSyncDevices([senderIdentity], true, false)
609
554
  const senderLinkedDevices = senderDevices.filter(d => d.device !== 0 && d.user === senderOwnUser)
@@ -639,7 +584,6 @@ export const makeMessagesSocket = (config) => {
639
584
  shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
640
585
  }
641
586
 
642
- // ── Build final stanza ──
643
587
  if (participants.length) {
644
588
  if (additionalAttributes?.category === 'peer') {
645
589
  const peerNode = participants[0]?.content?.[0]
@@ -670,41 +614,90 @@ export const makeMessagesSocket = (config) => {
670
614
  stanza.attrs.to = destinationJid
671
615
  }
672
616
 
673
- // Attach button metadata if needed
674
- if (!isNewsletter && buttonType) {
617
+ let didPushAdditional = false
618
+
619
+ if (!isNewsletter && buttonType && !isStatus) {
675
620
  const buttonsNode = getButtonArgs(messages)
676
621
  const filteredButtons = getBinaryFilteredButtons(additionalNodes || [])
677
- if (filteredButtons) { stanza.content.push(...additionalNodes) }
678
- else stanza.content.push(buttonsNode)
679
- } else if (additionalNodes?.length > 0) {
622
+ if (filteredButtons) {
623
+ stanza.content.push(...additionalNodes)
624
+ didPushAdditional = true
625
+ } else {
626
+ stanza.content.push(...buttonsNode)
627
+ }
628
+ }
629
+
630
+ if (!didPushAdditional && additionalNodes?.length > 0) {
680
631
  stanza.content.push(...additionalNodes)
681
632
  }
682
633
 
683
- // Attach device identity for LID groups and pkmsg sessions
684
634
  if ((shouldIncludeDeviceIdentity || (meLid && (isLid || (isGroup && groupAddressingMode === 'lid')))) && !isNewsletter) {
685
635
  stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) })
686
636
  logger.debug({ jid }, 'adding device identity')
687
637
  }
688
638
 
689
- // Attach TC token for DM messages (auto-refresh if expired)
690
- if (!isGroup && !isRetryResend && !isStatus) {
691
- const contactTcTokenData = await authState.keys.get('tctoken', [destinationJid])
692
- let tcTokenBuffer = contactTcTokenData[destinationJid]?.token
693
- if (isTokenExpired(contactTcTokenData[destinationJid])) {
694
- logger.debug({ jid: destinationJid }, 'tctoken expired, refreshing')
695
- try {
696
- const freshTokens = await getPrivacyTokens([destinationJid])
697
- tcTokenBuffer = freshTokens[destinationJid]?.token
698
- } catch (err) {
699
- logger.warn({ jid: destinationJid, err }, 'failed to refresh expired tctoken')
639
+ const isPeerMessage = additionalAttributes?.category === 'peer'
640
+ const is1on1 = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage
641
+ if (is1on1) {
642
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
643
+ const tcTokenJid = await resolveTcTokenJid(destinationJid, getLIDForPN)
644
+ const contactTcTokenData = await authState.keys.get('tctoken', [tcTokenJid])
645
+ const existingEntry = contactTcTokenData[tcTokenJid]
646
+ let tcTokenBuffer = existingEntry?.token
647
+ if (tcTokenBuffer?.length && isTcTokenExpired(existingEntry?.timestamp)) {
648
+ logger.debug({ jid: destinationJid, timestamp: existingEntry?.timestamp }, 'tctoken expired, clearing')
649
+ tcTokenBuffer = undefined
650
+ const cleared = existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null
651
+ try { await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } }) } catch (err) { logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup') }
652
+ }
653
+ if (tcTokenBuffer?.length && sock.serverProps?.privacyTokenOn1to1) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
654
+ const isProtocolMsg = !!normalizeMessageContent(message)?.protocolMessage
655
+ const isBotOrPSA = isJidBot(destinationJid) || isJidMetaAI(destinationJid)
656
+ if (!isProtocolMsg && !isBotOrPSA && shouldSendNewTcToken(existingEntry?.senderTimestamp) && !inFlightTcTokenIssuance.has(tcTokenJid)) {
657
+ inFlightTcTokenIssuance.add(tcTokenJid)
658
+ const issueTimestamp = unixTimestampSeconds()
659
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
660
+ const issueToLid = sock.serverProps?.lidTrustedTokenIssueToLid ?? false
661
+ resolveIssuanceJid(destinationJid, issueToLid, getLIDForPN, getPNForLID)
662
+ .then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
663
+ .then(async (result) => {
664
+ await storeTcTokensFromIqResult({ result, fallbackJid: tcTokenJid, keys: authState.keys, getLIDForPN })
665
+ const currentData = await authState.keys.get('tctoken', [tcTokenJid])
666
+ const currentEntry = currentData[tcTokenJid]
667
+ const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid])
668
+ await authState.keys.set({ tctoken: { [tcTokenJid]: { token: Buffer.alloc(0), ...currentEntry, senderTimestamp: issueTimestamp }, ...indexWrite } })
669
+ })
670
+ .catch(err => logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed'))
671
+ .finally(() => inFlightTcTokenIssuance.delete(tcTokenJid))
672
+ }
673
+ }
674
+ if (
675
+ !isNewsletter &&
676
+ !isRetryResend &&
677
+ reportingMessage?.messageContextInfo?.messageSecret &&
678
+ shouldIncludeReportingToken(reportingMessage)
679
+ ) {
680
+ try {
681
+ const encoded = encodeWAMessage(reportingMessage)
682
+ const reportingKey = {
683
+ id: finalMsgId,
684
+ fromMe: true,
685
+ remoteJid: destinationJid,
686
+ participant: participant?.jid
700
687
  }
688
+ const reportingNode = await getMessageReportingToken(encoded, reportingMessage, reportingKey)
689
+ if (reportingNode) {
690
+ stanza.content.push(reportingNode)
691
+ logger.trace({ jid }, 'added reporting token to message')
692
+ }
693
+ } catch (error) {
694
+ logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token')
701
695
  }
702
- if (tcTokenBuffer) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
703
696
  }
704
697
 
705
- logger.debug({ msgId }, `sending message to ${participants.length} devices`)
698
+ logger.debug({ msgId: finalMsgId }, `sending message to ${participants.length} devices`)
706
699
  await sendNode(stanza)
707
- if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid, msgId, message)
700
+ if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid, finalMsgId, message)
708
701
 
709
702
  }, activeSender)
710
703
 
@@ -722,21 +715,98 @@ export const makeMessagesSocket = (config) => {
722
715
  }
723
716
  }
724
717
 
725
- const waUploadToServer = getWAUploadToServer(config, refreshMediaConn)
726
- const nexus = new NexusHandler(Utils, waUploadToServer, relayMessage, { logger, mediaCache: config.mediaCache, options: config.options, mediaUploadTimeoutMs: config.mediaUploadTimeoutMs, user: authState.creds.me })
718
+ 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 }) })
727
719
  const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update')
728
720
 
721
+ const sendMessage = async (jid, content, options = {}) => {
722
+ const meId = authState.creds.me.id
723
+ const meLid = authState.creds.me?.lid
724
+ const { server } = jidDecode(jid)
725
+ const isGroup = server === 'g.us'
726
+ const isDestinationLid = server === 'lid'
727
+ const useCache = options.useCachedGroupMetadata !== false
728
+ const { quoted } = options
729
+
730
+ let activeSender = meId
731
+ let addressingMode = 'pn'
732
+ if (isGroup) {
733
+ const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
734
+ addressingMode = groupData?.addressingMode || 'lid'
735
+ if (addressingMode === 'lid' && meLid) activeSender = meLid
736
+ } else if (isDestinationLid && meLid) {
737
+ activeSender = meLid
738
+ addressingMode = 'lid'
739
+ }
740
+
741
+ // Unwrap shorthand `interactive` key
742
+ if (content.interactive && !content.interactiveMessage) {
743
+ const { interactive, ...rest } = content
744
+ content = { ...rest, interactiveMessage: interactive }
745
+ }
746
+
747
+ // never route status reactions through nexus
748
+ const isStatusJid = jid === 'status@broadcast'
749
+ const messageType = !isStatusJid && nexus.detectType(content)
750
+ if (messageType) return await nexus.processMessage(content, jid, quoted)
751
+
752
+ if (content.disappearingMessagesInChat && isJidGroup(jid)) {
753
+ const value = typeof content.disappearingMessagesInChat === 'boolean'
754
+ ? (content.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0)
755
+ : content.disappearingMessagesInChat
756
+ await groupToggleEphemeral(jid, value)
757
+ return
758
+ }
759
+
760
+
761
+ const fullMsg = await generateWAMessage(jid, content, {
762
+ logger,
763
+ userJid: jidNormalizedUser(activeSender),
764
+ getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
765
+ getProfilePicUrl: sock.profilePictureUrl,
766
+ getCallLink: sock.createCallLink,
767
+ upload: waUploadToServer,
768
+ mediaCache: config.mediaCache,
769
+ options: config.options,
770
+ messageId: generateMessageIDV2(activeSender),
771
+ ...options
772
+ })
773
+
774
+ const additionalAttributes = {}, additionalNodes = []
775
+ if (content.delete) {
776
+ const fromMe = content.delete?.fromMe
777
+ const isGroupDelete = isJidGroup(content.delete?.remoteJid)
778
+ additionalAttributes.edit = (isGroupDelete && !fromMe) ? '8' : '7'
779
+ } else if (content.edit) {
780
+ additionalAttributes.edit = '1'
781
+ } else if (content.pin) {
782
+ additionalAttributes.edit = '2'
783
+ }
784
+ if (content.poll) additionalNodes.push({ tag: 'meta', attrs: { polltype: 'creation' } })
785
+ if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
786
+ if (content.ai) additionalNodes.push({ tag: 'bot', attrs: { biz_bot: '1' } })
787
+
788
+ await relayMessage(jid, fullMsg.message, {
789
+ messageId: fullMsg.key.id,
790
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
791
+ additionalAttributes,
792
+ statusJidList: options.statusJidList,
793
+ additionalNodes
794
+ })
795
+
796
+ if (config.emitOwnEvents) {
797
+ process.nextTick(() => processingMutex.mutex(() => upsertMessage(fullMsg, 'append')))
798
+ }
799
+ return fullMsg
800
+ }
801
+
729
802
  return {
730
803
  ...sock,
731
- getPrivacyTokens, assertSessions, relayMessage, sendReceipt, sendReceipts, nexus,
732
- readMessages, refreshMediaConn, waUploadToServer, fetchPrivacySettings,
733
- sendPeerDataOperationMessage, createParticipantNodes, getUSyncDevices,
734
- messageRetryManager, updateMemberLabel,
735
-
736
- // ─────────────────────────────────────────────
737
- // UPDATE MEDIA MESSAGE
738
- // Re-requests media upload for expired/missing media.
739
- // ─────────────────────────────────────────────
804
+ getPrivacyTokens, issuePrivacyTokens, assertSessions, relayMessage,
805
+ sendReceipt, sendReceipts, nexus, readMessages, refreshMediaConn,
806
+ waUploadToServer, fetchPrivacySettings, sendPeerDataOperationMessage,
807
+ createParticipantNodes, getUSyncDevices, messageRetryManager, updateMemberLabel,
808
+ userDevicesCache, devicesMutex,
809
+
740
810
  updateMediaMessage: async (message) => {
741
811
  const content = assertMediaContent(message.message)
742
812
  const mediaKey = content.mediaKey
@@ -767,11 +837,6 @@ export const makeMessagesSocket = (config) => {
767
837
  return message
768
838
  },
769
839
 
770
- // ─────────────────────────────────────────────
771
- // SEND STATUS MENTIONS
772
- // Broadcasts a status update and notifies mentioned
773
- // users or groups via statusMentionMessage protocol.
774
- // ─────────────────────────────────────────────
775
840
  sendStatusMentions: async (content, jids = []) => {
776
841
  const userJid = jidNormalizedUser(authState.creds.me.id)
777
842
  const allUsers = new Set([userJid])
@@ -825,7 +890,7 @@ export const makeMessagesSocket = (config) => {
825
890
  return msg
826
891
  },
827
892
 
828
- // Nexus handler shortcuts for rich message types
893
+ // Nexus handler shortcuts
829
894
  sendPaymentMessage: (jid, data, quoted) => nexus.handlePayment({ requestPaymentMessage: data }, jid, quoted),
830
895
  sendProductMessage: (jid, data, quoted) => nexus.handleProduct({ productMessage: data }, jid, quoted),
831
896
  sendInteractiveMessage: (jid, data, quoted) => nexus.handleInteractive({ interactiveMessage: data }, jid, quoted),
@@ -838,133 +903,20 @@ export const makeMessagesSocket = (config) => {
838
903
  sendCarouselMessage: (jid, data, quoted) => nexus.handleCarousel({ carouselMessage: data }, jid, quoted),
839
904
  sendCarouselProtoMessage: (jid, data, quoted) => nexus.handleCarouselProto({ carouselProto: data }, jid, quoted),
840
905
  stickerPackMessage: (jid, data, options) => nexus.handleStickerPack(data, jid, options?.quoted),
841
-
842
- // ─────────────────────────────────────────────
843
- // SEND MESSAGE
844
- // Main public API for sending any message type.
845
- // Handles delete normalization, LID/PN identity selection,
846
- // message generation, and relay.
847
- // ─────────────────────────────────────────────
848
- sendMessage: async (jid, content, options = {}) => {
849
- const meId = authState.creds.me.id
850
- const meLid = authState.creds.me?.lid
851
- const { server } = jidDecode(jid)
852
- const isGroup = server === 'g.us'
853
- const isDestinationLid = server === 'lid'
854
- const useCache = options.useCachedGroupMetadata !== false
855
- const { quoted } = options
856
-
857
- // Resolve sender identity for this destination
858
- let activeSender = meId
859
- let addressingMode = 'pn'
860
- if (isGroup) {
861
- const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
862
- addressingMode = groupData?.addressingMode || 'lid'
863
- if (addressingMode === 'lid' && meLid) activeSender = meLid
864
- } else if (isDestinationLid && meLid) {
865
- activeSender = meLid
866
- addressingMode = 'lid'
867
- }
868
-
869
- // Unwrap shorthand `interactive` key
870
- if (content.interactive && !content.interactiveMessage) {
871
- const { interactive, ...rest } = content
872
- content = { ...rest, interactiveMessage: interactive }
873
- }
874
-
875
- // Delegate rich message types to NexusHandler
876
- const messageType = nexus.detectType(content)
877
- if (messageType) return await nexus.processMessage(content, jid, quoted)
878
-
879
- // Handle disappearing message toggle for groups
880
- if (content.disappearingMessagesInChat && isJidGroup(jid)) {
881
- const value = typeof content.disappearingMessagesInChat === 'boolean'
882
- ? (content.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0)
883
- : content.disappearingMessagesInChat
884
- await groupToggleEphemeral(jid, value)
885
- return
886
- }
887
-
888
- // ── Normalize delete key ──
889
- // Groups require a participant field matching the sender's LID or PN identity.
890
- // edit="7" = delete own message, edit="8" = admin delete another's message.
891
- if (content.delete) {
892
- const deleteKey = content.delete
893
- if (!deleteKey.remoteJid || !deleteKey.id) {
894
- logger.error({ deleteKey }, 'Invalid delete key: missing remoteJid or id')
895
- throw new Boom('Delete key must have remoteJid and id', { statusCode: 400 })
896
- }
897
-
898
- const { server: deleteServer } = jidDecode(deleteKey.remoteJid)
899
- let deleteAddressingMode = deleteServer === 'lid' ? 'lid' : 'pn'
900
- if (isJidGroup(deleteKey.remoteJid)) {
901
- const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(deleteKey.remoteJid) : undefined
902
- deleteAddressingMode = groupData?.addressingMode || 'lid'
903
- }
904
-
905
- let normalizedParticipant = deleteKey.participant
906
- if (deleteKey.fromMe || isJidGroup(deleteKey.remoteJid)) {
907
- const senderJid = (deleteAddressingMode === 'lid' && meLid) ? meLid : meId
908
- normalizedParticipant = jidNormalizedUser(senderJid)
909
- }
910
-
911
- content.delete = {
912
- remoteJid: deleteKey.remoteJid,
913
- fromMe: deleteKey.fromMe === true || deleteKey.fromMe === 'true',
914
- id: deleteKey.id,
915
- ...(normalizedParticipant ? { participant: jidNormalizedUser(normalizedParticipant) } : {}),
916
- addressingMode: deleteAddressingMode
917
- }
918
- logger.debug({ jid, deleteKey: content.delete }, 'processing message deletion')
919
- }
920
-
921
- // Generate the full WAMessage proto
922
- const fullMsg = await generateWAMessage(jid, content, {
923
- logger,
924
- userJid: jidNormalizedUser(activeSender),
925
- getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
926
- getProfilePicUrl: sock.profilePictureUrl,
927
- getCallLink: sock.createCallLink,
928
- upload: waUploadToServer,
929
- mediaCache: config.mediaCache,
930
- options: config.options,
931
- messageId: generateMessageIDV2(activeSender),
932
- ...options
933
- })
934
-
935
- // Build additional stanza attributes
936
- const additionalAttributes = {}, additionalNodes = []
937
- if (content.delete) {
938
- additionalAttributes.edit = isJidGroup(content.delete.remoteJid) && !content.delete.fromMe ? '8' : '7'
939
- } else if (content.edit) {
940
- additionalAttributes.edit = '1'
941
- } else if (content.pin) {
942
- additionalAttributes.edit = '2'
943
- }
944
- if (content.poll) additionalNodes.push({ tag: 'meta', attrs: { polltype: 'creation' } })
945
- if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
946
-
947
- // Fetch TC token for new DM contacts
948
- if (!isJidGroup(jid) && !content.disappearingMessagesInChat) {
949
- const existingToken = await authState.keys.get('tctoken', [jid])
950
- if (!existingToken[jid]) {
951
- try { await getPrivacyTokens([jid]); logger.debug({ jid }, 'fetched tctoken for new contact') }
952
- catch (err) { logger.warn({ jid, err }, 'failed to fetch tctoken') }
953
- }
954
- }
955
-
956
- await relayMessage(jid, fullMsg.message, {
957
- messageId: fullMsg.key.id,
958
- useCachedGroupMetadata: options.useCachedGroupMetadata,
959
- additionalAttributes,
960
- statusJidList: options.statusJidList,
961
- additionalNodes
962
- })
963
-
964
- if (config.emitOwnEvents) {
965
- process.nextTick(() => processingMutex.mutex(() => upsertMessage(fullMsg, 'append')))
966
- }
967
- return fullMsg
968
- }
906
+ sendMessage,
907
+ // Shorthand wrappers
908
+ sendText: (jid, text, options = {}) => sendMessage(jid, { text, ...options }, options),
909
+ sendImage: (jid, image, caption = '', options = {}) => sendMessage(jid, { image, caption, ...options }, options),
910
+ sendVideo: (jid, video, caption = '', options = {}) => sendMessage(jid, { video, caption, ...options }, options),
911
+ sendDocument: (jid, document, caption = '', options = {}) => sendMessage(jid, { document, caption, ...options }, options),
912
+ sendAudio: (jid, audio, options = {}) => sendMessage(jid, { audio, ...options }, options),
913
+ sendLocation: (jid, { degreesLatitude, degreesLongitude, name, url, address } = {}, options = {}) =>
914
+ sendMessage(jid, { location: { degreesLatitude, degreesLongitude, name, url, address }, ...options }, options),
915
+ sendPoll: (jid, name, pollVote = [], multiSelect = false, options = {}) =>
916
+ sendMessage(jid, { poll: { name, values: pollVote, selectableOptionsCount: multiSelect ? pollVote.length : 0 }, ...options }, options),
917
+ sendReaction: (jid, key, reaction, options = {}) => sendMessage(jid, { react: { text: reaction, key }, ...options }, options),
918
+ sendSticker: (jid, sticker, options = {}) => sendMessage(jid, { sticker, ...options }, options),
919
+ sendContact: (jid, contact, options = {}) => sendMessage(jid, { contacts: { contacts: Array.isArray(contact) ? contact : [contact] }, ...options }, options),
920
+ sendForward: (jid, message, options = {}) => sendMessage(jid, { forward: message, force: options.force }, options),
969
921
  }
970
922
  }