@nexustechpro/baileys 2.0.1 → 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 +667 -393
  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 -88
  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 -532
  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,
@@ -34,15 +71,21 @@ export const makeMessagesSocket = (config) => {
34
71
  } = config
35
72
 
36
73
  const sock = makeNewsletterSocket(config)
37
- const { ev, authState, processingMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral } = sock
74
+ const {
75
+ ev, authState, processingMutex, signalRepository, upsertMessage, query,
76
+ fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral
77
+ } = sock
38
78
 
39
79
  const userDevicesCache = config.userDevicesCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
40
80
  const peerSessionsCache = new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
41
81
  const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null
42
82
  const encryptionMutex = makeKeyedMutex()
83
+
84
+ // Prevents duplicate TC token IQ requests from concurrent sends
85
+ const inFlightTcTokenIssuance = new Set()
86
+
43
87
  let mediaConn
44
88
 
45
- // ===== MEDIA CONNECTION =====
46
89
  const refreshMediaConn = async (forceGet = false) => {
47
90
  const media = await mediaConn
48
91
  if (!media || forceGet || Date.now() - media.fetchDate.getTime() > media.ttl * 1000) {
@@ -51,7 +94,9 @@ export const makeMessagesSocket = (config) => {
51
94
  const mediaConnNode = getBinaryNodeChild(result, 'media_conn')
52
95
  return {
53
96
  hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(({ attrs }) => ({ hostname: attrs.hostname, maxContentLengthBytes: +attrs.maxContentLengthBytes })),
54
- auth: mediaConnNode.attrs.auth, ttl: +mediaConnNode.attrs.ttl, fetchDate: new Date()
97
+ auth: mediaConnNode.attrs.auth,
98
+ ttl: +mediaConnNode.attrs.ttl,
99
+ fetchDate: new Date()
55
100
  }
56
101
  })()
57
102
  logger.debug('fetched media conn')
@@ -59,23 +104,33 @@ export const makeMessagesSocket = (config) => {
59
104
  return mediaConn
60
105
  }
61
106
 
62
- // ===== RECEIPTS =====
107
+ const waUploadToServer = getWAUploadToServer(config, refreshMediaConn)
108
+
63
109
  const sendReceipt = async (jid, participant, messageIds, type) => {
64
110
  if (!messageIds?.length) throw new Boom('missing ids in receipt')
65
111
  const node = { tag: 'receipt', attrs: { id: messageIds[0] } }
66
112
  const isReadReceipt = type === 'read' || type === 'read-self'
67
113
  if (isReadReceipt) node.attrs.t = unixTimestampSeconds().toString()
68
- if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) { node.attrs.recipient = jid; node.attrs.to = participant }
69
- else { node.attrs.to = jid; if (participant) node.attrs.participant = participant }
114
+ if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) {
115
+ node.attrs.recipient = jid
116
+ node.attrs.to = participant
117
+ } else {
118
+ node.attrs.to = jid
119
+ if (participant) node.attrs.participant = participant
120
+ }
70
121
  if (type) node.attrs.type = type
71
- if (messageIds.length > 1) node.content = [{ tag: 'list', attrs: {}, content: messageIds.slice(1).map(id => ({ tag: 'item', attrs: { id } })) }]
122
+ if (messageIds.length > 1) {
123
+ node.content = [{ tag: 'list', attrs: {}, content: messageIds.slice(1).map(id => ({ tag: 'item', attrs: { id } })) }]
124
+ }
72
125
  logger.debug({ attrs: node.attrs, messageIds }, 'sending receipt')
73
126
  await sendNode(node)
74
127
  }
75
128
 
76
129
  const sendReceipts = async (keys, type) => {
77
130
  const recps = aggregateMessageKeysNotFromMe(keys)
78
- for (const { jid, participant, messageIds } of recps) await sendReceipt(jid, participant, messageIds, type)
131
+ for (const { jid, participant, messageIds } of recps) {
132
+ await sendReceipt(jid, participant, messageIds, type)
133
+ }
79
134
  }
80
135
 
81
136
  const readMessages = async (keys) => {
@@ -83,7 +138,6 @@ export const makeMessagesSocket = (config) => {
83
138
  await sendReceipts(keys, privacySettings.readreceipts === 'all' ? 'read' : 'read-self')
84
139
  }
85
140
 
86
- // ===== DEVICES & SESSIONS =====
87
141
  const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
88
142
  const deviceResults = []
89
143
  if (!useCache) logger.debug('not using cache for devices')
@@ -97,95 +151,113 @@ export const makeMessagesSocket = (config) => {
97
151
  }).filter(Boolean)
98
152
 
99
153
  let mgetDevices
100
- if (useCache && userDevicesCache.mget) mgetDevices = await userDevicesCache.mget(jidsWithUser.map(j => j?.user).filter(Boolean))
154
+ if (useCache && userDevicesCache.mget) {
155
+ mgetDevices = await userDevicesCache.mget(jidsWithUser.map(j => j?.user).filter(Boolean))
156
+ }
101
157
 
102
158
  const toFetch = []
103
159
  for (const { jid, user } of jidsWithUser) {
104
160
  if (useCache) {
105
161
  const devices = mgetDevices?.[user] || (userDevicesCache.mget ? undefined : await userDevicesCache.get(user))
106
- if (devices) { deviceResults.push(...devices.map(d => ({ ...d, jid: jidEncode(d.user, d.server, d.device) }))); logger.trace({ user }, 'using cache for devices') }
107
- else toFetch.push(jid)
108
- } else toFetch.push(jid)
162
+ if (devices) {
163
+ deviceResults.push(...devices.map(d => ({ ...d, jid: jidEncode(d.user, d.server, d.device) })))
164
+ logger.trace({ user }, 'using cache for devices')
165
+ } else {
166
+ toFetch.push(jid)
167
+ }
168
+ } else {
169
+ toFetch.push(jid)
170
+ }
109
171
  }
110
172
 
111
173
  if (!toFetch.length) return deviceResults
112
174
 
113
175
  const requestedLidUsers = new Set()
114
- for (const jid of toFetch) if (isLidUser(jid) || isHostedLidUser(jid)) { const user = jidDecode(jid)?.user; if (user) requestedLidUsers.add(user) }
176
+ for (const jid of toFetch) {
177
+ if (isLidUser(jid) || isHostedLidUser(jid)) {
178
+ const user = jidDecode(jid)?.user
179
+ if (user) requestedLidUsers.add(user)
180
+ }
181
+ }
115
182
 
116
- const query = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
117
- 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))
118
185
 
119
- const result = await sock.executeUSyncQuery(query)
186
+ const result = await sock.executeUSyncQuery(usyncQuery)
120
187
  if (result) {
121
188
  const lidResults = result.list.filter(a => !!a.lid)
122
- if (lidResults.length > 0) { logger.trace('Storing LID maps from device call'); await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id }))) }
123
- try {
124
- const lids = lidResults.map(a => a.lid)
125
- // Re-fetch sessions during device lookup to ensure fresh state
126
- if (lids.length) await assertSessions(lids, false)
127
- } catch (e) {
128
- logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
189
+ if (lidResults.length > 0) {
190
+ logger.trace('Storing LID maps from device call')
191
+ await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })))
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
+ }
129
198
  }
130
199
 
131
200
  const extracted = extractDeviceJids(result?.list, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices)
132
201
  const deviceMap = {}
133
- for (const item of extracted) { deviceMap[item.user] = deviceMap[item.user] || []; deviceMap[item.user]?.push(item) }
202
+ for (const item of extracted) {
203
+ deviceMap[item.user] = deviceMap[item.user] || []
204
+ deviceMap[item.user]?.push(item)
205
+ }
134
206
 
135
207
  for (const [user, userDevices] of Object.entries(deviceMap)) {
136
- const isLidUser = requestedLidUsers.has(user)
208
+ const isLid = requestedLidUsers.has(user)
137
209
  for (const item of userDevices) {
138
- const finalJid = isLidUser ? jidEncode(user, item.server, item.device) : jidEncode(item.user, item.server, item.device)
210
+ const finalJid = isLid ? jidEncode(user, item.server, item.device) : jidEncode(item.user, item.server, item.device)
139
211
  deviceResults.push({ ...item, jid: finalJid })
140
212
  }
141
213
  }
142
214
 
143
- if (userDevicesCache.mset) await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
144
- else for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
215
+ if (userDevicesCache.mset) {
216
+ await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
217
+ } else {
218
+ for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
219
+ }
145
220
 
221
+ // Persist device lists for session migration (capped at 500 users)
146
222
  const userDeviceUpdates = {}
147
- for (const [userId, devices] of Object.entries(deviceMap)) if (devices?.length > 0) userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
148
-
223
+ for (const [userId, devices] of Object.entries(deviceMap)) {
224
+ if (devices?.length > 0) userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
225
+ }
149
226
  if (Object.keys(userDeviceUpdates).length > 0) {
150
227
  try {
151
- const existingData = await authState.keys.get('device-list', ['_index'])
152
- const currentBatch = existingData?.['_index'] || {}
228
+ const currentBatch = await migrateIndexKey(authState.keys, 'device-list')
153
229
  const mergedBatch = { ...currentBatch, ...userDeviceUpdates }
154
- const userKeys = Object.keys(mergedBatch).sort()
155
- const trimmedBatch = {}
156
- userKeys.slice(-500).forEach(userId => { trimmedBatch[userId] = mergedBatch[userId] })
157
- await authState.keys.set({ 'device-list': { '_index': trimmedBatch } })
158
- logger.debug({ userCount: Object.keys(userDeviceUpdates).length, batchSize: Object.keys(trimmedBatch).length }, 'stored user device lists')
159
- } catch (error) { logger.warn({ error }, 'failed to store 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')
232
+ } catch (error) {
233
+ logger.warn({ error }, 'failed to store user device lists')
234
+ }
160
235
  }
161
236
  }
162
237
  return deviceResults
163
238
  }
164
239
 
165
240
  const assertSessions = async (jids, force) => {
166
- let didFetchNewSession = false
167
- const uniqueJids = [...new Set(jids)]
168
- const jidsRequiringFetch = []
169
-
170
- for (const jid of uniqueJids) {
171
- const signalId = signalRepository.jidToSignalProtocolAddress(jid)
172
- const cachedSession = peerSessionsCache.get(signalId)
173
- if (cachedSession !== undefined) { if (cachedSession && !force) continue } // ← Add && !force
174
- else { const sessionValidation = await signalRepository.validateSession(jid); peerSessionsCache.set(signalId, sessionValidation.exists); if (sessionValidation.exists && !force) continue } // ← Add && !force
175
- jidsRequiringFetch.push(jid)
176
- }
177
-
178
- if (jidsRequiringFetch.length) {
179
- const wireJids = [...jidsRequiringFetch.filter(jid => isLidUser(jid) || isHostedLidUser(jid)), ...(await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => isPnUser(jid) || isHostedPnUser(jid))) || []).map(a => a.lid)]
180
- logger.debug({ jidsRequiringFetch, wireJids }, 'fetching sessions')
181
- const result = await query({ tag: 'iq', attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET }, content: [{ tag: 'key', attrs: {}, content: wireJids.map(jid => { const attrs = { jid }; if (force) attrs.reason = 'identity'; return { tag: 'user', attrs } }) }] })
182
- await parseAndInjectE2ESessions(result, signalRepository)
183
- didFetchNewSession = true
184
- for (const wireJid of wireJids) peerSessionsCache.set(signalRepository.jidToSignalProtocolAddress(wireJid), true)
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)
251
+ }
252
+ }
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
185
260
  }
186
- return didFetchNewSession
187
- }
188
-
189
261
  const sendPeerDataOperationMessage = async (pdoMessage) => {
190
262
  if (!authState.creds.me?.id) throw new Boom('Not authenticated')
191
263
  return await relayMessage(jidNormalizedUser(authState.creds.me.id), {
@@ -193,72 +265,60 @@ export const makeMessagesSocket = (config) => {
193
265
  }, { additionalAttributes: { category: 'peer', push_priority: 'high_force' }, additionalNodes: [{ tag: 'meta', attrs: { appdata: 'default' } }] })
194
266
  }
195
267
 
196
- const parseTCTokens = (result) => {
197
- const tokens = {}
198
- const tokenList = getBinaryNodeChild(result, 'tokens')
199
- if (tokenList) {
200
- const tokenNodes = getBinaryNodeChildren(tokenList, 'token')
201
- for (const node of tokenNodes) {
202
- const jid = node.attrs.jid
203
- const token = node.content
204
- if (jid && token) tokens[jid] = { token, timestamp: Number(unixTimestampSeconds()) }
205
- }
206
- }
207
- return tokens
208
- }
209
- const updateMemberLabel = (jid, memberLabel) => {
210
- if (!memberLabel || typeof memberLabel !== 'string') throw new Error('Member label must be a non-empty string')
211
- if (!isJidGroup(jid)) throw new Error('Member labels can only be set in groups')
212
- return relayMessage(jid, { protocolMessage: { type: proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE, memberLabel: { label: memberLabel.slice(0, 30), labelTimestamp: unixTimestampSeconds() } } }, { additionalNodes: [{ tag: 'meta', attrs: { tag_reason: 'user_update', appdata: 'member_tag' }, content: undefined }] })
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
+ })
213
276
  }
214
277
 
215
- const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
216
- if (!recipientJids.length) return { nodes: [], shouldIncludeDeviceIdentity: false }
217
- const patched = await patchMessageBeforeSending(message, recipientJids)
218
- const patchedMessages = Array.isArray(patched) ? patched : recipientJids.map(jid => ({ recipientJid: jid, message: patched }))
219
- let shouldIncludeDeviceIdentity = false
220
- const meId = authState.creds.me.id
221
- const meLid = authState.creds.me?.lid
222
- const meLidUser = meLid ? jidDecode(meLid)?.user : null
223
-
224
- const encryptionPromises = patchedMessages.map(async ({ recipientJid: jid, message: patchedMessage }) => {
225
- try {
226
- if (!jid) return null
227
- let msgToEncrypt = patchedMessage
228
- if (dsmMessage) {
229
- const { user: targetUser } = jidDecode(jid)
230
- const { user: ownPnUser } = jidDecode(meId)
231
- const isOwnUser = targetUser === ownPnUser || (meLidUser && targetUser === meLidUser)
232
- const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
233
- if (isOwnUser && !isExactSenderDevice) { msgToEncrypt = dsmMessage; logger.debug({ jid, targetUser }, 'Using DSM for own device') }
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
+ })
286
+ const tokens = {}
287
+ const tokenList = getBinaryNodeChild(result, 'tokens')
288
+ if (tokenList) {
289
+ for (const node of getBinaryNodeChildren(tokenList, 'token')) {
290
+ const { jid, content } = { jid: node.attrs.jid, content: node.content }
291
+ if (jid && content) tokens[jid] = { token: content, timestamp: Number(unixTimestampSeconds()) }
234
292
  }
235
- const bytes = encodeWAMessage(msgToEncrypt)
236
- return await encryptionMutex.mutex(jid, async () => {
237
- const { type, ciphertext } = await signalRepository.encryptMessage({ jid, data: bytes })
238
- if (type === 'pkmsg') shouldIncludeDeviceIdentity = true
239
- return { tag: 'to', attrs: { jid }, content: [{ tag: 'enc', attrs: { v: '2', type, ...(extraAttrs || {}) }, content: ciphertext }] }
240
- })
241
- } catch (err) {
242
- logger.error({jid, err }, 'Failed to encrypt for recipient')
243
- return null
244
293
  }
245
- })
294
+ if (Object.keys(tokens).length > 0) await authState.keys.set({ 'tctoken': tokens })
295
+ return tokens
296
+ }
246
297
 
247
- const nodes = (await Promise.all(encryptionPromises)).filter(Boolean)
248
- return { nodes, shouldIncludeDeviceIdentity }
298
+ const updateMemberLabel = (jid, memberLabel) => {
299
+ if (!memberLabel || typeof memberLabel !== 'string') throw new Error('Member label must be a non-empty string')
300
+ if (!isJidGroup(jid)) throw new Error('Member labels can only be set in groups')
301
+ return relayMessage(jid, {
302
+ protocolMessage: {
303
+ type: proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE,
304
+ memberLabel: { label: memberLabel.slice(0, 30), labelTimestamp: unixTimestampSeconds() }
305
+ }
306
+ }, { additionalNodes: [{ tag: 'meta', attrs: { tag_reason: 'user_update', appdata: 'member_tag' }, content: undefined }] })
249
307
  }
250
308
 
251
- // ===== MESSAGE HELPERS =====
252
309
  const getMessageType = (msg) => {
253
310
  const message = normalizeMessageContent(msg)
311
+ if (!message) return 'text'
254
312
  if (message.pollCreationMessage || message.pollCreationMessageV2 || message.pollCreationMessageV3) return 'poll'
255
- if (message.reactionMessage) return 'reaction'
313
+ if (message.reactionMessage || message.encReactionMessage) return 'reaction'
256
314
  if (message.eventMessage) return 'event'
257
315
  if (getMediaType(message)) return 'media'
258
316
  return 'text'
259
317
  }
260
318
 
261
319
  const getMediaType = (message) => {
320
+ const inner = message.viewOnceMessage?.message || message.viewOnceMessageV2?.message || message.viewOnceMessageV2Extension?.message
321
+ if (inner) return getMediaType(inner)
262
322
  if (message.imageMessage) return 'image'
263
323
  if (message.stickerMessage) return message.stickerMessage.isLottie ? '1p_sticker' : message.stickerMessage.isAvatar ? 'avatar_sticker' : 'sticker'
264
324
  if (message.videoMessage) return message.videoMessage.gifPlayback ? 'gif' : 'video'
@@ -282,275 +342,507 @@ export const makeMessagesSocket = (config) => {
282
342
  if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) return 'url'
283
343
  }
284
344
 
285
- // ===== RELAY MESSAGE =====
286
- const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList, quoted } = {}) => {
287
- const meId = authState.creds.me.id
288
- const meLid = authState.creds.me?.lid
289
- const isRetryResend = Boolean(participant?.jid)
290
- let shouldIncludeDeviceIdentity = isRetryResend
291
- let finalMsgId = msgId
292
-
293
- // Check if message is already in proper WAMessage format
294
- const hasProtoMessageType = Object.keys(message).some(key => key.endsWith('Message') || key === 'conversation')
295
-
296
- if (!hasProtoMessageType) {
297
- logger.debug({ jid }, 'relayMessage: auto-generating message from raw content')
298
- const generatedMsg = await generateWAMessage(jid, message, { logger, userJid: meId, getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }), getProfilePicUrl: sock.profilePictureUrl, getCallLink: sock.createCallLink, upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options, messageId: finalMsgId || generateMessageIDV2(meId), quoted: quoted })
299
- message = generatedMsg.message
300
- if (!finalMsgId) finalMsgId = generatedMsg.key.id
301
- logger.debug({ msgId: finalMsgId, jid }, 'message auto-generated successfully')
302
- }
345
+ const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
346
+ if (!recipientJids.length) return { nodes: [], shouldIncludeDeviceIdentity: false }
303
347
 
304
- const { user, server } = jidDecode(jid)
305
- const isGroup = server === 'g.us'
306
- const isStatus = jid === 'status@broadcast'
307
- const isLid = server === 'lid'
308
- const isNewsletter = server === 'newsletter'
309
-
310
- finalMsgId = finalMsgId || generateMessageIDV2(meId)
311
- useUserDevicesCache = useUserDevicesCache !== false
312
- useCachedGroupMetadata = useCachedGroupMetadata !== false && !isStatus
313
-
314
- const participants = []
315
- const destinationJid = !isStatus ? jid : 'status@broadcast'
316
- const binaryNodeContent = []
317
- const devices = []
318
-
319
- const meMsg = { deviceSentMessage: { destinationJid, message }, messageContextInfo: message.messageContextInfo }
320
- const extraAttrs = {}
321
- const messages = normalizeMessageContent(message)
322
- const buttonType = getButtonType(messages)
323
-
324
- if (participant) {
325
- if (!isGroup && !isStatus) additionalAttributes = { ...additionalAttributes, device_fanout: 'false' }
326
- const { user, device } = jidDecode(participant.jid)
327
- devices.push({ user, device, jid: participant.jid })
348
+ const patched = await patchMessageBeforeSending(message, recipientJids)
349
+ const patchedMessages = Array.isArray(patched) ? patched : recipientJids.map(jid => ({ recipientJid: jid, message: patched }))
350
+ let shouldIncludeDeviceIdentity = false
351
+
352
+ const meId = authState.creds.me.id
353
+ const meLid = authState.creds.me?.lid
354
+ const meLidUser = meLid ? jidDecode(meLid)?.user : null
355
+
356
+ const encryptionPromises = patchedMessages.map(async ({ recipientJid: jid, message: patchedMessage }) => {
357
+ try {
358
+ if (!jid) return null
359
+ let msgToEncrypt = patchedMessage
360
+
361
+ // Use DSM for own linked devices so they can read the message
362
+ if (dsmMessage) {
363
+ const { user: targetUser } = jidDecode(jid)
364
+ const { user: ownPnUser } = jidDecode(meId)
365
+ const isOwnUser = targetUser === ownPnUser || (meLidUser && targetUser === meLidUser)
366
+ const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
367
+ if (isOwnUser && !isExactSenderDevice) { msgToEncrypt = dsmMessage; logger.debug({ jid, targetUser }, 'Using DSM for own device') }
368
+ }
369
+
370
+ const bytes = encodeWAMessage(msgToEncrypt)
371
+ return await encryptionMutex.mutex(jid, async () => {
372
+ const { type, ciphertext } = await signalRepository.encryptMessage({ jid, data: bytes })
373
+ if (type === 'pkmsg') shouldIncludeDeviceIdentity = true
374
+ return { tag: 'to', attrs: { jid }, content: [{ tag: 'enc', attrs: { v: '2', type, ...(extraAttrs || {}) }, content: ciphertext }] }
375
+ })
376
+ } catch (err) {
377
+ logger.warn({ jid, err: err?.message || err }, 'Failed to encrypt for recipient — no session, will retry on next interaction')
378
+ return null
379
+ }
380
+ })
381
+
382
+ const nodes = (await Promise.all(encryptionPromises)).filter(Boolean)
383
+ return { nodes, shouldIncludeDeviceIdentity }
328
384
  }
329
385
 
330
- await authState.keys.transaction(async () => {
331
- const mediaType = getMediaType(message)
332
- if (mediaType) extraAttrs.mediatype = mediaType
386
+ const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList, quoted } = {}) => {
387
+ const meId = authState.creds.me.id
388
+ const meLid = authState.creds.me?.lid
389
+ const { user, server } = jidDecode(jid)
390
+ const isGroup = server === 'g.us'
391
+ const isStatus = jid === 'status@broadcast'
392
+ const isLid = server === 'lid'
393
+ const isNewsletter = server === 'newsletter'
394
+
395
+ let activeSender = meId
396
+ let groupAddressingMode = 'pn'
397
+ if (isGroup && !isStatus) {
398
+ const groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
399
+ groupAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
400
+ if (groupAddressingMode === 'lid' && meLid) activeSender = meLid
401
+ } else if (isLid && meLid) {
402
+ activeSender = meLid
403
+ }
333
404
 
334
- if (isNewsletter) {
335
- const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
336
- const bytes = encodeNewsletterMessage(patched)
337
- binaryNodeContent.push({ tag: 'plaintext', attrs: {}, content: bytes })
338
- const stanza = { tag: 'message', attrs: { to: jid, id: finalMsgId, type: getMessageType(message), ...(additionalAttributes || {}) }, content: binaryNodeContent }
339
- logger.debug({ msgId: finalMsgId }, `sending newsletter message to ${jid}`)
340
- await sendNode(stanza)
341
- return
405
+ const isRetryResend = Boolean(participant?.jid)
406
+ let shouldIncludeDeviceIdentity = isRetryResend
407
+ let finalMsgId = msgId
408
+
409
+ // Auto-generate WAMessage from raw content if needed
410
+ const hasProtoMessageType = Object.keys(message).some(key => key.endsWith('Message') || key === 'conversation')
411
+ if (!hasProtoMessageType) {
412
+ logger.debug({ jid }, 'relayMessage: auto-generating message from raw content')
413
+ const generatedMsg = await generateWAMessage(jid, message, {
414
+ logger, userJid: jidNormalizedUser(activeSender),
415
+ getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
416
+ getProfilePicUrl: sock.profilePictureUrl, getCallLink: sock.createCallLink,
417
+ upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options,
418
+ messageId: finalMsgId || generateMessageIDV2(activeSender), quoted
419
+ })
420
+ message = generatedMsg.message
421
+ if (!finalMsgId) finalMsgId = generatedMsg.key.id
422
+ logger.debug({ msgId: finalMsgId, jid }, 'message auto-generated successfully')
423
+ }
424
+
425
+ finalMsgId = finalMsgId || generateMessageIDV2(activeSender)
426
+ useUserDevicesCache = useUserDevicesCache !== false
427
+ useCachedGroupMetadata = useCachedGroupMetadata !== false && !isStatus
428
+
429
+ const participants = []
430
+ const destinationJid = !isStatus ? jid : 'status@broadcast'
431
+ const binaryNodeContent = []
432
+ const devices = []
433
+ const meMsg = { deviceSentMessage: { destinationJid, message }, messageContextInfo: message.messageContextInfo }
434
+ const extraAttrs = {}
435
+ const messages = normalizeMessageContent(message)
436
+ const buttonType = getButtonType(messages)
437
+
438
+ let hasDeviceFanoutFalse = false
439
+ if (participant) {
440
+ if (!isGroup && !isStatus) hasDeviceFanoutFalse = true
441
+ const { user, device } = jidDecode(participant.jid)
442
+ devices.push({ user, device, jid: participant.jid })
342
443
  }
343
444
 
344
- if (messages.pinInChatMessage || messages.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) extraAttrs['decrypt-fail'] = 'hide'
445
+ await authState.keys.transaction(async () => {
446
+ const mediaType = getMediaType(message)
447
+ if (mediaType) extraAttrs.mediatype = mediaType
345
448
 
346
- if (isGroup || isStatus) {
347
- const [groupData, senderKeyMap] = await Promise.all([
348
- (async () => {
349
- let groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
350
- if (groupData?.participants) logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata')
351
- else if (!isStatus) groupData = await groupMetadata(jid)
352
- return groupData
353
- })(),
354
- (async () => !participant && !isStatus ? (await authState.keys.get('sender-key-memory', [jid]))[jid] || {} : {})()
355
- ])
449
+ if (isNewsletter) {
450
+ const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
451
+ binaryNodeContent.push({ tag: 'plaintext', attrs: {}, content: encodeNewsletterMessage(patched) })
452
+ await sendNode({ tag: 'message', attrs: { to: jid, id: finalMsgId, type: getMessageType(message), ...(additionalAttributes || {}) }, content: binaryNodeContent })
453
+ logger.debug({ msgId: finalMsgId }, `sending newsletter message to ${jid}`)
454
+ return
455
+ }
456
+
457
+ if (messages?.pinInChatMessage || messages?.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) {
458
+ extraAttrs['decrypt-fail'] = 'hide'
459
+ }
460
+
461
+ if ((isGroup || isStatus) && !isRetryResend) {
462
+ const [groupData] = await Promise.all([
463
+ (async () => {
464
+ let groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
465
+ if (groupData?.participants) logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata')
466
+ else if (!isStatus) groupData = await groupMetadata(jid)
467
+ return groupData
468
+ })(),
469
+ Promise.resolve({}) // senderKeyMap always empty — forces fresh SKDM every send
470
+ ])
356
471
 
357
- if (!participant) {
358
472
  const participantsList = []
359
- if (isStatus) { if (statusJidList?.length) participantsList.push(...statusJidList) }
360
- else {
473
+ if (isStatus) {
474
+ if (statusJidList?.length) participantsList.push(...statusJidList)
475
+ } else {
361
476
  let groupAddressingMode = 'lid'
362
477
  if (groupData) { participantsList.push(...groupData.participants.map(p => p.id)); groupAddressingMode = groupData?.addressingMode || groupAddressingMode }
363
478
  additionalAttributes = { ...additionalAttributes, addressing_mode: groupAddressingMode }
364
479
  }
365
-
366
- // DEVICE 0 PRESERVATION FOR GROUPS: Initialize device 0 for all participants
367
- const device0EntriesGroup = []
368
- for (const jid of participantsList) {
369
- const { user, server } = jidDecode(jid)
370
- if (user) {
371
- device0EntriesGroup.push({ user, device: 0, jid: jidEncode(user, server, 0) })
372
- }
480
+
481
+ if (groupData?.ephemeralDuration > 0) {
482
+ additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
373
483
  }
374
-
484
+
375
485
  const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
376
- // Combine device 0 entries with fetched devices, avoiding duplicates
377
- const deviceMap = new Map()
378
- for (const d of device0EntriesGroup) deviceMap.set(`${d.user}:${d.device}`, d)
379
- for (const d of additionalDevices) {
380
- const key = `${d.user}:${d.device}`
381
- if (!deviceMap.has(key)) deviceMap.set(key, d)
486
+ devices.push(...additionalDevices)
487
+
488
+ // Force Device 0 inclusion USync sometimes omits it for LID groups
489
+ for (const pJid of participantsList) {
490
+ const decoded = jidDecode(pJid)
491
+ if (decoded?.user && !devices.some(d => d.user === decoded.user && d.device === 0)) {
492
+ devices.push({ user: decoded.user, device: 0, server: decoded.server, domainType: decoded.domainType, jid: jidEncode(decoded.user, decoded.server, 0) })
493
+ }
494
+ }
495
+
496
+ const patched = await patchMessageBeforeSending(message)
497
+ if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
498
+
499
+ const bytes = encodeWAMessage(patched)
500
+ const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
501
+ const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
502
+ const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
503
+
504
+ const senderKeyRecipients = devices
505
+ .filter(d => !isHostedLidUser(d.jid) && !isHostedPnUser(d.jid) && d.device !== 99)
506
+ .map(d => d.jid)
507
+
508
+ if (senderKeyRecipients.length) {
509
+ logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending sender key')
510
+ const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
511
+ await assertSessions(senderKeyRecipients)
512
+ const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, {})
513
+ shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
514
+ participants.push(...result.nodes)
382
515
  }
383
- devices.push(...Array.from(deviceMap.values()))
384
- }
385
516
 
386
- if (groupData?.ephemeralDuration > 0) additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
517
+ binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg', ...extraAttrs }, content: ciphertext })
387
518
 
388
- const patched = await patchMessageBeforeSending(message)
389
- if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
519
+ } else if ((isGroup || isStatus) && isRetryResend) {
520
+ const groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
521
+ if (!groupData && !isStatus) await groupMetadata(jid)
390
522
 
391
- const bytes = encodeWAMessage(patched)
392
- const groupAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
393
- const groupSenderIdentity = groupAddressingMode === 'lid' && meLid ? meLid : meId
523
+ if (groupData?.ephemeralDuration > 0) additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
524
+ additionalAttributes = { ...additionalAttributes, addressing_mode: groupData?.addressingMode || 'lid' }
394
525
 
395
- const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
526
+ const patched = await patchMessageBeforeSending(message)
527
+ if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
396
528
 
397
- const senderKeyRecipients = []
398
- for (const device of devices) {
399
- const deviceJid = device.jid
400
- const hasKey = !!senderKeyMap[deviceJid]
401
- if ((!hasKey || !!participant) && !isHostedLidUser(deviceJid) && !isHostedPnUser(deviceJid) && device.device !== 99) { senderKeyRecipients.push(deviceJid); senderKeyMap[deviceJid] = true }
402
- }
529
+ const bytes = encodeWAMessage(patched)
530
+ const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
531
+ const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
532
+ const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
403
533
 
404
- // Assert sessions once for sender key recipients ONLY to avoid concurrent conflicts
405
- if (senderKeyRecipients.length) {
406
- logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending sender key')
407
534
  const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
408
- await assertSessions(senderKeyRecipients)
409
- const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, extraAttrs)
410
- shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
411
- participants.push(...result.nodes)
535
+ await assertSessions([participant.jid])
536
+ const skResult = await createParticipantNodes([participant.jid], senderKeyMsg, {})
537
+ shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || skResult.shouldIncludeDeviceIdentity
538
+ participants.push(...skResult.nodes)
539
+
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 })
545
+ binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type, count: participant.count.toString() }, content: encryptedContent })
546
+
547
+ } else {
548
+ let ownId = meId
549
+ if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
550
+
551
+ const { user: ownUser } = jidDecode(ownId)
552
+
553
+ if (!participant) {
554
+ const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
555
+ devices.push({ user, device: 0, jid: jidEncode(user, targetUserServer, 0) })
556
+
557
+ if (user !== ownUser) {
558
+ const ownUserServer = isLid ? 'lid' : 's.whatsapp.net'
559
+ const ownUserForAddressing = isLid && meLid ? jidDecode(meLid).user : jidDecode(meId).user
560
+ devices.push({ user: ownUserForAddressing, device: 0, jid: jidEncode(ownUserForAddressing, ownUserServer, 0) })
561
+ }
562
+
563
+ if (additionalAttributes?.category !== 'peer') {
564
+ const device0Entries = devices.filter(d => d.device === 0)
565
+ const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
566
+ devices.length = 0
567
+ const senderIdentity = isLid && meLid
568
+ ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined)
569
+ : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
570
+ const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
571
+ devices.push(...device0Entries, ...sessionDevices)
572
+
573
+ if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
574
+ const senderDevices = await getUSyncDevices([senderIdentity], true, false)
575
+ const senderLinkedDevices = senderDevices.filter(d => d.device !== 0 && d.user === senderOwnUser)
576
+ if (senderLinkedDevices.length > 0) devices.push(...senderLinkedDevices)
577
+ }
578
+ }
579
+ }
580
+
581
+ const allRecipients = [], meRecipients = [], otherRecipients = []
582
+ const { user: mePnUser } = jidDecode(meId)
583
+ const { user: meLidUser } = meLid ? jidDecode(meLid) : { user: null }
584
+
585
+ for (const { user, jid } of devices) {
586
+ const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
587
+ if (isExactSenderDevice) continue
588
+ const isMe = user === mePnUser || user === meLidUser
589
+ if (isMe) meRecipients.push(jid)
590
+ else otherRecipients.push(jid)
591
+ allRecipients.push(jid)
592
+ }
593
+
594
+ await assertSessions(allRecipients)
595
+
596
+ const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
597
+ createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
598
+ createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
599
+ ])
600
+
601
+ participants.push(...meNodes, ...otherNodes)
602
+ if (meRecipients.length > 0 || otherRecipients.length > 0) {
603
+ extraAttrs.phash = generateParticipantHashV2([...meRecipients, ...otherRecipients])
604
+ }
605
+ shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
412
606
  }
413
607
 
414
- if (isRetryResend) {
415
- const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({ data: bytes, jid: participant?.jid })
416
- binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type, count: participant.count.toString() }, content: encryptedContent })
608
+ if (participants.length) {
609
+ if (additionalAttributes?.category === 'peer') {
610
+ const peerNode = participants[0]?.content?.[0]
611
+ if (peerNode) binaryNodeContent.push(peerNode)
612
+ } else {
613
+ binaryNodeContent.push({ tag: 'participants', attrs: {}, content: participants })
614
+ }
615
+ }
616
+
617
+ const stanza = {
618
+ tag: 'message',
619
+ attrs: {
620
+ id: finalMsgId,
621
+ to: destinationJid,
622
+ type: getMessageType(message),
623
+ ...(isLid || (isGroup && groupAddressingMode === 'lid') ? { addressing_mode: 'lid' } : {}),
624
+ ...(hasDeviceFanoutFalse ? { device_fanout: 'false' } : {}),
625
+ ...(additionalAttributes || {})
626
+ },
627
+ content: binaryNodeContent
628
+ }
629
+
630
+ if (participant) {
631
+ if (isJidGroup(destinationJid)) { stanza.attrs.to = destinationJid; stanza.attrs.participant = participant.jid }
632
+ else if (areJidsSameUser(participant.jid, meId)) { stanza.attrs.to = participant.jid; stanza.attrs.recipient = destinationJid }
633
+ else stanza.attrs.to = participant.jid
417
634
  } else {
418
- binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg', ...extraAttrs }, content: ciphertext })
419
- await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } })
635
+ stanza.attrs.to = destinationJid
636
+ }
637
+
638
+ let didPushAdditional = false
639
+
640
+ if (!isNewsletter && buttonType && !isStatus) {
641
+ const buttonsNode = getButtonArgs(messages)
642
+ const filteredButtons = getBinaryFilteredButtons(additionalNodes || [])
643
+ if (filteredButtons) {
644
+ stanza.content.push(...additionalNodes)
645
+ didPushAdditional = true
646
+ } else {
647
+ stanza.content.push(...buttonsNode)
648
+ }
420
649
  }
421
- } else {
422
- let ownId = meId
423
- if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
424
650
 
425
- const { user: ownUser } = jidDecode(ownId)
651
+ if (!didPushAdditional && additionalNodes?.length > 0) {
652
+ stanza.content.push(...additionalNodes)
653
+ }
426
654
 
427
- if (!participant) {
428
- const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
429
- devices.push({ user, device: 0, jid: jidEncode(user, targetUserServer, 0) })
655
+ if ((shouldIncludeDeviceIdentity || (meLid && (isLid || (isGroup && groupAddressingMode === 'lid')))) && !isNewsletter) {
656
+ stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) })
657
+ logger.debug({ jid }, 'adding device identity')
658
+ }
430
659
 
431
- if (user !== ownUser) {
432
- const ownUserServer = isLid ? 'lid' : 's.whatsapp.net'
433
- const ownUserForAddressing = isLid && meLid ? jidDecode(meLid).user : jidDecode(meId).user
434
- devices.push({ user: ownUserForAddressing, device: 0, jid: jidEncode(ownUserForAddressing, ownUserServer, 0) })
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
675
+ try {
676
+ await authState.keys.set({ tctoken: { [tcTokenJid]: existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null } })
677
+ } catch { }
435
678
  }
436
679
 
437
- if (additionalAttributes?.category !== 'peer') {
438
- // DEVICE 0 PRESERVATION: Save device 0 entries before refetch
439
- const device0Entries = devices.filter(d => d.device === 0)
440
- const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
441
- devices.length = 0
442
- const senderIdentity = isLid && meLid ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined) : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
443
- // Fetch both sender and recipient devices to ensure complete enumeration
444
- const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
445
- devices.push(...device0Entries, ...sessionDevices)
446
- // If sender devices weren't enumerated, explicitly fetch them
447
- if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
448
- const senderDevices = await getUSyncDevices([senderIdentity], true, false)
449
- const senderLinkedDevices = senderDevices.filter(d => d.device !== 0 && d.user === senderOwnUser)
450
- if (senderLinkedDevices.length > 0) devices.push(...senderLinkedDevices)
451
- }
680
+ if (tcTokenBuffer?.length) {
681
+ stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
452
682
  }
453
- }
454
683
 
455
- const allRecipients = [], meRecipients = [], otherRecipients = []
456
- const { user: mePnUser } = jidDecode(meId)
457
- const { user: meLidUser } = meLid ? jidDecode(meLid) : { user: null }
458
-
459
- for (const { user, jid } of devices) {
460
- const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
461
- if (isExactSenderDevice) continue
462
- const isMe = user === mePnUser || user === meLidUser
463
- if (isMe) meRecipients.push(jid)
464
- else otherRecipients.push(jid)
465
- allRecipients.push(jid)
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))
700
+ }
466
701
  }
467
702
 
468
- await assertSessions(allRecipients)
703
+ logger.debug({ msgId: finalMsgId }, `sending message to ${participants.length} devices`)
704
+ await sendNode(stanza)
705
+ if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid, finalMsgId, message)
706
+
707
+ }, activeSender)
708
+
709
+ const isSelf = areJidsSameUser(jid, meId) || (meLid && areJidsSameUser(jid, meLid))
710
+ const returnParticipant = (isGroup || isSelf) ? jidNormalizedUser(activeSender) : undefined
711
+ return {
712
+ key: {
713
+ remoteJid: jid,
714
+ fromMe: true,
715
+ id: finalMsgId,
716
+ participant: returnParticipant,
717
+ addressingMode: (isLid || (isGroup && groupAddressingMode === 'lid')) ? 'lid' : 'pn'
718
+ },
719
+ messageId: finalMsgId
720
+ }
721
+ }
469
722
 
470
- const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
471
- createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
472
- createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
473
- ])
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 }) })
724
+ const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update')
474
725
 
475
- participants.push(...meNodes, ...otherNodes)
476
- if (meRecipients.length > 0 || otherRecipients.length > 0) extraAttrs.phash = generateParticipantHashV2([...meRecipients, ...otherRecipients])
477
- shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
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'
478
744
  }
479
745
 
480
- if (participants.length) {
481
- if (additionalAttributes?.category === 'peer') { const peerNode = participants[0]?.content?.[0]; if (peerNode) binaryNodeContent.push(peerNode) }
482
- else binaryNodeContent.push({ tag: 'participants', attrs: {}, content: participants })
746
+ // Unwrap shorthand `interactive` key
747
+ if (content.interactive && !content.interactiveMessage) {
748
+ const { interactive, ...rest } = content
749
+ content = { ...rest, interactiveMessage: interactive }
483
750
  }
484
751
 
485
- const stanza = { tag: 'message', attrs: { id: finalMsgId, to: destinationJid, type: getMessageType(message), ...(additionalAttributes || {}) }, content: binaryNodeContent }
752
+ const messageType = nexus.detectType(content)
753
+ if (messageType) return await nexus.processMessage(content, jid, quoted)
486
754
 
487
- if (participant) {
488
- if (isJidGroup(destinationJid)) { stanza.attrs.to = destinationJid; stanza.attrs.participant = participant.jid }
489
- else if (areJidsSameUser(participant.jid, meId)) { stanza.attrs.to = participant.jid; stanza.attrs.recipient = destinationJid }
490
- else stanza.attrs.to = participant.jid
491
- } else stanza.attrs.to = destinationJid
492
-
493
- let additionalAlready = false
494
- if (!isNewsletter && buttonType) {
495
- const buttonsNode = getButtonArgs(messages)
496
- const filteredButtons = getBinaryFilteredButtons(additionalNodes || [])
497
- if (filteredButtons) { stanza.content.push(...additionalNodes); additionalAlready = true }
498
- else stanza.content.push(buttonsNode)
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
499
761
  }
500
762
 
501
- if (shouldIncludeDeviceIdentity) { stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) }); logger.debug({ jid }, 'adding device identity') }
502
- if (additionalNodes?.length > 0 && !additionalAlready) stanza.content.push(...additionalNodes)
503
- // Add TCToken support with expiration validation
504
- if (!isGroup && !isRetryResend && !isStatus) {
505
- const contactTcTokenData = await authState.keys.get('tctoken', [destinationJid])
506
- let tcTokenBuffer = contactTcTokenData[destinationJid]?.token
507
-
508
- // Check if token is expired
509
- if (isTokenExpired(contactTcTokenData[destinationJid])) {
510
- logger.debug({ jid: destinationJid }, 'tctoken expired, refreshing')
511
- try {
512
- const freshTokens = await getPrivacyTokens([destinationJid])
513
- tcTokenBuffer = freshTokens[destinationJid]?.token
514
- } catch (err) {
515
- logger.warn({ jid: destinationJid, err }, 'failed to refresh expired tctoken')
516
- }
517
- }
518
-
519
- if (tcTokenBuffer) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
520
- }
521
-
522
- logger.debug({ msgId }, `sending message to ${participants.length} devices`)
523
- await sendNode(stanza)
524
- if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid, msgId, message)
525
- }, meId)
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
+ }
526
769
 
527
- return {key: {remoteJid: jid, fromMe: true, id: finalMsgId, participant: isGroup ? authState.creds.me.id : undefined}, messageId: finalMsgId}
528
- }
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
+ }
529
776
 
530
- const TOKEN_EXPIRY_TTL = 24 * 60 * 60 // 24 hours in seconds
531
-
532
- const isTokenExpired = (tokenData) => {
533
- if (!tokenData || !tokenData.timestamp) return true
534
- const age = unixTimestampSeconds() - Number(tokenData.timestamp)
535
- return age > TOKEN_EXPIRY_TTL
536
- }
537
-
538
- const getPrivacyTokens = async (jids) => {
539
- const t = unixTimestampSeconds().toString()
540
- const result = await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' }, content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }] })
541
- const tokens = parseTCTokens(result)
542
- if (Object.keys(tokens).length > 0) await authState.keys.set({ 'tctoken': tokens })
543
- return tokens
544
- }
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
+ }
545
782
 
546
- const waUploadToServer = getWAUploadToServer(config, refreshMediaConn)
547
- const nexus = new NexusHandler(Utils, waUploadToServer, relayMessage, { logger, mediaCache: config.mediaCache, options: config.options, mediaUploadTimeoutMs: config.mediaUploadTimeoutMs, user: authState.creds.me })
548
- const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update')
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
+ }
549
840
 
550
841
  return {
551
842
  ...sock,
552
- getPrivacyTokens, assertSessions, relayMessage, sendReceipt, sendReceipts, nexus, readMessages,
553
- refreshMediaConn, waUploadToServer, fetchPrivacySettings, sendPeerDataOperationMessage,
843
+ getPrivacyTokens, issuePrivacyTokens, assertSessions, relayMessage,
844
+ sendReceipt, sendReceipts, nexus, readMessages, refreshMediaConn,
845
+ waUploadToServer, fetchPrivacySettings, sendPeerDataOperationMessage,
554
846
  createParticipantNodes, getUSyncDevices, messageRetryManager, updateMemberLabel,
555
847
 
556
848
  updateMediaMessage: async (message) => {
@@ -562,11 +854,14 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
562
854
  await Promise.all([sendNode(node), waitForMsgMediaUpdate(async (update) => {
563
855
  const result = update.find(c => c.key.id === message.key.id)
564
856
  if (result) {
565
- if (result.error) error = result.error
566
- else {
857
+ if (result.error) {
858
+ error = result.error
859
+ } else {
567
860
  try {
568
861
  const media = await decryptMediaRetryData(result.media, mediaKey, result.key.id)
569
- if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) throw new Boom(`Media re-upload failed (${proto.MediaRetryNotification.ResultType[media.result]})`, { data: media, statusCode: getStatusCodeForMediaRetry(media.result) || 404 })
862
+ if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) {
863
+ throw new Boom(`Media re-upload failed (${proto.MediaRetryNotification.ResultType[media.result]})`, { data: media, statusCode: getStatusCodeForMediaRetry(media.result) || 404 })
864
+ }
570
865
  content.directPath = media.directPath
571
866
  content.url = getUrlFromDirectPath(content.directPath)
572
867
  logger.debug({ directPath: media.directPath, key: result.key }, 'media update successful')
@@ -584,30 +879,41 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
584
879
  const userJid = jidNormalizedUser(authState.creds.me.id)
585
880
  const allUsers = new Set([userJid])
586
881
  for (const id of jids) {
587
- if (isJidGroup(id)) { try { const metadata = await cachedGroupMetadata(id) || await groupMetadata(id); metadata.participants.forEach(p => allUsers.add(jidNormalizedUser(p.id))) } catch (error) { logger.error(`Error getting metadata for ${id}: ${error}`) } }
588
- else if (isJidUser(id)) allUsers.add(jidNormalizedUser(id))
882
+ if (isJidGroup(id)) {
883
+ try { const metadata = await cachedGroupMetadata(id) || await groupMetadata(id); metadata.participants.forEach(p => allUsers.add(jidNormalizedUser(p.id))) }
884
+ catch (error) { logger.error(`Error getting metadata for ${id}: ${error}`) }
885
+ } else if (isJidUser(id)) {
886
+ allUsers.add(jidNormalizedUser(id))
887
+ }
589
888
  }
590
- const uniqueUsers = Array.from(allUsers)
889
+
591
890
  const getRandomHex = () => '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')
592
891
  const isMedia = content.image || content.video || content.audio
593
892
  const isAudio = !!content.audio
594
893
  const msgContent = { ...content }
595
894
  if (isMedia && !isAudio) { if (msgContent.text) { msgContent.caption = msgContent.text; delete msgContent.text }; delete msgContent.ptt; delete msgContent.font; delete msgContent.backgroundColor; delete msgContent.textColor }
596
895
  if (isAudio) { delete msgContent.text; delete msgContent.caption; delete msgContent.font; delete msgContent.textColor }
597
- const font = !isMedia ? (content.font || Math.floor(Math.random() * 9)) : undefined
598
- const textColor = !isMedia ? (content.textColor || getRandomHex()) : undefined
599
- const backgroundColor = (!isMedia || isAudio) ? (content.backgroundColor || getRandomHex()) : undefined
600
- const ptt = isAudio ? (typeof content.ptt === 'boolean' ? content.ptt : true) : undefined
601
- let msg, mediaHandle
896
+
897
+ let msg
602
898
  try {
603
899
  msg = await generateWAMessage(STORIES_JID, msgContent, {
604
900
  logger, userJid,
605
901
  getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
606
- upload: async (encFilePath, opts) => { const up = await waUploadToServer(encFilePath, { ...opts }); mediaHandle = up.handle; return up },
607
- mediaCache: config.mediaCache, options: config.options, font, textColor, backgroundColor, ptt
902
+ upload: async (encFilePath, opts) => { const up = await waUploadToServer(encFilePath, { ...opts }); return up },
903
+ mediaCache: config.mediaCache, options: config.options,
904
+ font: !isMedia ? (content.font || Math.floor(Math.random() * 9)) : undefined,
905
+ textColor: !isMedia ? (content.textColor || getRandomHex()) : undefined,
906
+ backgroundColor: (!isMedia || isAudio) ? (content.backgroundColor || getRandomHex()) : undefined,
907
+ ptt: isAudio ? (typeof content.ptt === 'boolean' ? content.ptt : true) : undefined
608
908
  })
609
909
  } catch (error) { logger.error(`Error generating message: ${error}`); throw error }
610
- await relayMessage(STORIES_JID, msg.message, { messageId: msg.key.id, statusJidList: uniqueUsers, additionalNodes: [{ tag: 'meta', attrs: {}, content: [{ tag: 'mentioned_users', attrs: {}, content: jids.map(jid => ({ tag: 'to', attrs: { jid: jidNormalizedUser(jid) } })) }] }] })
910
+
911
+ await relayMessage(STORIES_JID, msg.message, {
912
+ messageId: msg.key.id,
913
+ statusJidList: Array.from(allUsers),
914
+ additionalNodes: [{ tag: 'meta', attrs: {}, content: [{ tag: 'mentioned_users', attrs: {}, content: jids.map(jid => ({ tag: 'to', attrs: { jid: jidNormalizedUser(jid) } })) }] }]
915
+ })
916
+
611
917
  for (const id of jids) {
612
918
  try {
613
919
  const normalizedId = jidNormalizedUser(id)
@@ -622,6 +928,7 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
622
928
  return msg
623
929
  },
624
930
 
931
+ // Nexus handler shortcuts
625
932
  sendPaymentMessage: (jid, data, quoted) => nexus.handlePayment({ requestPaymentMessage: data }, jid, quoted),
626
933
  sendProductMessage: (jid, data, quoted) => nexus.handleProduct({ productMessage: data }, jid, quoted),
627
934
  sendInteractiveMessage: (jid, data, quoted) => nexus.handleInteractive({ interactiveMessage: data }, jid, quoted),
@@ -634,53 +941,20 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
634
941
  sendCarouselMessage: (jid, data, quoted) => nexus.handleCarousel({ carouselMessage: data }, jid, quoted),
635
942
  sendCarouselProtoMessage: (jid, data, quoted) => nexus.handleCarouselProto({ carouselProto: data }, jid, quoted),
636
943
  stickerPackMessage: (jid, data, options) => nexus.handleStickerPack(data, jid, options?.quoted),
637
-
638
- sendMessage: async (jid, content, options = {}) => {
639
- const userJid = authState.creds.me.id
640
- const { quoted } = options
641
-
642
- if (content.interactive && !content.interactiveMessage) { const { interactive, ...rest } = content; content = { ...rest, interactiveMessage: interactive } }
643
-
644
- const messageType = nexus.detectType(content)
645
- if (messageType) return await nexus.processMessage(content, jid, quoted)
646
-
647
- if (content.disappearingMessagesInChat && isJidGroup(jid)) {
648
- const value = typeof content.disappearingMessagesInChat === 'boolean' ? (content.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : content.disappearingMessagesInChat
649
- await groupToggleEphemeral(jid, value)
650
- return
651
- }
652
-
653
- const fullMsg = await generateWAMessage(jid, content, {
654
- logger, userJid,
655
- getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
656
- getProfilePicUrl: sock.profilePictureUrl, getCallLink: sock.createCallLink,
657
- upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options,
658
- messageId: generateMessageIDV2(sock.user?.id), ...options
659
- })
660
-
661
- const additionalAttributes = {}, additionalNodes = []
662
- if (content.delete) additionalAttributes.edit = isJidGroup(content.delete?.remoteJid) && !content.delete?.fromMe ? '8' : '7'
663
- else if (content.edit) additionalAttributes.edit = '1'
664
- else if (content.pin) additionalAttributes.edit = '2'
665
- if (content.poll) additionalNodes.push({ tag: 'meta', attrs: { polltype: 'creation' } })
666
- if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
667
-
668
- // Auto-fetch TCToken for new contacts
669
- if (!isJidGroup(jid) && !content.disappearingMessagesInChat) {
670
- const existingToken = await authState.keys.get('tctoken', [jid])
671
- if (!existingToken[jid]) {
672
- try { await getPrivacyTokens([jid]); logger.debug({ jid }, 'fetched tctoken for new contact') }
673
- catch (err) { logger.warn({ jid, err }, 'failed to fetch tctoken') }
674
- }
675
- }
676
-
677
- await relayMessage(jid, fullMsg.message, {
678
- messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata,
679
- additionalAttributes, statusJidList: options.statusJidList, additionalNodes
680
- })
681
-
682
- if (config.emitOwnEvents) process.nextTick(() => { processingMutex.mutex(() => upsertMessage(fullMsg, 'append')) })
683
- return fullMsg
684
- }
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),
685
959
  }
686
960
  }