@nexustechpro/baileys 2.0.2 → 2.0.5

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 (102) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +924 -1299
  3. package/lib/Defaults/baileys-version.json +6 -2
  4. package/lib/Defaults/index.js +172 -172
  5. package/lib/Signal/libsignal.js +380 -292
  6. package/lib/Signal/lid-mapping.js +264 -171
  7. package/lib/Socket/Client/index.js +2 -2
  8. package/lib/Socket/Client/types.js +10 -10
  9. package/lib/Socket/Client/websocket.js +45 -310
  10. package/lib/Socket/business.js +375 -375
  11. package/lib/Socket/chats.js +909 -963
  12. package/lib/Socket/communities.js +430 -430
  13. package/lib/Socket/groups.js +342 -342
  14. package/lib/Socket/index.js +22 -22
  15. package/lib/Socket/messages-recv.js +777 -743
  16. package/lib/Socket/messages-send.js +295 -305
  17. package/lib/Socket/mex.js +50 -50
  18. package/lib/Socket/newsletter.js +148 -148
  19. package/lib/Socket/nexus-handler.js +75 -261
  20. package/lib/Socket/socket.js +709 -1201
  21. package/lib/Store/index.js +5 -5
  22. package/lib/Store/make-cache-manager-store.js +81 -81
  23. package/lib/Store/make-in-memory-store.js +416 -416
  24. package/lib/Store/make-ordered-dictionary.js +81 -81
  25. package/lib/Store/object-repository.js +30 -30
  26. package/lib/Types/Auth.js +1 -1
  27. package/lib/Types/Bussines.js +1 -1
  28. package/lib/Types/Call.js +1 -1
  29. package/lib/Types/Chat.js +7 -7
  30. package/lib/Types/Contact.js +1 -1
  31. package/lib/Types/Events.js +1 -1
  32. package/lib/Types/GroupMetadata.js +1 -1
  33. package/lib/Types/Label.js +24 -24
  34. package/lib/Types/LabelAssociation.js +6 -6
  35. package/lib/Types/Message.js +10 -10
  36. package/lib/Types/Newsletter.js +28 -28
  37. package/lib/Types/Product.js +1 -1
  38. package/lib/Types/Signal.js +1 -1
  39. package/lib/Types/Socket.js +2 -2
  40. package/lib/Types/State.js +12 -12
  41. package/lib/Types/USync.js +1 -1
  42. package/lib/Types/index.js +25 -25
  43. package/lib/Utils/auth-utils.js +264 -256
  44. package/lib/Utils/baileys-event-stream.js +55 -55
  45. package/lib/Utils/browser-utils.js +27 -27
  46. package/lib/Utils/business.js +228 -230
  47. package/lib/Utils/chat-utils.js +694 -764
  48. package/lib/Utils/crypto.js +109 -135
  49. package/lib/Utils/decode-wa-message.js +310 -314
  50. package/lib/Utils/event-buffer.js +547 -547
  51. package/lib/Utils/generics.js +297 -297
  52. package/lib/Utils/history.js +91 -83
  53. package/lib/Utils/index.js +21 -20
  54. package/lib/Utils/key-store.js +17 -0
  55. package/lib/Utils/link-preview.js +97 -98
  56. package/lib/Utils/logger.js +2 -2
  57. package/lib/Utils/lt-hash.js +47 -47
  58. package/lib/Utils/make-mutex.js +39 -39
  59. package/lib/Utils/message-retry-manager.js +148 -148
  60. package/lib/Utils/messages-media.js +534 -534
  61. package/lib/Utils/messages.js +705 -705
  62. package/lib/Utils/noise-handler.js +255 -255
  63. package/lib/Utils/pre-key-manager.js +105 -105
  64. package/lib/Utils/process-message.js +412 -412
  65. package/lib/Utils/signal.js +160 -158
  66. package/lib/Utils/use-multi-file-auth-state.js +120 -120
  67. package/lib/Utils/validate-connection.js +194 -194
  68. package/lib/WABinary/constants.js +1300 -1300
  69. package/lib/WABinary/decode.js +237 -237
  70. package/lib/WABinary/encode.js +232 -232
  71. package/lib/WABinary/generic-utils.js +252 -211
  72. package/lib/WABinary/index.js +5 -5
  73. package/lib/WABinary/jid-utils.js +279 -95
  74. package/lib/WABinary/types.js +1 -1
  75. package/lib/WAM/BinaryInfo.js +9 -9
  76. package/lib/WAM/constants.js +22852 -22852
  77. package/lib/WAM/encode.js +149 -149
  78. package/lib/WAM/index.js +3 -3
  79. package/lib/WAUSync/Protocols/USyncContactProtocol.js +28 -28
  80. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +53 -53
  81. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -26
  82. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -37
  83. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -50
  84. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -28
  85. package/lib/WAUSync/Protocols/index.js +4 -4
  86. package/lib/WAUSync/USyncQuery.js +93 -93
  87. package/lib/WAUSync/USyncUser.js +22 -22
  88. package/lib/WAUSync/index.js +3 -3
  89. package/lib/index.js +66 -66
  90. package/package.json +171 -144
  91. package/lib/Signal/Group/ciphertext-message.js +0 -12
  92. package/lib/Signal/Group/group-session-builder.js +0 -30
  93. package/lib/Signal/Group/group_cipher.js +0 -100
  94. package/lib/Signal/Group/index.js +0 -12
  95. package/lib/Signal/Group/keyhelper.js +0 -18
  96. package/lib/Signal/Group/sender-chain-key.js +0 -26
  97. package/lib/Signal/Group/sender-key-distribution-message.js +0 -63
  98. package/lib/Signal/Group/sender-key-message.js +0 -66
  99. package/lib/Signal/Group/sender-key-name.js +0 -48
  100. package/lib/Signal/Group/sender-key-record.js +0 -41
  101. package/lib/Signal/Group/sender-key-state.js +0 -84
  102. package/lib/Signal/Group/sender-message-key.js +0 -26
@@ -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 } from '../Utils/link-preview.js'
7
+ import { getUrlInfo, migrateIndexKey } from '../Utils/index.js'
8
8
  import { makeKeyedMutex } from '../Utils/make-mutex.js'
9
9
  import { USyncQuery, USyncUser } from '../WAUSync/index.js'
10
10
  import { makeNewsletterSocket } from './newsletter.js'
@@ -22,10 +22,47 @@ const {
22
22
 
23
23
  const {
24
24
  areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser,
25
- isJidGroup, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET,
25
+ isJidBroadcast, isJidGroup, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET,
26
26
  getBinaryFilteredButtons, STORIES_JID, isJidUser, getButtonArgs, getButtonType
27
27
  } = WABinary
28
28
 
29
+ const TC_TOKEN_BUCKET_DURATION = 604800 // 7 days
30
+ const TC_TOKEN_NUM_BUCKETS = 4 // ~28-day rolling window
31
+
32
+ const isTcTokenExpired = (timestamp) => {
33
+ if (timestamp === null || timestamp === undefined) return true
34
+ const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
35
+ if (isNaN(ts)) return true
36
+ const now = Math.floor(Date.now() / 1000)
37
+ const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION)
38
+ const cutoffBucket = currentBucket - (TC_TOKEN_NUM_BUCKETS - 1)
39
+ return ts < (cutoffBucket * TC_TOKEN_BUCKET_DURATION)
40
+ }
41
+
42
+ const shouldSendNewTcToken = (senderTimestamp) => {
43
+ if (senderTimestamp === undefined) return true
44
+ const now = Math.floor(Date.now() / 1000)
45
+ const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION)
46
+ const senderBucket = Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION)
47
+ return currentBucket > senderBucket
48
+ }
49
+
50
+ const resolveTcTokenJid = async (jid, getLIDForPN) => {
51
+ if (isLidUser(jid)) return jid
52
+ const lid = await getLIDForPN(jid)
53
+ return lid ?? jid
54
+ }
55
+
56
+ const resolveIssuanceJid = async (jid, issueToLid, getLIDForPN, getPNForLID) => {
57
+ if (issueToLid) {
58
+ if (isLidUser(jid)) return jid
59
+ return (await getLIDForPN(jid)) ?? jid
60
+ }
61
+ if (!isLidUser(jid)) return jid
62
+ if (getPNForLID) return (await getPNForLID(jid)) ?? jid
63
+ return jid
64
+ }
65
+
29
66
  export const makeMessagesSocket = (config) => {
30
67
  const {
31
68
  logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview,
@@ -43,13 +80,12 @@ export const makeMessagesSocket = (config) => {
43
80
  const peerSessionsCache = new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
44
81
  const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null
45
82
  const encryptionMutex = makeKeyedMutex()
83
+
84
+ // Prevents duplicate TC token IQ requests from concurrent sends
85
+ const inFlightTcTokenIssuance = new Set()
86
+
46
87
  let mediaConn
47
88
 
48
- // ─────────────────────────────────────────────
49
- // MEDIA CONNECTION
50
- // Fetches and caches the WhatsApp media upload connection.
51
- // Refreshes automatically when TTL expires or forced.
52
- // ─────────────────────────────────────────────
53
89
  const refreshMediaConn = async (forceGet = false) => {
54
90
  const media = await mediaConn
55
91
  if (!media || forceGet || Date.now() - media.fetchDate.getTime() > media.ttl * 1000) {
@@ -68,10 +104,8 @@ export const makeMessagesSocket = (config) => {
68
104
  return mediaConn
69
105
  }
70
106
 
71
- // ─────────────────────────────────────────────
72
- // RECEIPTS
73
- // Sends read receipts and delivery confirmations.
74
- // ─────────────────────────────────────────────
107
+ const waUploadToServer = getWAUploadToServer(config, refreshMediaConn)
108
+
75
109
  const sendReceipt = async (jid, participant, messageIds, type) => {
76
110
  if (!messageIds?.length) throw new Boom('missing ids in receipt')
77
111
  const node = { tag: 'receipt', attrs: { id: messageIds[0] } }
@@ -104,11 +138,6 @@ export const makeMessagesSocket = (config) => {
104
138
  await sendReceipts(keys, privacySettings.readreceipts === 'all' ? 'read' : 'read-self')
105
139
  }
106
140
 
107
- // ─────────────────────────────────────────────
108
- // DEVICE & SESSION MANAGEMENT
109
- // Fetches participant devices via USync and manages
110
- // Signal protocol sessions for encryption.
111
- // ─────────────────────────────────────────────
112
141
  const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
113
142
  const deviceResults = []
114
143
  if (!useCache) logger.debug('not using cache for devices')
@@ -117,7 +146,6 @@ export const makeMessagesSocket = (config) => {
117
146
  const decoded = jidDecode(jid)
118
147
  const user = decoded?.user
119
148
  const device = decoded?.device
120
- // Already has a device number — push directly
121
149
  if (typeof device === 'number' && device >= 0 && user) { deviceResults.push({ user, device, jid }); return null }
122
150
  return { jid: jidNormalizedUser(jid), user }
123
151
  }).filter(Boolean)
@@ -144,7 +172,6 @@ export const makeMessagesSocket = (config) => {
144
172
 
145
173
  if (!toFetch.length) return deviceResults
146
174
 
147
- // Track which JIDs are LID-based so we can encode them correctly
148
175
  const requestedLidUsers = new Set()
149
176
  for (const jid of toFetch) {
150
177
  if (isLidUser(jid) || isHostedLidUser(jid)) {
@@ -153,23 +180,21 @@ export const makeMessagesSocket = (config) => {
153
180
  }
154
181
  }
155
182
 
156
- const query = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
157
- for (const jid of toFetch) query.withUser(new USyncUser().withId(jid))
183
+ const usyncQuery = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
184
+ for (const jid of toFetch) usyncQuery.withUser(new USyncUser().withId(jid))
158
185
 
159
- const result = await sock.executeUSyncQuery(query)
186
+ const result = await sock.executeUSyncQuery(usyncQuery)
160
187
  if (result) {
161
188
  const lidResults = result.list.filter(a => !!a.lid)
162
189
  if (lidResults.length > 0) {
163
190
  logger.trace('Storing LID maps from device call')
164
191
  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')
192
+ try {
193
+ const lids = lidResults.map(a => a.lid)
194
+ if (lids.length) await assertSessions(lids, false)
195
+ } catch (e) {
196
+ logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
197
+ }
173
198
  }
174
199
 
175
200
  const extracted = extractDeviceJids(result?.list, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices)
@@ -187,27 +212,23 @@ export const makeMessagesSocket = (config) => {
187
212
  }
188
213
  }
189
214
 
190
- // Cache results
191
215
  if (userDevicesCache.mset) {
192
216
  await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
193
217
  } else {
194
218
  for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
195
219
  }
196
220
 
197
- // Persist device lists for session migration support (capped at 500 users)
221
+ // Persist device lists for session migration (capped at 500 users)
198
222
  const userDeviceUpdates = {}
199
223
  for (const [userId, devices] of Object.entries(deviceMap)) {
200
224
  if (devices?.length > 0) userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
201
225
  }
202
226
  if (Object.keys(userDeviceUpdates).length > 0) {
203
227
  try {
204
- const existingData = await authState.keys.get('device-list', ['_index'])
205
- const currentBatch = existingData?.['_index'] || {}
228
+ const currentBatch = await migrateIndexKey(authState.keys, 'device-list')
206
229
  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')
230
+ await authState.keys.set({ 'device-list': { 'index': mergedBatch } })
231
+ logger.debug({ userCount: Object.keys(userDeviceUpdates).length, batchSize: Object.keys(mergedBatch).length }, 'stored user device lists')
211
232
  } catch (error) {
212
233
  logger.warn({ error }, 'failed to store user device lists')
213
234
  }
@@ -217,47 +238,26 @@ export const makeMessagesSocket = (config) => {
217
238
  }
218
239
 
219
240
  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
241
+ let didFetchNewSession = false
242
+ let jidsRequiringFetch = []
243
+ if (force) {
244
+ jidsRequiringFetch = jids
245
+ } else {
246
+ // assertSessions
247
+ const sessionBatch = await migrateIndexKey(authState.keys, 'session') // sessions live in index blob, not individual files
248
+ for (const jid of jids) {
249
+ const signalId = signalRepository.jidToSignalProtocolAddress(jid)
250
+ if (!sessionBatch[signalId]) jidsRequiringFetch.push(jid)
232
251
  }
233
- jidsRequiringFetch.push(jid)
234
252
  }
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
253
+ if (jidsRequiringFetch.length) {
254
+ logger.debug({ jidsRequiringFetch }, 'fetching sessions')
255
+ const result = await query({ tag: 'iq', attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET }, content: [{ tag: 'key', attrs: {}, content: jidsRequiringFetch.map(jid => ({ tag: 'user', attrs: { jid } })) }] })
256
+ await parseAndInjectE2ESessions(result, signalRepository)
257
+ didFetchNewSession = true
258
+ }
259
+ return didFetchNewSession
255
260
  }
256
-
257
- // ─────────────────────────────────────────────
258
- // PEER DATA OPERATIONS
259
- // Used for history sync requests and placeholder resends.
260
- // ─────────────────────────────────────────────
261
261
  const sendPeerDataOperationMessage = async (pdoMessage) => {
262
262
  if (!authState.creds.me?.id) throw new Boom('Not authenticated')
263
263
  return await relayMessage(jidNormalizedUser(authState.creds.me.id), {
@@ -265,18 +265,24 @@ export const makeMessagesSocket = (config) => {
265
265
  }, { additionalAttributes: { category: 'peer', push_priority: 'high_force' }, additionalNodes: [{ tag: 'meta', attrs: { appdata: 'default' } }] })
266
266
  }
267
267
 
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
268
+ // Issues our TC token to a contact so they can send us private messages. Fire-and-forget.
269
+ const issuePrivacyTokens = async (jids, timestamp) => {
270
+ const t = (timestamp ?? unixTimestampSeconds()).toString()
271
+ return query({
272
+ tag: 'iq',
273
+ attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
274
+ content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }]
275
+ })
277
276
  }
278
277
 
279
- const parseTCTokens = (result) => {
278
+ // Fetches TC tokens from the server for the given JIDs and stores them locally.
279
+ const getPrivacyTokens = async (jids) => {
280
+ const t = unixTimestampSeconds().toString()
281
+ const result = await query({
282
+ tag: 'iq',
283
+ attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
284
+ content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }]
285
+ })
280
286
  const tokens = {}
281
287
  const tokenList = getBinaryNodeChild(result, 'tokens')
282
288
  if (tokenList) {
@@ -285,25 +291,10 @@ export const makeMessagesSocket = (config) => {
285
291
  if (jid && content) tokens[jid] = { token: content, timestamp: Number(unixTimestampSeconds()) }
286
292
  }
287
293
  }
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
294
  if (Object.keys(tokens).length > 0) await authState.keys.set({ 'tctoken': tokens })
300
295
  return tokens
301
296
  }
302
297
 
303
- // ─────────────────────────────────────────────
304
- // GROUP MEMBER LABEL
305
- // Sets a custom label for a member in a group.
306
- // ─────────────────────────────────────────────
307
298
  const updateMemberLabel = (jid, memberLabel) => {
308
299
  if (!memberLabel || typeof memberLabel !== 'string') throw new Error('Member label must be a non-empty string')
309
300
  if (!isJidGroup(jid)) throw new Error('Member labels can only be set in groups')
@@ -315,20 +306,19 @@ export const makeMessagesSocket = (config) => {
315
306
  }, { additionalNodes: [{ tag: 'meta', attrs: { tag_reason: 'user_update', appdata: 'member_tag' }, content: undefined }] })
316
307
  }
317
308
 
318
- // ─────────────────────────────────────────────
319
- // MESSAGE TYPE DETECTION
320
- // Classifies messages for proper stanza type attribute.
321
- // ─────────────────────────────────────────────
322
309
  const getMessageType = (msg) => {
323
310
  const message = normalizeMessageContent(msg)
311
+ if (!message) return 'text'
324
312
  if (message.pollCreationMessage || message.pollCreationMessageV2 || message.pollCreationMessageV3) return 'poll'
325
- if (message.reactionMessage) return 'reaction'
313
+ if (message.reactionMessage || message.encReactionMessage) return 'reaction'
326
314
  if (message.eventMessage) return 'event'
327
315
  if (getMediaType(message)) return 'media'
328
316
  return 'text'
329
317
  }
330
318
 
331
319
  const getMediaType = (message) => {
320
+ const inner = message.viewOnceMessage?.message || message.viewOnceMessageV2?.message || message.viewOnceMessageV2Extension?.message
321
+ if (inner) return getMediaType(inner)
332
322
  if (message.imageMessage) return 'image'
333
323
  if (message.stickerMessage) return message.stickerMessage.isLottie ? '1p_sticker' : message.stickerMessage.isAvatar ? 'avatar_sticker' : 'sticker'
334
324
  if (message.videoMessage) return message.videoMessage.gifPlayback ? 'gif' : 'video'
@@ -352,11 +342,6 @@ export const makeMessagesSocket = (config) => {
352
342
  if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) return 'url'
353
343
  }
354
344
 
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
345
  const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
361
346
  if (!recipientJids.length) return { nodes: [], shouldIncludeDeviceIdentity: false }
362
347
 
@@ -373,7 +358,7 @@ export const makeMessagesSocket = (config) => {
373
358
  if (!jid) return null
374
359
  let msgToEncrypt = patchedMessage
375
360
 
376
- // Use Device Sent Message (DSM) for own linked devices so they can read the message
361
+ // Use DSM for own linked devices so they can read the message
377
362
  if (dsmMessage) {
378
363
  const { user: targetUser } = jidDecode(jid)
379
364
  const { user: ownPnUser } = jidDecode(meId)
@@ -389,7 +374,7 @@ export const makeMessagesSocket = (config) => {
389
374
  return { tag: 'to', attrs: { jid }, content: [{ tag: 'enc', attrs: { v: '2', type, ...(extraAttrs || {}) }, content: ciphertext }] }
390
375
  })
391
376
  } catch (err) {
392
- logger.error({ jid, err }, 'Failed to encrypt for recipient')
377
+ logger.warn({ jid, err: err?.message || err }, 'Failed to encrypt for recipient — no session, will retry on next interaction')
393
378
  return null
394
379
  }
395
380
  })
@@ -398,17 +383,6 @@ export const makeMessagesSocket = (config) => {
398
383
  return { nodes, shouldIncludeDeviceIdentity }
399
384
  }
400
385
 
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
386
  const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList, quoted } = {}) => {
413
387
  const meId = authState.creds.me.id
414
388
  const meLid = authState.creds.me?.lid
@@ -418,7 +392,6 @@ export const makeMessagesSocket = (config) => {
418
392
  const isLid = server === 'lid'
419
393
  const isNewsletter = server === 'newsletter'
420
394
 
421
- // Choose sender identity (PN or LID) based on destination
422
395
  let activeSender = meId
423
396
  let groupAddressingMode = 'pn'
424
397
  if (isGroup && !isStatus) {
@@ -473,7 +446,6 @@ export const makeMessagesSocket = (config) => {
473
446
  const mediaType = getMediaType(message)
474
447
  if (mediaType) extraAttrs.mediatype = mediaType
475
448
 
476
- // ── Newsletter: plaintext encoding, no encryption ──
477
449
  if (isNewsletter) {
478
450
  const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
479
451
  binaryNodeContent.push({ tag: 'plaintext', attrs: {}, content: encodeNewsletterMessage(patched) })
@@ -482,11 +454,10 @@ export const makeMessagesSocket = (config) => {
482
454
  return
483
455
  }
484
456
 
485
- if (messages.pinInChatMessage || messages.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) {
457
+ if (messages?.pinInChatMessage || messages?.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) {
486
458
  extraAttrs['decrypt-fail'] = 'hide'
487
459
  }
488
460
 
489
- // ── Group / Status: sender key (SKDM + skmsg) ──
490
461
  if ((isGroup || isStatus) && !isRetryResend) {
491
462
  const [groupData] = await Promise.all([
492
463
  (async () => {
@@ -495,10 +466,9 @@ export const makeMessagesSocket = (config) => {
495
466
  else if (!isStatus) groupData = await groupMetadata(jid)
496
467
  return groupData
497
468
  })(),
498
- Promise.resolve({}) // senderKeyMap intentionally always empty — forces fresh SKDM every send
469
+ Promise.resolve({}) // senderKeyMap always empty — forces fresh SKDM every send
499
470
  ])
500
471
 
501
- // Build participant list
502
472
  const participantsList = []
503
473
  if (isStatus) {
504
474
  if (statusJidList?.length) participantsList.push(...statusJidList)
@@ -515,9 +485,7 @@ export const makeMessagesSocket = (config) => {
515
485
  const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
516
486
  devices.push(...additionalDevices)
517
487
 
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.
488
+ // Force Device 0 inclusion USync sometimes omits it for LID groups
521
489
  for (const pJid of participantsList) {
522
490
  const decoded = jidDecode(pJid)
523
491
  if (decoded?.user && !devices.some(d => d.user === decoded.user && d.device === 0)) {
@@ -533,7 +501,6 @@ export const makeMessagesSocket = (config) => {
533
501
  const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
534
502
  const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
535
503
 
536
- // Send SKDM to ALL devices every time (senderKeyMap is always {})
537
504
  const senderKeyRecipients = devices
538
505
  .filter(d => !isHostedLidUser(d.jid) && !isHostedPnUser(d.jid) && d.device !== 99)
539
506
  .map(d => d.jid)
@@ -549,7 +516,6 @@ export const makeMessagesSocket = (config) => {
549
516
 
550
517
  binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg', ...extraAttrs }, content: ciphertext })
551
518
 
552
- // ── Group Retry: direct pairwise re-encrypt for specific participant ──
553
519
  } else if ((isGroup || isStatus) && isRetryResend) {
554
520
  const groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
555
521
  if (!groupData && !isStatus) await groupMetadata(jid)
@@ -565,17 +531,19 @@ export const makeMessagesSocket = (config) => {
565
531
  const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
566
532
  const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
567
533
 
568
- // Send fresh SKDM directly to the requesting participant
569
534
  const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
570
535
  await assertSessions([participant.jid])
571
536
  const skResult = await createParticipantNodes([participant.jid], senderKeyMsg, {})
572
537
  shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || skResult.shouldIncludeDeviceIdentity
573
538
  participants.push(...skResult.nodes)
574
539
 
575
- const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({ data: bytes, jid: participant?.jid })
540
+ // For retry resend, encrypt directly to the requesting participant
541
+ const isParticipantLid = isLidUser(participant.jid)
542
+ const isMe = areJidsSameUser(participant.jid, isParticipantLid ? meLid : meId)
543
+ const encodedMsg = isMe ? encodeWAMessage({ deviceSentMessage: { destinationJid, message } }) : encodeWAMessage(message)
544
+ const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({ data: encodedMsg, jid: participant.jid })
576
545
  binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type, count: participant.count.toString() }, content: encryptedContent })
577
546
 
578
- // ── DM / LID: standard pairwise encryption ──
579
547
  } else {
580
548
  let ownId = meId
581
549
  if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
@@ -593,7 +561,6 @@ export const makeMessagesSocket = (config) => {
593
561
  }
594
562
 
595
563
  if (additionalAttributes?.category !== 'peer') {
596
- // Preserve Device 0 entries before USync refetch which may omit them
597
564
  const device0Entries = devices.filter(d => d.device === 0)
598
565
  const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
599
566
  devices.length = 0
@@ -603,7 +570,6 @@ export const makeMessagesSocket = (config) => {
603
570
  const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
604
571
  devices.push(...device0Entries, ...sessionDevices)
605
572
 
606
- // Explicitly fetch sender's linked devices if not returned by USync
607
573
  if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
608
574
  const senderDevices = await getUSyncDevices([senderIdentity], true, false)
609
575
  const senderLinkedDevices = senderDevices.filter(d => d.device !== 0 && d.user === senderOwnUser)
@@ -639,7 +605,6 @@ export const makeMessagesSocket = (config) => {
639
605
  shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
640
606
  }
641
607
 
642
- // ── Build final stanza ──
643
608
  if (participants.length) {
644
609
  if (additionalAttributes?.category === 'peer') {
645
610
  const peerNode = participants[0]?.content?.[0]
@@ -670,41 +635,74 @@ export const makeMessagesSocket = (config) => {
670
635
  stanza.attrs.to = destinationJid
671
636
  }
672
637
 
673
- // Attach button metadata if needed
674
- if (!isNewsletter && buttonType) {
638
+ let didPushAdditional = false
639
+
640
+ if (!isNewsletter && buttonType && !isStatus) {
675
641
  const buttonsNode = getButtonArgs(messages)
676
642
  const filteredButtons = getBinaryFilteredButtons(additionalNodes || [])
677
- if (filteredButtons) { stanza.content.push(...additionalNodes) }
678
- else stanza.content.push(buttonsNode)
679
- } else if (additionalNodes?.length > 0) {
643
+ if (filteredButtons) {
644
+ stanza.content.push(...additionalNodes)
645
+ didPushAdditional = true
646
+ } else {
647
+ stanza.content.push(...buttonsNode)
648
+ }
649
+ }
650
+
651
+ if (!didPushAdditional && additionalNodes?.length > 0) {
680
652
  stanza.content.push(...additionalNodes)
681
653
  }
682
654
 
683
- // Attach device identity for LID groups and pkmsg sessions
684
655
  if ((shouldIncludeDeviceIdentity || (meLid && (isLid || (isGroup && groupAddressingMode === 'lid')))) && !isNewsletter) {
685
656
  stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) })
686
657
  logger.debug({ jid }, 'adding device identity')
687
658
  }
688
659
 
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')
660
+ // TC token handling for 1:1 messages
661
+ const isPeerMessage = additionalAttributes?.category === 'peer'
662
+ const is1on1 = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage
663
+ if (is1on1) {
664
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
665
+ const tcTokenJid = await resolveTcTokenJid(destinationJid, getLIDForPN)
666
+
667
+ const contactTcTokenData = await authState.keys.get('tctoken', [tcTokenJid])
668
+ const existingEntry = contactTcTokenData[tcTokenJid]
669
+ let tcTokenBuffer = existingEntry?.token
670
+
671
+ // Clear expired tokens
672
+ if (tcTokenBuffer?.length && isTcTokenExpired(existingEntry?.timestamp)) {
673
+ logger.debug({ jid: destinationJid }, 'tctoken expired, clearing')
674
+ tcTokenBuffer = undefined
695
675
  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')
700
- }
676
+ await authState.keys.set({ tctoken: { [tcTokenJid]: existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null } })
677
+ } catch { }
678
+ }
679
+
680
+ if (tcTokenBuffer?.length) {
681
+ stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
682
+ }
683
+
684
+ // Fire-and-forget: issue our token to the contact after send
685
+ const isProtocolMsg = !!normalizeMessageContent(message)?.protocolMessage
686
+ if (!isProtocolMsg && shouldSendNewTcToken(existingEntry?.senderTimestamp) && !inFlightTcTokenIssuance.has(tcTokenJid)) {
687
+ inFlightTcTokenIssuance.add(tcTokenJid)
688
+ const issueTimestamp = unixTimestampSeconds()
689
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
690
+ const issueToLid = sock.serverProps?.lidTrustedTokenIssueToLid ?? false
691
+ resolveIssuanceJid(destinationJid, issueToLid, getLIDForPN, getPNForLID)
692
+ .then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
693
+ .then(async () => {
694
+ const currentData = await authState.keys.get('tctoken', [tcTokenJid])
695
+ const current = currentData[tcTokenJid]
696
+ await authState.keys.set({ tctoken: { [tcTokenJid]: { ...current, senderTimestamp: issueTimestamp } } })
697
+ })
698
+ .catch(err => logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed'))
699
+ .finally(() => inFlightTcTokenIssuance.delete(tcTokenJid))
701
700
  }
702
- if (tcTokenBuffer) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
703
701
  }
704
702
 
705
- logger.debug({ msgId }, `sending message to ${participants.length} devices`)
703
+ logger.debug({ msgId: finalMsgId }, `sending message to ${participants.length} devices`)
706
704
  await sendNode(stanza)
707
- if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid, msgId, message)
705
+ if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid, finalMsgId, message)
708
706
 
709
707
  }, activeSender)
710
708
 
@@ -722,21 +720,131 @@ export const makeMessagesSocket = (config) => {
722
720
  }
723
721
  }
724
722
 
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 })
723
+ const nexus = new NexusHandler(Utils, waUploadToServer, relayMessage, { logger, mediaCache: config.mediaCache, options: config.options, mediaUploadTimeoutMs: config.mediaUploadTimeoutMs, user: authState.creds.me, getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }) })
727
724
  const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update')
728
725
 
726
+ const sendMessage = async (jid, content, options = {}) => {
727
+ const meId = authState.creds.me.id
728
+ const meLid = authState.creds.me?.lid
729
+ const { server } = jidDecode(jid)
730
+ const isGroup = server === 'g.us'
731
+ const isDestinationLid = server === 'lid'
732
+ const useCache = options.useCachedGroupMetadata !== false
733
+ const { quoted } = options
734
+
735
+ let activeSender = meId
736
+ let addressingMode = 'pn'
737
+ if (isGroup) {
738
+ const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
739
+ addressingMode = groupData?.addressingMode || 'lid'
740
+ if (addressingMode === 'lid' && meLid) activeSender = meLid
741
+ } else if (isDestinationLid && meLid) {
742
+ activeSender = meLid
743
+ addressingMode = 'lid'
744
+ }
745
+
746
+ // Unwrap shorthand `interactive` key
747
+ if (content.interactive && !content.interactiveMessage) {
748
+ const { interactive, ...rest } = content
749
+ content = { ...rest, interactiveMessage: interactive }
750
+ }
751
+
752
+ const messageType = nexus.detectType(content)
753
+ if (messageType) return await nexus.processMessage(content, jid, quoted)
754
+
755
+ if (content.disappearingMessagesInChat && isJidGroup(jid)) {
756
+ const value = typeof content.disappearingMessagesInChat === 'boolean'
757
+ ? (content.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0)
758
+ : content.disappearingMessagesInChat
759
+ await groupToggleEphemeral(jid, value)
760
+ return
761
+ }
762
+
763
+ if (content.delete) {
764
+ const deleteKey = content.delete
765
+ if (!deleteKey.remoteJid || !deleteKey.id) {
766
+ logger.error({ deleteKey }, 'Invalid delete key: missing remoteJid or id')
767
+ throw new Boom('Delete key must have remoteJid and id', { statusCode: 400 })
768
+ }
769
+
770
+ const { server: deleteServer } = jidDecode(deleteKey.remoteJid)
771
+ let deleteAddressingMode = deleteServer === 'lid' ? 'lid' : 'pn'
772
+ if (isJidGroup(deleteKey.remoteJid)) {
773
+ const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(deleteKey.remoteJid) : undefined
774
+ deleteAddressingMode = groupData?.addressingMode || 'lid'
775
+ }
776
+
777
+ let normalizedParticipant = deleteKey.participant
778
+ if (deleteKey.fromMe || isJidGroup(deleteKey.remoteJid)) {
779
+ const senderJid = (deleteAddressingMode === 'lid' && meLid) ? meLid : meId
780
+ normalizedParticipant = jidNormalizedUser(senderJid)
781
+ }
782
+
783
+ content.delete = {
784
+ remoteJid: deleteKey.remoteJid,
785
+ fromMe: deleteKey.fromMe === true || deleteKey.fromMe === 'true',
786
+ id: deleteKey.id,
787
+ ...(normalizedParticipant ? { participant: jidNormalizedUser(normalizedParticipant) } : {}),
788
+ addressingMode: deleteAddressingMode
789
+ }
790
+ logger.debug({ jid, deleteKey: content.delete }, 'processing message deletion')
791
+ }
792
+
793
+ const fullMsg = await generateWAMessage(jid, content, {
794
+ logger,
795
+ userJid: jidNormalizedUser(activeSender),
796
+ getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
797
+ getProfilePicUrl: sock.profilePictureUrl,
798
+ getCallLink: sock.createCallLink,
799
+ upload: waUploadToServer,
800
+ mediaCache: config.mediaCache,
801
+ options: config.options,
802
+ messageId: generateMessageIDV2(activeSender),
803
+ ...options
804
+ })
805
+
806
+ const additionalAttributes = {}, additionalNodes = []
807
+ if (content.delete) {
808
+ additionalAttributes.edit = isJidGroup(content.delete.remoteJid) ? '8' : '7'
809
+ } else if (content.edit) {
810
+ additionalAttributes.edit = '1'
811
+ } else if (content.pin) {
812
+ additionalAttributes.edit = '2'
813
+ }
814
+ if (content.poll) additionalNodes.push({ tag: 'meta', attrs: { polltype: 'creation' } })
815
+ if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
816
+ if (content.ai) additionalNodes.push({ tag: 'bot', attrs: { biz_bot: '1' } })
817
+
818
+ // Pre-fetch TC token for new DM contacts
819
+ if (!isJidGroup(jid) && !isJidStatusBroadcast(jid) && !isJidBroadcast(jid) && !content.disappearingMessagesInChat) {
820
+ const existingToken = await authState.keys.get('tctoken', [jid])
821
+ if (!existingToken[jid]) {
822
+ try { await getPrivacyTokens([jid]); logger.debug({ jid }, 'fetched tctoken for new contact') }
823
+ catch (err) { logger.warn({ jid, err }, 'failed to fetch tctoken') }
824
+ }
825
+ }
826
+
827
+ await relayMessage(jid, fullMsg.message, {
828
+ messageId: fullMsg.key.id,
829
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
830
+ additionalAttributes,
831
+ statusJidList: options.statusJidList,
832
+ additionalNodes
833
+ })
834
+
835
+ if (config.emitOwnEvents) {
836
+ process.nextTick(() => processingMutex.mutex(() => upsertMessage(fullMsg, 'append')))
837
+ }
838
+ return fullMsg
839
+ }
840
+
729
841
  return {
730
842
  ...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
- // ─────────────────────────────────────────────
843
+ getPrivacyTokens, issuePrivacyTokens, assertSessions, relayMessage,
844
+ sendReceipt, sendReceipts, nexus, readMessages, refreshMediaConn,
845
+ waUploadToServer, fetchPrivacySettings, sendPeerDataOperationMessage,
846
+ createParticipantNodes, getUSyncDevices, messageRetryManager, updateMemberLabel,
847
+
740
848
  updateMediaMessage: async (message) => {
741
849
  const content = assertMediaContent(message.message)
742
850
  const mediaKey = content.mediaKey
@@ -767,11 +875,6 @@ export const makeMessagesSocket = (config) => {
767
875
  return message
768
876
  },
769
877
 
770
- // ─────────────────────────────────────────────
771
- // SEND STATUS MENTIONS
772
- // Broadcasts a status update and notifies mentioned
773
- // users or groups via statusMentionMessage protocol.
774
- // ─────────────────────────────────────────────
775
878
  sendStatusMentions: async (content, jids = []) => {
776
879
  const userJid = jidNormalizedUser(authState.creds.me.id)
777
880
  const allUsers = new Set([userJid])
@@ -825,7 +928,7 @@ export const makeMessagesSocket = (config) => {
825
928
  return msg
826
929
  },
827
930
 
828
- // Nexus handler shortcuts for rich message types
931
+ // Nexus handler shortcuts
829
932
  sendPaymentMessage: (jid, data, quoted) => nexus.handlePayment({ requestPaymentMessage: data }, jid, quoted),
830
933
  sendProductMessage: (jid, data, quoted) => nexus.handleProduct({ productMessage: data }, jid, quoted),
831
934
  sendInteractiveMessage: (jid, data, quoted) => nexus.handleInteractive({ interactiveMessage: data }, jid, quoted),
@@ -838,133 +941,20 @@ export const makeMessagesSocket = (config) => {
838
941
  sendCarouselMessage: (jid, data, quoted) => nexus.handleCarousel({ carouselMessage: data }, jid, quoted),
839
942
  sendCarouselProtoMessage: (jid, data, quoted) => nexus.handleCarouselProto({ carouselProto: data }, jid, quoted),
840
943
  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
- }
944
+ sendMessage,
945
+ // Shorthand wrappers
946
+ sendText: (jid, text, options = {}) => sendMessage(jid, { text, ...options }, options),
947
+ sendImage: (jid, image, caption = '', options = {}) => sendMessage(jid, { image, caption, ...options }, options),
948
+ sendVideo: (jid, video, caption = '', options = {}) => sendMessage(jid, { video, caption, ...options }, options),
949
+ sendDocument: (jid, document, caption = '', options = {}) => sendMessage(jid, { document, caption, ...options }, options),
950
+ sendAudio: (jid, audio, options = {}) => sendMessage(jid, { audio, ...options }, options),
951
+ sendLocation: (jid, { degreesLatitude, degreesLongitude, name, url, address } = {}, options = {}) =>
952
+ sendMessage(jid, { location: { degreesLatitude, degreesLongitude, name, url, address }, ...options }, options),
953
+ sendPoll: (jid, name, pollVote = [], multiSelect = false, options = {}) =>
954
+ sendMessage(jid, { poll: { name, values: pollVote, selectableOptionsCount: multiSelect ? pollVote.length : 0 }, ...options }, options),
955
+ sendReaction: (jid, key, reaction, options = {}) => sendMessage(jid, { react: { text: reaction, key }, ...options }, options),
956
+ sendSticker: (jid, sticker, options = {}) => sendMessage(jid, { sticker, ...options }, options),
957
+ sendContact: (jid, contact, options = {}) => sendMessage(jid, { contacts: { contacts: Array.isArray(contact) ? contact : [contact] }, ...options }, options),
958
+ sendForward: (jid, message, options = {}) => sendMessage(jid, { forward: message, force: options.force }, options),
969
959
  }
970
960
  }