@hansaka02/baileys 7.3.4 → 7.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +203 -247
  2. package/lib/Defaults/baileys-version.json +2 -2
  3. package/lib/Defaults/connection.js +1 -1
  4. package/lib/Defaults/constants.js +13 -1
  5. package/lib/Defaults/history.js +3 -1
  6. package/lib/Signal/Group/sender-chain-key.js +1 -14
  7. package/lib/Signal/Group/sender-key-distribution-message.js +2 -2
  8. package/lib/Signal/Group/sender-key-record.js +2 -11
  9. package/lib/Signal/Group/sender-key-state.js +11 -57
  10. package/lib/Signal/libsignal.js +200 -116
  11. package/lib/Signal/lid-mapping.js +121 -68
  12. package/lib/Socket/Client/websocket.js +9 -2
  13. package/lib/Socket/business.js +5 -1
  14. package/lib/Socket/chats.js +180 -89
  15. package/lib/Socket/community.js +169 -41
  16. package/lib/Socket/groups.js +25 -21
  17. package/lib/Socket/messages-recv.js +458 -333
  18. package/lib/Socket/messages-send.js +517 -572
  19. package/lib/Socket/mex.js +61 -0
  20. package/lib/Socket/newsletter.js +159 -252
  21. package/lib/Socket/socket.js +283 -100
  22. package/lib/Types/Newsletter.js +32 -25
  23. package/lib/Utils/auth-utils.js +189 -354
  24. package/lib/Utils/browser-utils.js +43 -0
  25. package/lib/Utils/chat-utils.js +166 -41
  26. package/lib/Utils/decode-wa-message.js +77 -35
  27. package/lib/Utils/event-buffer.js +80 -24
  28. package/lib/Utils/generics.js +28 -128
  29. package/lib/Utils/history.js +10 -8
  30. package/lib/Utils/index.js +1 -1
  31. package/lib/Utils/link-preview.js +17 -32
  32. package/lib/Utils/lt-hash.js +28 -22
  33. package/lib/Utils/make-mutex.js +26 -28
  34. package/lib/Utils/message-retry-manager.js +51 -3
  35. package/lib/Utils/messages-media.js +343 -151
  36. package/lib/Utils/messages.js +806 -792
  37. package/lib/Utils/noise-handler.js +33 -2
  38. package/lib/Utils/pre-key-manager.js +126 -0
  39. package/lib/Utils/process-message.js +115 -55
  40. package/lib/Utils/signal.js +45 -18
  41. package/lib/Utils/validate-connection.js +52 -29
  42. package/lib/WABinary/constants.js +1268 -1268
  43. package/lib/WABinary/decode.js +58 -4
  44. package/lib/WABinary/encode.js +54 -7
  45. package/lib/WABinary/jid-utils.js +58 -11
  46. package/lib/WAM/constants.js +19064 -11563
  47. package/lib/WAM/encode.js +57 -8
  48. package/lib/WAUSync/USyncQuery.js +35 -19
  49. package/package.json +9 -8
  50. package/lib/Socket/usync.js +0 -83
@@ -34,21 +34,19 @@ const {
34
34
  prepareAlbumMessageContent,
35
35
  aggregateMessageKeysNotFromMe
36
36
  } = require("../Utils")
37
- const {
38
- QueryIds,
39
- XWAPaths,
40
- WAMessageAddressingMode
41
- } = require("../Types")
37
+ const { WAMessageAddressingMode } = require("../Types")
42
38
  const {
43
39
  areJidsSameUser,
44
40
  getBinaryNodeChild,
45
41
  getBinaryNodeChildren,
46
42
  getBinaryFilteredBizBot,
47
43
  getBinaryFilteredButtons,
44
+ isHostedLidUser,
45
+ isHostedPnUser,
48
46
  isJidNewsletter,
49
47
  isJidGroup,
50
48
  isLidUser,
51
- isJidUser,
49
+ isPnUser,
52
50
  jidDecode,
53
51
  jidEncode,
54
52
  jidNormalizedUser,
@@ -64,15 +62,46 @@ const { getUrlInfo } = require("../Utils/link-preview")
64
62
  const { makeKeyedMutex } = require("../Utils/make-mutex")
65
63
 
66
64
  const makeMessagesSocket = (config) => {
67
- const { logger, maxMsgRetryCount, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: axiosOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache } = config
65
+ const {
66
+ logger,
67
+ linkPreviewImageThumbnailWidth,
68
+ generateHighQualityLinkPreview,
69
+ options: httpRequestOptions,
70
+ patchMessageBeforeSending,
71
+ cachedGroupMetadata,
72
+ enableRecentMessageCache,
73
+ maxMsgRetryCount
74
+ } = config
75
+
68
76
  const suki = makeNewsletterSocket(config)
69
- const { ev, authState, processingMutex, signalRepository, upsertMessage, createCallLink, query, fetchPrivacySettings, sendNode, groupQuery, groupMetadata, groupToggleEphemeral, newsletterWMexQuery, executeUSyncQuery } = suki
77
+
78
+ const {
79
+ ev,
80
+ authState,
81
+ messageMutex,
82
+ signalRepository,
83
+ upsertMessage,
84
+ createCallLink,
85
+ query,
86
+ fetchPrivacySettings,
87
+ sendNode,
88
+ groupQuery,
89
+ groupMetadata,
90
+ groupToggleEphemeral,
91
+ executeUSyncQuery,
92
+ newsletterMetadata
93
+ } = suki
70
94
 
71
95
  const userDevicesCache = config.userDevicesCache || new NodeCache({
72
96
  stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES,
73
97
  useClones: false
74
98
  })
75
99
 
100
+ const peerSessionsCache = new NodeCache({
101
+ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES,
102
+ useClones: false
103
+ })
104
+
76
105
  // Initialize message retry manager if enabled
77
106
  const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null
78
107
 
@@ -83,38 +112,38 @@ const makeMessagesSocket = (config) => {
83
112
 
84
113
  const refreshMediaConn = async (forceGet = false) => {
85
114
  const media = await mediaConn
86
-
87
- if (!media || forceGet || (new Date().getTime() - media.fetchDate.getTime()) > media.ttl * 1000) {
115
+
116
+ if (!media || forceGet || new Date().getTime() - media.fetchDate.getTime() > media.ttl * 1000) {
88
117
  mediaConn = (async () => {
89
-
90
118
  const result = await query({
91
119
  tag: 'iq',
92
120
  attrs: {
93
121
  type: 'set',
94
122
  xmlns: 'w:m',
95
- to: S_WHATSAPP_NET,
123
+ to: S_WHATSAPP_NET
96
124
  },
97
125
  content: [{ tag: 'media_conn', attrs: {} }]
98
126
  })
99
-
127
+
100
128
  const mediaConnNode = getBinaryNodeChild(result, 'media_conn')
101
-
129
+
130
+ // TODO: explore full length of data that whatsapp provides
102
131
  const node = {
103
132
  hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(({ attrs }) => ({
104
133
  hostname: attrs.hostname,
105
- maxContentLengthBytes: +attrs.maxContentLengthBytes,
134
+ maxContentLengthBytes: +attrs.maxContentLengthBytes
106
135
  })),
107
136
  auth: mediaConnNode.attrs.auth,
108
137
  ttl: +mediaConnNode.attrs.ttl,
109
138
  fetchDate: new Date()
110
139
  }
111
-
140
+
112
141
  logger.debug('fetched media conn')
113
-
142
+
114
143
  return node
115
144
  })()
116
145
  }
117
-
146
+
118
147
  return mediaConn
119
148
  }
120
149
 
@@ -123,37 +152,42 @@ const makeMessagesSocket = (config) => {
123
152
  * used for receipts of phone call, read, delivery etc.
124
153
  * */
125
154
  const sendReceipt = async (jid, participant, messageIds, type) => {
155
+ if (!messageIds || messageIds.length === 0) {
156
+ throw new Boom('missing ids in receipt')
157
+ }
158
+
126
159
  const node = {
127
160
  tag: 'receipt',
128
161
  attrs: {
129
- id: messageIds[0],
130
- },
162
+ id: messageIds[0]
163
+ }
131
164
  }
132
-
165
+
133
166
  const isReadReceipt = type === 'read' || type === 'read-self'
134
-
167
+
135
168
  if (isReadReceipt) {
136
169
  node.attrs.t = unixTimestampSeconds().toString()
137
170
  }
138
-
139
- if (type === 'sender' && isJidUser(jid)) {
171
+
172
+ if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) {
140
173
  node.attrs.recipient = jid
141
174
  node.attrs.to = participant
142
175
  }
143
-
176
+
144
177
  else {
145
178
  node.attrs.to = jid
179
+
146
180
  if (participant) {
147
181
  node.attrs.participant = participant
148
182
  }
149
183
  }
150
-
184
+
151
185
  if (type) {
152
- node.attrs.type = isJidNewsletter(jid) ? 'read-self' : type
186
+ node.attrs.type = type
153
187
  }
154
-
188
+
155
189
  const remainingMessageIds = messageIds.slice(1)
156
-
190
+
157
191
  if (remainingMessageIds.length) {
158
192
  node.content = [
159
193
  {
@@ -166,9 +200,9 @@ const makeMessagesSocket = (config) => {
166
200
  }
167
201
  ]
168
202
  }
169
-
203
+
170
204
  logger.debug({ attrs: node.attrs, messageIds }, 'sending receipt for messages')
171
-
205
+
172
206
  await sendNode(node)
173
207
  }
174
208
 
@@ -191,84 +225,6 @@ const makeMessagesSocket = (config) => {
191
225
  await sendReceipts(keys, readType)
192
226
  }
193
227
 
194
- /**
195
- * Deduplicate JIDs when both LID and PN versions exist for same user
196
- * Prefers LID over PN to maintain single encryption layer
197
- */
198
- const deduplicateLidPnJids = (jids) => {
199
- const lidUsers = new Set()
200
- const filteredJids = []
201
-
202
- // Collect all LID users
203
- for (const jid of jids) {
204
- if (isLidUser(jid)) {
205
- const user = jidDecode(jid)?.user
206
- if (user)
207
- lidUsers.add(user)
208
- }
209
- }
210
-
211
- // Filter out PN versions when LID exists
212
- for (const jid of jids) {
213
- if (isJidUser(jid)) {
214
- const user = jidDecode(jid)?.user
215
- if (user && lidUsers.has(user)) {
216
- logger.debug({ jid }, 'Skipping PN - LID version exists')
217
- continue
218
- }
219
- }
220
- filteredJids.push(jid)
221
- }
222
- return filteredJids
223
- }
224
-
225
- /** Fetch image for groups, user, and newsletter **/
226
- const profilePictureUrl = async (jid) => {
227
- if (isJidNewsletter(jid)) {
228
-
229
- let node = await newsletterWMexQuery(undefined, QueryIds.METADATA, {
230
- input: {
231
- key: jid,
232
- type: 'JID',
233
- view_role: 'GUEST'
234
- },
235
- fetch_viewer_metadata: true,
236
- fetch_full_image: true,
237
- fetch_creation_time: true
238
- })
239
-
240
- let result = getBinaryNodeChild(node, 'result')?.content?.toString()
241
-
242
- let metadata = JSON.parse(result).data[XWAPaths.NEWSLETTER]
243
-
244
- return getUrlFromDirectPath(metadata.thread_metadata.picture?.direct_path || '')
245
-
246
- }
247
-
248
- else {
249
- const result = await query({
250
- tag: 'iq',
251
- attrs: {
252
- target: jidNormalizedUser(jid),
253
- to: S_WHATSAPP_NET,
254
- type: 'get',
255
- xmlns: 'w:profile:picture'
256
- },
257
- content: [{
258
- tag: 'picture',
259
- attrs: {
260
- type: 'image',
261
- query: 'url'
262
- }
263
- }]
264
- })
265
-
266
- const child = getBinaryNodeChild(result, 'picture')
267
-
268
- return child?.attrs?.url || null
269
- }
270
- }
271
-
272
228
  /** Fetch all the devices we've to send a message to */
273
229
  const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
274
230
  const deviceResults = []
@@ -279,7 +235,6 @@ const makeMessagesSocket = (config) => {
279
235
 
280
236
  const toFetch = []
281
237
 
282
- jids = deduplicateLidPnJids(Array.from(new Set(jids)))
283
238
  const jidsWithUser = jids
284
239
  .map(jid => {
285
240
  const decoded = jidDecode(jid)
@@ -291,20 +246,22 @@ const makeMessagesSocket = (config) => {
291
246
  deviceResults.push({
292
247
  user,
293
248
  device,
294
- wireJid: jid // again this makes no sense
295
- });
249
+ jid
250
+ })
251
+
296
252
  return null
297
253
  }
298
254
 
299
255
  jid = jidNormalizedUser(jid)
256
+
300
257
  return { jid, user }
301
- })
302
- .filter(jid => jid !== null)
303
-
258
+ }).filter(jid => jid !== null)
259
+
304
260
  let mgetDevices
305
261
 
306
262
  if (useCache && userDevicesCache.mget) {
307
263
  const usersToFetch = jidsWithUser.map(j => j?.user).filter(Boolean)
264
+
308
265
  mgetDevices = await userDevicesCache.mget(usersToFetch)
309
266
  }
310
267
 
@@ -314,19 +271,21 @@ const makeMessagesSocket = (config) => {
314
271
  (userDevicesCache.mget ? undefined : (await userDevicesCache.get(user)))
315
272
 
316
273
  if (devices) {
317
- const isLidJid = isLidUser(jid)
318
- const devicesWithWire = devices.map(d => ({
274
+ const devicesWithJid = devices.map(d => ({
319
275
  ...d,
320
- wireJid: isLidJid ? jidEncode(d.user, 'lid', d.device) : jidEncode(d.user, 's.whatsapp.net', d.device)
276
+ jid: jidEncode(d.user, d.server, d.device)
321
277
  }))
322
278
 
323
- deviceResults.push(...devicesWithWire)
279
+ deviceResults.push(...devicesWithJid)
280
+
324
281
  logger.trace({ user }, 'using cache for devices')
325
282
  }
283
+
326
284
  else {
327
285
  toFetch.push(jid)
328
286
  }
329
287
  }
288
+
330
289
  else {
331
290
  toFetch.push(jid)
332
291
  }
@@ -337,23 +296,49 @@ const makeMessagesSocket = (config) => {
337
296
  }
338
297
 
339
298
  const requestedLidUsers = new Set()
299
+
340
300
  for (const jid of toFetch) {
341
- if (isLidUser(jid)) {
301
+ if (isLidUser(jid) || isHostedLidUser(jid)) {
342
302
  const user = jidDecode(jid)?.user
303
+
343
304
  if (user)
344
305
  requestedLidUsers.add(user)
345
306
  }
346
307
  }
347
308
 
348
- const query = new USyncQuery().withContext('message').withDeviceProtocol()
309
+ const query = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
310
+
349
311
  for (const jid of toFetch) {
350
- query.withUser(new USyncUser().withId(jid))
312
+ query.withUser(new USyncUser().withId(jid)) // todo: investigate - the idea here is that <user> should have an inline lid field with the lid being the pn equivalent
351
313
  }
352
314
 
353
315
  const result = await executeUSyncQuery(query)
354
316
 
355
317
  if (result) {
356
- const extracted = extractDeviceJids(result?.list, authState.creds.me.id, ignoreZeroDevices)
318
+ // TODO: LID MAP this stuff (lid protocol will now return lid with devices)
319
+ const lidResults = result.list.filter(a => !!a.lid)
320
+
321
+ if (lidResults.length > 0) {
322
+ logger.trace('Storing LID maps from device call')
323
+
324
+ await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })))
325
+
326
+ // Force-refresh sessions for newly mapped LIDs to align identity addressing
327
+ try {
328
+ const lids = lidResults.map(a => a.lid)
329
+
330
+ if (lids.length) {
331
+ await assertSessions(lids, true)
332
+ }
333
+ }
334
+
335
+ catch (e) {
336
+ logger.warn({ e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
337
+ }
338
+ }
339
+
340
+ const extracted = extractDeviceJids(result?.list, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices)
341
+
357
342
  const deviceMap = {}
358
343
 
359
344
  for (const item of extracted) {
@@ -363,22 +348,21 @@ const makeMessagesSocket = (config) => {
363
348
 
364
349
  // Process each user's devices as a group for bulk LID migration
365
350
  for (const [user, userDevices] of Object.entries(deviceMap)) {
366
- const isLidUser = requestedLidUsers.has(user)
367
-
351
+ const isLidUser = requestedLidUsers.has(user);
368
352
  // Process all devices for this user
369
353
  for (const item of userDevices) {
370
- const finalWireJid = isLidUser
371
- ? jidEncode(user, 'lid', item.device)
372
- : jidEncode(item.user, 's.whatsapp.net', item.device)
354
+ const finalJid = isLidUser
355
+ ? jidEncode(user, item.server, item.device)
356
+ : jidEncode(item.user, item.server, item.device)
373
357
  deviceResults.push({
374
358
  ...item,
375
- wireJid: finalWireJid
376
- });
359
+ jid: finalJid
360
+ })
377
361
 
378
362
  logger.debug({
379
363
  user: item.user,
380
364
  device: item.device,
381
- finalWireJid,
365
+ finalJid,
382
366
  usedLid: isLidUser
383
367
  }, 'Processed device with LID priority')
384
368
  }
@@ -396,162 +380,99 @@ const makeMessagesSocket = (config) => {
396
380
  }
397
381
  }
398
382
 
399
- }
400
- return deviceResults
401
- }
402
-
403
- const assertSessions = async (jids, force) => {
404
- let didFetchNewSession = false
405
- const jidsRequiringFetch = []
406
-
407
- // Apply same deduplication as in getUSyncDevices
408
- jids = deduplicateLidPnJids(jids)
409
-
410
- if (force) {
411
- // Check which sessions are missing (with LID migration check)
412
- const addrs = jids.map(jid => signalRepository.jidToSignalProtocolAddress(jid))
413
- const sessions = await authState.keys.get('session', addrs)
383
+ const userDeviceUpdates = {}
414
384
 
415
- const checkJidSession = (jid) => {
416
- const signalId = signalRepository.jidToSignalProtocolAddress(jid)
417
- const hasSession = !!sessions[signalId]
418
-
419
- // Add to fetch list if no session exists
420
- // Session type selection (LID vs PN) is handled in encryptMessage
421
- if (!hasSession) {
422
- if (jid.includes('@lid')) {
423
- logger.debug({ jid }, 'No LID session found, will create new LID session')
424
- }
425
- jidsRequiringFetch.push(jid)
385
+ for (const [userId, devices] of Object.entries(deviceMap)) {
386
+ if (devices && devices.length > 0) {
387
+ userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
426
388
  }
427
389
  }
428
390
 
429
- // Process all JIDs
430
- for (const jid of jids) {
431
- checkJidSession(jid)
391
+ if (Object.keys(userDeviceUpdates).length > 0) {
392
+ try {
393
+ await authState.keys.set({ 'device-list': userDeviceUpdates })
394
+
395
+ logger.debug({ userCount: Object.keys(userDeviceUpdates).length }, 'stored user device lists for bulk migration')
396
+ }
397
+
398
+ catch (error) {
399
+ logger.warn({ error }, 'failed to store user device lists')
400
+ }
432
401
  }
433
402
  }
434
- else {
435
- const addrs = jids.map(jid => signalRepository.jidToSignalProtocolAddress(jid))
436
- const sessions = await authState.keys.get('session', addrs)
437
-
438
- // Group JIDs by user for bulk migration
439
- const userGroups = new Map()
440
- for (const jid of jids) {
441
- const user = jidNormalizedUser(jid)
442
- if (!userGroups.has(user)) {
443
- userGroups.set(user, [])
403
+
404
+ return deviceResults
405
+ }
406
+
407
+ const updateMemberLabel = (jid, memberLabel) => {
408
+ if (!isJidGroup(jid)) {
409
+ throw new Error('Jid must a group!')
410
+ }
411
+
412
+ const protocolMessage = {
413
+ protocolMessage: {
414
+ type: proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE,
415
+ memberLabel: {
416
+ label: memberLabel?.slice(0, 30),
417
+ labelTimestamp: unixTimestampSeconds()
444
418
  }
445
- userGroups.get(user).push(jid)
446
419
  }
447
-
448
- // Helper to check LID mapping for a user
449
- const checkUserLidMapping = async (user, userJids) => {
450
- if (!userJids.some(jid => isJidUser(jid))) {
451
- return { shouldMigrate: false, lidForPN: undefined }
452
- }
453
-
454
- try {
455
- // Convert user to proper PN JID format for getLIDForPN
456
- const pnJid = `${user}@s.whatsapp.net`
457
- const mapping = await signalRepository.lidMapping.getLIDForPN(pnJid)
458
-
459
- if (mapping?.includes('@lid')) {
460
- logger.debug({ user, lidForPN: mapping, deviceCount: userJids.length }, 'User has LID mapping - preparing bulk migration')
461
- return { shouldMigrate: true, lidForPN: mapping }
420
+ }
421
+
422
+ return relayMessage(jid, protocolMessage, {
423
+ additionalNodes: [
424
+ {
425
+ tag: 'meta',
426
+ attrs: {
427
+ tag_reason: 'user_update',
428
+ appdata: 'member_tag'
462
429
  }
463
430
  }
464
- catch (error) {
465
- logger.debug({ user, error }, 'Failed to check LID mapping for user')
431
+ ]
432
+ })
433
+ }
434
+
435
+ const assertSessions = async (jids, force) => {
436
+ let didFetchNewSession = false
437
+
438
+ const uniqueJids = [...new Set(jids)] // Deduplicate JIDs
439
+ const jidsRequiringFetch = []
440
+
441
+ logger.debug({ jids }, 'assertSessions call with jids')
442
+
443
+ // Check peerSessionsCache and validate sessions using libsignal loadSession
444
+ for (const jid of uniqueJids) {
445
+ const signalId = signalRepository.jidToSignalProtocolAddress(jid)
446
+ const cachedSession = peerSessionsCache.get(signalId)
447
+
448
+ if (cachedSession !== undefined) {
449
+ if (cachedSession && !force) {
450
+ continue // Session exists in cache
466
451
  }
467
-
468
- return { shouldMigrate: false, lidForPN: undefined }
469
452
  }
470
453
 
471
- // Process each user group for potential bulk LID migration
472
- for (const [user, userJids] of userGroups) {
473
- const mappingResult = await checkUserLidMapping(user, userJids)
474
- const shouldMigrateUser = mappingResult.shouldMigrate
475
- const lidForPN = mappingResult.lidForPN
454
+ else {
455
+ const sessionValidation = await signalRepository.validateSession(jid)
456
+ const hasSession = sessionValidation.exists
476
457
 
477
- // Migrate all devices for this user if LID mapping exists
478
- if (shouldMigrateUser && lidForPN) {
479
- // Bulk migrate all user devices in single transaction
480
- const migrationResult = await signalRepository.migrateSession(userJids, lidForPN)
481
-
482
- if (migrationResult.migrated > 0) {
483
- logger.info({
484
- user,
485
- lidMapping: lidForPN,
486
- migrated: migrationResult.migrated,
487
- skipped: migrationResult.skipped,
488
- total: migrationResult.total
489
- }, 'Completed bulk migration for user devices');
490
- }
491
- else {
492
- logger.debug({
493
- user,
494
- lidMapping: lidForPN,
495
- skipped: migrationResult.skipped,
496
- total: migrationResult.total
497
- }, 'All user device sessions already migrated');
498
- }
499
- }
458
+ peerSessionsCache.set(signalId, hasSession)
500
459
 
501
- // Direct bulk session check with LID single source of truth
502
- const addMissingSessionsToFetchList = (jid) => {
503
- const signalId = signalRepository.jidToSignalProtocolAddress(jid)
504
-
505
- if (sessions[signalId]) return
506
-
507
- // Determine correct JID to fetch (LID if mapping exists, otherwise original)
508
- if (jid.includes('@s.whatsapp.net') && shouldMigrateUser && lidForPN) {
509
- const decoded = jidDecode(jid)
510
- const lidDeviceJid = decoded.device !== undefined ? `${jidDecode(lidForPN).user}:${decoded.device}@lid` : lidForPN
511
-
512
- jidsRequiringFetch.push(lidDeviceJid)
513
- logger.debug({ pnJid: jid, lidJid: lidDeviceJid }, 'Adding LID JID to fetch list (conversion)')
514
- }
515
-
516
- else {
517
- jidsRequiringFetch.push(jid)
518
- logger.debug({ jid }, 'Adding JID to fetch list')
519
- }
460
+ if (hasSession && !force) {
461
+ continue
520
462
  }
521
-
522
- userJids.forEach(addMissingSessionsToFetchList)
523
463
  }
464
+
465
+ jidsRequiringFetch.push(jid)
524
466
  }
525
467
 
526
468
  if (jidsRequiringFetch.length) {
527
- logger.debug({ jidsRequiringFetch }, 'fetching sessions')
528
-
529
- // DEBUG: Check if there are PN versions of LID users being fetched
530
- const lidUsersBeingFetched = new Set()
531
- const pnUsersBeingFetched = new Set()
532
-
533
- for (const jid of jidsRequiringFetch) {
534
- const user = jidDecode(jid)?.user
535
-
536
- if (user) {
537
- if (isLidUser(jid)) {
538
- lidUsersBeingFetched.add(user)
539
- }
540
- else if (isJidUser(jid)) {
541
- pnUsersBeingFetched.add(user)
542
- }
543
- }
544
- }
469
+ // LID if mapped, otherwise original
470
+ const wireJids = [
471
+ ...jidsRequiringFetch.filter(jid => !!isLidUser(jid) || !!isHostedLidUser(jid)),
472
+ ...((await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => !!isPnUser(jid) || !!isHostedPnUser(jid)))) || []).map(a => a.lid)
473
+ ]
545
474
 
546
- // Find overlaps
547
- const overlapping = Array.from(pnUsersBeingFetched).filter(user => lidUsersBeingFetched.has(user))
548
- if (overlapping.length > 0) {
549
- logger.warn({
550
- overlapping,
551
- lidUsersBeingFetched: Array.from(lidUsersBeingFetched),
552
- pnUsersBeingFetched: Array.from(pnUsersBeingFetched)
553
- }, 'Fetching both LID and PN sessions for same users')
554
- }
475
+ logger.debug({ jidsRequiringFetch, wireJids }, 'fetching sessions')
555
476
 
556
477
  const result = await query({
557
478
  tag: 'iq',
@@ -564,163 +485,99 @@ const makeMessagesSocket = (config) => {
564
485
  {
565
486
  tag: 'key',
566
487
  attrs: {},
567
- content: jidsRequiringFetch.map(jid => ({
568
- tag: 'user',
569
- attrs: { jid }
570
- }))
488
+ content: wireJids.map(jid => {
489
+ const attrs = { jid }
490
+
491
+ if (force) attrs.reason = 'identity'
492
+
493
+ return { tag: 'user', attrs }
494
+ })
571
495
  }
572
496
  ]
573
497
  })
574
498
 
575
499
  await parseAndInjectE2ESessions(result, signalRepository)
500
+
576
501
  didFetchNewSession = true
502
+
503
+ // Cache fetched sessions using wire JIDs
504
+ for (const wireJid of wireJids) {
505
+ const signalId = signalRepository.jidToSignalProtocolAddress(wireJid)
506
+ peerSessionsCache.set(signalId, true)
507
+ }
577
508
  }
509
+
578
510
  return didFetchNewSession
579
511
  }
580
-
581
- /** Send Peer Operation */
512
+
582
513
  const sendPeerDataOperationMessage = async (pdoMessage) => {
583
514
  //TODO: for later, abstract the logic to send a Peer Message instead of just PDO - useful for App State Key Resync with phone
584
515
  if (!authState.creds.me?.id) {
585
516
  throw new Boom('Not authenticated')
586
517
  }
587
-
518
+
588
519
  const protocolMessage = {
589
520
  protocolMessage: {
590
521
  peerDataOperationRequestMessage: pdoMessage,
591
522
  type: proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_MESSAGE
592
523
  }
593
524
  }
594
-
525
+
595
526
  const meJid = jidNormalizedUser(authState.creds.me.id)
596
-
597
527
  const msgId = await relayMessage(meJid, protocolMessage, {
598
528
  additionalAttributes: {
599
529
  category: 'peer',
600
- // eslint-disable-next-line camelcase
601
- push_priority: 'high_force',
530
+ push_priority: 'high_force'
602
531
  },
532
+ additionalNodes: [
533
+ {
534
+ tag: 'meta',
535
+ attrs: { appdata: 'default' }
536
+ }
537
+ ]
603
538
  })
604
-
539
+
605
540
  return msgId
606
541
  }
607
542
 
608
- const createParticipantNodes = async (jids, message, extraAttrs, dsmMessage) => {
609
- let patched = await patchMessageBeforeSending(message, jids)
610
-
611
- if (!Array.isArray(patched)) {
612
- patched = jids ? jids.map(jid => ({ recipientJid: jid, ...patched })) : [patched]
543
+ const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
544
+ if (!recipientJids.length) {
545
+ return { nodes: [], shouldIncludeDeviceIdentity: false }
613
546
  }
614
547
 
548
+ const patched = await patchMessageBeforeSending(message, recipientJids)
549
+ const patchedMessages = Array.isArray(patched)
550
+ ? patched
551
+ : recipientJids.map(jid => ({ recipientJid: jid, message: patched }))
552
+
615
553
  let shouldIncludeDeviceIdentity = false
616
554
 
617
555
  const meId = authState.creds.me.id
618
556
  const meLid = authState.creds.me?.lid
619
557
  const meLidUser = meLid ? jidDecode(meLid)?.user : null
620
- const devicesByUser = new Map()
621
-
622
- for (const patchedMessageWithJid of patched) {
623
- const { recipientJid: wireJid, ...patchedMessage } = patchedMessageWithJid
624
- if (!wireJid)
625
- continue
558
+ const encryptionPromises = patchedMessages.map(async ({ recipientJid: jid, message: patchedMessage }) => {
559
+ if (!jid) return null
626
560
 
627
- // Extract user from JID for grouping
628
- const decoded = jidDecode(wireJid)
629
- const user = decoded?.user
561
+ let msgToEncrypt = patchedMessage
630
562
 
631
- if (!user)
632
- continue
563
+ if (dsmMessage) {
564
+ const { user: targetUser } = jidDecode(jid)
565
+ const { user: ownPnUser } = jidDecode(meId)
566
+ const ownLidUser = meLidUser
567
+ const isOwnUser = targetUser === ownPnUser || (ownLidUser && targetUser === ownLidUser)
568
+ const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
633
569
 
634
- if (!devicesByUser.has(user)) {
635
- devicesByUser.set(user, []);
636
- }
637
-
638
- devicesByUser.get(user).push({ recipientJid: wireJid, patchedMessage })
639
- }
640
-
641
- // Process each user's devices sequentially, but different users in parallel
642
- const userEncryptionPromises = Array.from(devicesByUser.entries()).map(([user, userDevices]) => encryptionMutex.mutex(user, async () => {
643
- logger.debug({ user, deviceCount: userDevices.length }, 'Acquiring encryption lock for user devices');
644
- const userNodes = []
645
-
646
- // Helper to get encryption JID with LID migration
647
- const getEncryptionJid = async (wireJid) => {
648
- if (!isJidUser(wireJid))
649
- return wireJid
650
-
651
- try {
652
- const lidForPN = await signalRepository.lidMapping.getLIDForPN(wireJid)
653
-
654
- if (!lidForPN?.includes('@lid'))
655
- return wireJid
656
-
657
- // Preserve device ID from original wire JID
658
- const wireDecoded = jidDecode(wireJid)
659
- const deviceId = wireDecoded?.device || 0
660
- const lidDecoded = jidDecode(lidForPN)
661
- const lidWithDevice = jidEncode(lidDecoded?.user, 'lid', deviceId)
662
-
663
- // Migrate session to LID for unified encryption layer
664
- try {
665
- const migrationResult = await signalRepository.migrateSession([wireJid], lidWithDevice)
666
- const recipientUser = jidNormalizedUser(wireJid)
667
- const ownPnUser = jidNormalizedUser(meId)
668
- const isOwnDevice = recipientUser === ownPnUser
669
- logger.info({ wireJid, lidWithDevice, isOwnDevice }, 'Migrated to LID encryption')
670
-
671
- // Delete PN session after successful migration
672
- try {
673
- if (migrationResult.migrated) {
674
- await signalRepository.deleteSession([wireJid])
675
- logger.debug({ deletedPNSession: wireJid }, 'Deleted PN session')
676
- }
677
- }
678
- catch (deleteError) {
679
- logger.warn({ wireJid, error: deleteError }, 'Failed to delete PN session')
680
- }
681
- return lidWithDevice
682
- }
683
- catch (migrationError) {
684
- logger.warn({ wireJid, error: migrationError }, 'Failed to migrate session')
685
- return wireJid
686
- }
687
- }
688
- catch (error) {
689
- logger.debug({ wireJid, error }, 'Failed to check LID mapping')
690
- return wireJid
570
+ if (isOwnUser && !isExactSenderDevice) {
571
+ msgToEncrypt = dsmMessage
572
+ logger.debug({ jid, targetUser }, 'Using DSM for own device')
691
573
  }
692
574
  }
693
575
 
694
- // Encrypt to this user's devices sequentially to prevent session corruption
695
- for (const { recipientJid: wireJid, patchedMessage } of userDevices) {
696
- // DSM logic: Use DSM for own other devices (following whatsmeow implementation)
697
- let messageToEncrypt = patchedMessage
698
-
699
- if (dsmMessage) {
700
- const { user: targetUser } = jidDecode(wireJid)
701
- const { user: ownPnUser } = jidDecode(meId)
702
- const ownLidUser = meLidUser
703
-
704
- // Check if this is our device (same user, different device)
705
- const isOwnUser = targetUser === ownPnUser || (ownLidUser && targetUser === ownLidUser)
706
-
707
- // Exclude exact sender device (whatsmeow: if jid == ownJID || jid == ownLID { continue })
708
- const isExactSenderDevice = wireJid === meId || (authState.creds.me?.lid && wireJid === authState.creds.me.lid)
709
-
710
- if (isOwnUser && !isExactSenderDevice) {
711
- messageToEncrypt = dsmMessage
712
- logger.debug({ wireJid, targetUser }, 'Using DSM for own device')
713
- }
714
- }
715
-
716
- const bytes = encodeWAMessage(messageToEncrypt)
717
-
718
- // Get encryption JID with LID migration
719
- const encryptionJid = await getEncryptionJid(wireJid)
720
-
721
- // ENCRYPT: Use the determined encryption identity (prefers migrated LID)
576
+ const bytes = encodeWAMessage(msgToEncrypt)
577
+ const mutexKey = jid
578
+ const node = await encryptionMutex.mutex(mutexKey, async () => {
722
579
  const { type, ciphertext } = await signalRepository.encryptMessage({
723
- jid: encryptionJid, // Unified encryption layer (LID when available)
580
+ jid,
724
581
  data: bytes
725
582
  })
726
583
 
@@ -728,9 +585,9 @@ const makeMessagesSocket = (config) => {
728
585
  shouldIncludeDeviceIdentity = true
729
586
  }
730
587
 
731
- const node = {
588
+ return {
732
589
  tag: 'to',
733
- attrs: { jid: wireJid }, // Always use original wire identity in envelope
590
+ attrs: { jid },
734
591
  content: [
735
592
  {
736
593
  tag: 'enc',
@@ -742,53 +599,71 @@ const makeMessagesSocket = (config) => {
742
599
  content: ciphertext
743
600
  }
744
601
  ]
745
- }
746
- userNodes.push(node)
747
- }
748
- logger.debug({ user, nodesCreated: userNodes.length }, 'Releasing encryption lock for user devices');
749
- return userNodes
750
- }))
602
+ };
603
+ })
604
+
605
+ return node
606
+ })
607
+
608
+ const nodes = (await Promise.all(encryptionPromises)).filter(node => node !== null)
751
609
 
752
- // Wait for all users to complete (users are processed in parallel)
753
- const userNodesArrays = await Promise.all(userEncryptionPromises)
754
- const nodes = userNodesArrays.flat()
755
610
  return { nodes, shouldIncludeDeviceIdentity }
756
611
  }
612
+
613
+ /** Fetch image for groups, user, and newsletter **/
614
+ const profilePictureUrl = async (jid) => {
615
+ if (isJidNewsletter(jid)) {
616
+ const metadata = await suki.newsletterMetadata('JID', jid)
617
+
618
+ return getUrlFromDirectPath(metadata.thread_metadata.picture?.direct_path || '')
619
+
620
+ }
621
+
622
+ else {
623
+ const result = await query({
624
+ tag: 'iq',
625
+ attrs: {
626
+ target: jidNormalizedUser(jid),
627
+ to: S_WHATSAPP_NET,
628
+ type: 'get',
629
+ xmlns: 'w:profile:picture'
630
+ },
631
+ content: [{
632
+ tag: 'picture',
633
+ attrs: {
634
+ type: 'image',
635
+ query: 'url'
636
+ }
637
+ }]
638
+ })
639
+
640
+ const child = getBinaryNodeChild(result, 'picture')
641
+
642
+ return child?.attrs?.url || null
643
+ }
644
+ }
757
645
 
758
646
  const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, useUserDevicesCache, useCachedGroupMetadata, statusJidList, additionalNodes, AI = false }) => {
759
647
  const meId = authState.creds.me.id
760
648
  const meLid = authState.creds.me?.lid
761
-
649
+ const isRetryResend = Boolean(participant?.jid)
650
+
651
+ let shouldIncludeDeviceIdentity = isRetryResend
762
652
  let didPushAdditional = false
763
- let shouldIncludeDeviceIdentity = false
764
-
765
- const { user, server } = jidDecode(jid)
766
-
653
+
767
654
  const statusJid = 'status@broadcast'
655
+ const { user, server } = jidDecode(jid)
768
656
  const isGroup = server === 'g.us'
769
- const isPrivate = server === 's.whatsapp.net'
770
- const isNewsletter = server == 'newsletter'
771
657
  const isStatus = jid === statusJid
772
658
  const isLid = server === 'lid'
773
-
774
- // Keep user's original JID choice for envelope addressing
659
+ const isNewsletter = server === 'newsletter'
660
+ const isGroupOrStatus = isGroup || isStatus
775
661
  const finalJid = jid
776
662
 
777
- // ADDRESSING CONSISTENCY: Match own identity to conversation context
778
- let ownId = meId
779
-
780
- if (isLid && meLid) {
781
- ownId = meLid
782
- logger.debug({ to: jid, ownId }, 'Using LID identity for @lid conversation')
783
- }
784
- else {
785
- logger.debug({ to: jid, ownId }, 'Using PN identity for @s.whatsapp.net conversation')
786
- }
787
-
788
- msgId = msgId || generateMessageID(authState.creds.me.id)
789
- useUserDevicesCache = useUserDevicesCache !== false
663
+ msgId = msgId || generateMessageID(meId)
664
+ useUserDevicesCache = useUserDevicesCache !== false;
790
665
  useCachedGroupMetadata = useCachedGroupMetadata !== false && !isStatus
791
-
666
+
792
667
  const participants = []
793
668
  const destinationJid = !isStatus ? finalJid : statusJid
794
669
  const binaryNodeContent = []
@@ -799,7 +674,7 @@ const makeMessagesSocket = (config) => {
799
674
  destinationJid,
800
675
  message
801
676
  },
802
- messageContextInfo: message.messageContextInfo || {}
677
+ messageContextInfo: message.messageContextInfo
803
678
  }
804
679
 
805
680
  const extraAttrs = {}
@@ -813,19 +688,16 @@ const makeMessagesSocket = (config) => {
813
688
 
814
689
 
815
690
  if (participant) {
816
- // when the retry request is not for a group
817
- // only send to the specific device that asked for a retry
818
- // otherwise the message is sent out to every device that should be a recipient
819
691
  if (!isGroup && !isStatus) {
820
- additionalAttributes = { ...additionalAttributes, 'device_fanout': 'false' }
692
+ additionalAttributes = { ...additionalAttributes, device_fanout: 'false' }
821
693
  }
822
-
694
+
823
695
  const { user, device } = jidDecode(participant.jid)
824
-
696
+
825
697
  devices.push({
826
698
  user,
827
699
  device,
828
- wireJid: participant.jid // Use the participant JID as wire JID
700
+ jid: participant.jid
829
701
  })
830
702
  }
831
703
 
@@ -835,6 +707,34 @@ const makeMessagesSocket = (config) => {
835
707
  if (mediaType) {
836
708
  extraAttrs['mediatype'] = mediaType
837
709
  }
710
+
711
+ if (isNewsletter) {
712
+ const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
713
+ const bytes = encodeNewsletterMessage(patched)
714
+
715
+ binaryNodeContent.push({
716
+ tag: 'plaintext',
717
+ attrs: {},
718
+ content: bytes
719
+ })
720
+
721
+ const stanza = {
722
+ tag: 'message',
723
+ attrs: {
724
+ to: jid,
725
+ id: msgId,
726
+ type: getTypeMessage(message),
727
+ ...(additionalAttributes || {})
728
+ },
729
+ content: binaryNodeContent
730
+ }
731
+
732
+ logger.debug({ msgId }, `sending newsletter message to ${jid}`)
733
+
734
+ await sendNode(stanza)
735
+
736
+ return
737
+ }
838
738
 
839
739
  if (messages.pinInChatMessage || messages.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) {
840
740
  extraAttrs['decrypt-fail'] = 'hide'
@@ -844,153 +744,155 @@ const makeMessagesSocket = (config) => {
844
744
  extraAttrs['native_flow_name'] = messages.interactiveResponseMessage.nativeFlowResponseMessage?.name || 'menu_options'
845
745
  }
846
746
 
847
- if (isGroup || isStatus) {
747
+ if (isGroupOrStatus && !isRetryResend) {
848
748
  const [groupData, senderKeyMap] = await Promise.all([
849
749
  (async () => {
850
- let groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
851
-
750
+ let groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined // todo: should we rely on the cache specially if the cache is outdated and the metadata has new fields?
751
+
852
752
  if (groupData && Array.isArray(groupData?.participants)) {
853
753
  logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata')
854
754
  }
855
-
755
+
856
756
  else if (!isStatus) {
857
- groupData = await groupMetadata(jid)
757
+ groupData = await groupMetadata(jid) // TODO: start storing group participant list + addr mode in Signal & stop relying on this
858
758
  }
859
-
759
+
860
760
  return groupData
861
761
  })(),
862
-
863
762
  (async () => {
864
763
  if (!participant && !isStatus) {
865
- const result = await authState.keys.get('sender-key-memory', [jid])
764
+ // what if sender memory is less accurate than the cached metadata
765
+ // on participant change in group, we should do sender memory manipulation
766
+ const result = await authState.keys.get('sender-key-memory', [jid]) // TODO: check out what if the sender key memory doesn't include the LID stuff now?
767
+
866
768
  return result[jid] || {}
867
769
  }
868
-
770
+
869
771
  return {}
870
-
871
772
  })()
872
773
  ])
873
-
874
- if (!participant) {
875
- const participantsList = (groupData && !isStatus) ? groupData.participants.map(p => p.id) : []
876
-
877
- if (isStatus && statusJidList) {
878
- participantsList.push(...statusJidList)
774
+
775
+ const participantsList = groupData ? groupData.participants.map(p => p.id) : []
776
+
777
+ if (groupData?.ephemeralDuration && groupData.ephemeralDuration > 0) {
778
+ additionalAttributes = {
779
+ ...additionalAttributes,
780
+ expiration: groupData.ephemeralDuration.toString()
879
781
  }
880
-
881
- if (!isStatus) {
882
- const groupAddressingMode = groupData?.addressingMode || (isLid ? WAMessageAddressingMode.LID : WAMessageAddressingMode.PN)
883
- additionalAttributes = {
884
- ...additionalAttributes,
885
- addressing_mode: groupAddressingMode
886
- }
782
+ }
783
+
784
+ if (isStatus && statusJidList) {
785
+ participantsList.push(...statusJidList)
786
+ }
787
+
788
+ const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
789
+
790
+ devices.push(...additionalDevices)
791
+
792
+ if (isGroup) {
793
+ additionalAttributes = {
794
+ ...additionalAttributes,
795
+ addressing_mode: groupData?.addressingMode || 'lid'
887
796
  }
888
-
889
- const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
890
- devices.push(...additionalDevices)
891
797
  }
892
-
893
- const patched = await patchMessageBeforeSending(message, devices.map(d => jidEncode(d.user, isLid ? 'lid' : 's.whatsapp.net', d.device)))
894
- const bytes = encodeWAMessage(patched)
895
798
 
896
- // This should match the group's addressing mode and conversation context
897
- const groupAddressingMode = groupData?.addressingMode || (isLid ? 'lid' : 'pn')
799
+ const patched = await patchMessageBeforeSending(message)
800
+
801
+ if (Array.isArray(patched)) {
802
+ throw new Boom('Per-jid patching is not supported in groups')
803
+ }
804
+
805
+ const bytes = encodeWAMessage(patched)
806
+ const groupAddressingMode = additionalAttributes?.['addressing_mode'] || groupData?.addressingMode || 'lid'
898
807
  const groupSenderIdentity = groupAddressingMode === 'lid' && meLid ? meLid : meId
899
-
900
808
  const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({
901
809
  group: destinationJid,
902
810
  data: bytes,
903
811
  meId: groupSenderIdentity
904
812
  })
905
-
906
- const senderKeyJids = []
907
-
908
- // ensure a connection is established with every device
813
+
814
+ const senderKeyRecipients = []
815
+
909
816
  for (const device of devices) {
910
- // This preserves the LID migration results from getUSyncDevices
911
- const deviceJid = device.wireJid
817
+ const deviceJid = device.jid
912
818
  const hasKey = !!senderKeyMap[deviceJid]
913
- if (!hasKey || !!participant) {
914
- senderKeyJids.push(deviceJid)
915
- // store that this person has had the sender keys sent to them
819
+
820
+ if ((!hasKey || !!participant) &&
821
+ !isHostedLidUser(deviceJid) &&
822
+ !isHostedPnUser(deviceJid) &&
823
+ device.device !== 99) {
824
+ //todo: revamp all this logic
825
+ // the goal is to follow with what I said above for each group, and instead of a true false map of ids, we can set an array full of those the app has already sent pkmsgs
826
+ senderKeyRecipients.push(deviceJid)
916
827
  senderKeyMap[deviceJid] = true
917
828
  }
918
829
  }
919
-
920
- // if there are some participants with whom the session has not been established
921
- // if there are, we re-send the senderkey
922
- if (senderKeyJids.length) {
923
- logger.debug({ senderKeyJids }, 'sending new sender key')
830
+
831
+ if (senderKeyRecipients.length) {
832
+ logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending new sender key')
833
+
924
834
  const senderKeyMsg = {
925
835
  senderKeyDistributionMessage: {
926
836
  axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage,
927
837
  groupId: destinationJid
928
838
  }
929
839
  }
930
-
931
- await assertSessions(senderKeyJids, false)
932
-
933
- const result = await createParticipantNodes(senderKeyJids, senderKeyMsg, extraAttrs)
934
-
935
- shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
936
-
840
+
841
+ const senderKeySessionTargets = senderKeyRecipients
842
+
843
+ await assertSessions(senderKeySessionTargets)
844
+
845
+ const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, extraAttrs)
846
+
847
+ shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity;
937
848
  participants.push(...result.nodes)
938
849
  }
939
-
850
+
940
851
  binaryNodeContent.push({
941
852
  tag: 'enc',
942
- attrs: { v: '2', type: 'skmsg', ...extraAttrs },
853
+ attrs: { v: '2', type: 'skmsg', ...extraAttrs },
943
854
  content: ciphertext
944
855
  })
945
-
856
+
946
857
  await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } })
947
858
  }
948
859
 
949
- else if (isNewsletter) {
950
- // Message edit
951
- if (message.protocolMessage?.editedMessage) {
952
- msgId = message.protocolMessage.key?.id
953
- message = message.protocolMessage.editedMessage
860
+ else {
861
+ // ADDRESSING CONSISTENCY: Match own identity to conversation context
862
+ // TODO: investigate if this is true
863
+ let ownId = meId
864
+
865
+ if (isLid && meLid) {
866
+ ownId = meLid;
867
+ logger.debug({ to: jid, ownId }, 'Using LID identity for @lid conversation')
954
868
  }
955
-
956
- // Message delete
957
- if (message.protocolMessage?.type === proto.Message.ProtocolMessage.Type.REVOKE) {
958
- msgId = message.protocolMessage.key?.id
959
- message = {}
869
+
870
+ else {
871
+ logger.debug({ to: jid, ownId }, 'Using PN identity for @s.whatsapp.net conversation')
960
872
  }
961
-
962
- const patched = await patchMessageBeforeSending(message, [])
963
- const bytes = encodeNewsletterMessage(patched)
964
-
965
- binaryNodeContent.push({
966
- tag: 'plaintext',
967
- attrs: extraAttrs,
968
- content: bytes
969
- })
970
- }
971
-
972
- else {
873
+
973
874
  const { user: ownUser } = jidDecode(ownId)
974
-
975
- if (!participant) {
875
+
876
+ if (!isRetryResend) {
976
877
  const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
878
+
977
879
  devices.push({
978
880
  user,
979
881
  device: 0,
980
- wireJid: jidEncode(user, targetUserServer, 0)
882
+ jid: jidEncode(user, targetUserServer, 0) // rajeh, todo: this entire logic is convoluted and weird.
981
883
  })
982
884
 
983
- // Own user matches conversation addressing mode
984
885
  if (user !== ownUser) {
985
- const ownUserServer = isLid ? 'lid' : 's.whatsapp.net';
886
+ const ownUserServer = isLid ? 'lid' : 's.whatsapp.net'
986
887
  const ownUserForAddressing = isLid && meLid ? jidDecode(meLid).user : jidDecode(meId).user
888
+
987
889
  devices.push({
988
890
  user: ownUserForAddressing,
989
891
  device: 0,
990
- wireJid: jidEncode(ownUserForAddressing, ownUserServer, 0)
892
+ jid: jidEncode(ownUserForAddressing, ownUserServer, 0)
991
893
  })
992
894
  }
993
-
895
+
994
896
  if (additionalAttributes?.['category'] !== 'peer') {
995
897
  // Clear placeholders and enumerate actual devices
996
898
  devices.length = 0
@@ -999,89 +901,116 @@ const makeMessagesSocket = (config) => {
999
901
  const senderIdentity = isLid && meLid
1000
902
  ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined)
1001
903
  : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
1002
-
1003
904
  // Enumerate devices for sender and target with consistent addressing
1004
- const sessionDevices = await getUSyncDevices([senderIdentity, jid], false, false)
905
+ const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
906
+
1005
907
  devices.push(...sessionDevices)
908
+
1006
909
  logger.debug({
1007
910
  deviceCount: devices.length,
1008
- devices: devices.map(d => `${d.user}:${d.device}@${jidDecode(d.wireJid)?.server}`)
911
+ devices: devices.map(d => `${d.user}:${d.device}@${jidDecode(d.jid)?.server}`)
1009
912
  }, 'Device enumeration complete with unified addressing')
1010
913
  }
1011
914
  }
1012
-
1013
- const allJids = []
1014
- const meJids = []
1015
- const otherJids = []
915
+
916
+ const allRecipients = []
917
+ const meRecipients = []
918
+ const otherRecipients = []
1016
919
 
1017
920
  const { user: mePnUser } = jidDecode(meId)
1018
921
  const { user: meLidUser } = meLid ? jidDecode(meLid) : { user: null }
1019
-
1020
- for (const { user, wireJid } of devices) {
1021
- const isExactSenderDevice = wireJid === meId || (meLid && wireJid === meLid)
922
+
923
+ for (const { user, jid } of devices) {
924
+ const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
925
+
1022
926
  if (isExactSenderDevice) {
1023
- logger.debug({ wireJid, meId, meLid }, 'Skipping exact sender device (whatsmeow pattern)')
927
+ logger.debug({ jid, meId, meLid }, 'Skipping exact sender device (whatsmeow pattern)')
1024
928
  continue
1025
929
  }
1026
930
 
1027
931
  // Check if this is our device (could match either PN or LID user)
1028
- const isMe = user === mePnUser || (meLidUser && user === meLidUser)
1029
- const jid = wireJid
1030
-
932
+ const isMe = user === mePnUser || user === meLidUser
933
+
1031
934
  if (isMe) {
1032
- meJids.push(jid)
935
+ meRecipients.push(jid);
1033
936
  }
1034
-
937
+
1035
938
  else {
1036
- otherJids.push(jid)
939
+ otherRecipients.push(jid)
1037
940
  }
1038
-
1039
- allJids.push(jid)
941
+
942
+ allRecipients.push(jid)
1040
943
  }
1041
-
1042
- await assertSessions([...otherJids, ...meJids], false)
1043
-
944
+
945
+ await assertSessions(allRecipients)
946
+
1044
947
  const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
1045
948
  // For own devices: use DSM if available (1:1 chats only)
1046
- createParticipantNodes(meJids, meMsg || message, extraAttrs),
1047
- createParticipantNodes(otherJids, message, extraAttrs, meMsg)
949
+ createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
950
+ createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
1048
951
  ])
1049
-
952
+
1050
953
  participants.push(...meNodes)
1051
-
1052
954
  participants.push(...otherNodes)
1053
955
 
1054
- if (meJids.length > 0 || otherJids.length > 0) {
1055
- extraAttrs['phash'] = generateParticipantHashV2([...meJids, ...otherJids])
956
+ if (meRecipients.length > 0 || otherRecipients.length > 0) {
957
+ extraAttrs['phash'] = generateParticipantHashV2([...meRecipients, ...otherRecipients])
1056
958
  }
1057
-
959
+
1058
960
  shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
1059
961
  }
1060
-
962
+
963
+ if (isRetryResend) {
964
+ const isParticipantLid = isLidUser(participant.jid)
965
+ const isMe = areJidsSameUser(participant.jid, isParticipantLid ? meLid : meId)
966
+ const encodedMessageToSend = isMe
967
+ ? encodeWAMessage({
968
+ deviceSentMessage: {
969
+ destinationJid,
970
+ message
971
+ }
972
+ })
973
+ : encodeWAMessage(message)
974
+ const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({
975
+ data: encodedMessageToSend,
976
+ jid: participant.jid
977
+ })
978
+
979
+ binaryNodeContent.push({
980
+ tag: 'enc',
981
+ attrs: {
982
+ v: '2',
983
+ type,
984
+ count: participant.count.toString()
985
+ },
986
+ content: encryptedContent
987
+ })
988
+ }
989
+
1061
990
  if (participants.length) {
1062
991
  if (additionalAttributes?.['category'] === 'peer') {
1063
992
  const peerNode = participants[0]?.content?.[0]
1064
-
993
+
1065
994
  if (peerNode) {
1066
995
  binaryNodeContent.push(peerNode) // push only enc
1067
996
  }
1068
997
  }
1069
-
998
+
1070
999
  else {
1071
1000
  binaryNodeContent.push({
1072
1001
  tag: 'participants',
1073
1002
  attrs: {},
1074
1003
  content: participants
1075
- })
1004
+ });
1076
1005
  }
1077
1006
  }
1078
-
1007
+
1079
1008
  const stanza = {
1080
1009
  tag: 'message',
1081
1010
  attrs: {
1011
+ id: msgId,
1082
1012
  to: destinationJid,
1083
- id: msgId,
1084
- type: getTypeMessage(message),
1013
+ type: getTypeMessage(message),
1085
1014
  ...(additionalAttributes || {})
1086
1015
  },
1087
1016
  content: binaryNodeContent
@@ -1095,30 +1024,41 @@ const makeMessagesSocket = (config) => {
1095
1024
  stanza.attrs.to = destinationJid
1096
1025
  stanza.attrs.participant = participant.jid
1097
1026
  }
1098
-
1027
+
1099
1028
  else if (areJidsSameUser(participant.jid, meId)) {
1100
1029
  stanza.attrs.to = participant.jid
1101
1030
  stanza.attrs.recipient = destinationJid
1102
1031
  }
1103
-
1032
+
1104
1033
  else {
1105
1034
  stanza.attrs.to = participant.jid
1106
1035
  }
1107
1036
  }
1108
-
1037
+
1109
1038
  else {
1110
1039
  stanza.attrs.to = destinationJid
1111
1040
  }
1112
-
1041
+
1113
1042
  if (shouldIncludeDeviceIdentity) {
1114
1043
  stanza.content.push({
1115
1044
  tag: 'device-identity',
1116
1045
  attrs: {},
1117
1046
  content: encodeSignedDeviceIdentity(authState.creds.account, true)
1118
1047
  })
1119
-
1048
+
1120
1049
  logger.debug({ jid }, 'adding device identity')
1121
1050
  }
1051
+
1052
+ const contactTcTokenData = !isGroup && !isRetryResend && !isStatus ? await authState.keys.get('tctoken', [destinationJid]) : {}
1053
+ const tcTokenBuffer = contactTcTokenData[destinationJid]?.token
1054
+
1055
+ if (tcTokenBuffer) {
1056
+ stanza.content.push({
1057
+ tag: 'tctoken',
1058
+ attrs: {},
1059
+ content: tcTokenBuffer
1060
+ })
1061
+ }
1122
1062
 
1123
1063
  if (isGroup && regexGroupOld.test(jid) && !message.reactionMessage) {
1124
1064
  stanza.content.push({
@@ -1449,11 +1389,11 @@ const makeMessagesSocket = (config) => {
1449
1389
  ...suki,
1450
1390
  getPrivacyTokens,
1451
1391
  assertSessions,
1392
+ profilePictureUrl,
1452
1393
  relayMessage,
1453
1394
  sendReceipt,
1454
1395
  sendReceipts,
1455
1396
  readMessages,
1456
- profilePictureUrl,
1457
1397
  getUSyncDevices,
1458
1398
  refreshMediaConn,
1459
1399
  waUploadToServer,
@@ -1462,57 +1402,59 @@ const makeMessagesSocket = (config) => {
1462
1402
  messageRetryManager,
1463
1403
  createParticipantNodes,
1464
1404
  sendPeerDataOperationMessage,
1405
+ updateMemberLabel,
1465
1406
  updateMediaMessage: async (message) => {
1466
1407
  const content = assertMediaContent(message.message)
1467
1408
  const mediaKey = content.mediaKey
1468
1409
  const meId = authState.creds.me.id
1469
1410
  const node = await encryptMediaRetryRequest(message.key, mediaKey, meId)
1411
+
1470
1412
  let error = undefined
1471
-
1413
+
1472
1414
  await Promise.all([
1473
1415
  sendNode(node),
1474
1416
  waitForMsgMediaUpdate(async (update) => {
1475
1417
  const result = update.find(c => c.key.id === message.key.id)
1418
+
1476
1419
  if (result) {
1477
1420
  if (result.error) {
1478
1421
  error = result.error
1479
1422
  }
1480
-
1423
+
1481
1424
  else {
1482
1425
  try {
1483
1426
  const media = await decryptMediaRetryData(result.media, mediaKey, result.key.id)
1484
-
1427
+
1485
1428
  if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) {
1486
1429
  const resultStr = proto.MediaRetryNotification.ResultType[media.result]
1487
-
1488
- throw new Boom(`Media re-upload failed by device (${resultStr})`, { data: media, statusCode: getStatusCodeForMediaRetry(media.result) || 404 })
1430
+
1431
+ throw new Boom(`Media re-upload failed by device (${resultStr})`, {
1432
+ data: media,
1433
+ statusCode: getStatusCodeForMediaRetry(media.result) || 404
1434
+ })
1489
1435
  }
1490
-
1436
+
1491
1437
  content.directPath = media.directPath
1492
-
1493
1438
  content.url = getUrlFromDirectPath(content.directPath)
1494
-
1495
1439
  logger.debug({ directPath: media.directPath, key: result.key }, 'media update successful')
1496
1440
  }
1497
-
1441
+
1498
1442
  catch (err) {
1499
1443
  error = err
1500
1444
  }
1501
1445
  }
1502
-
1446
+
1503
1447
  return true
1504
1448
  }
1505
1449
  })
1506
1450
  ])
1507
-
1451
+
1508
1452
  if (error) {
1509
1453
  throw error
1510
1454
  }
1511
-
1512
- ev.emit('messages.update', [
1513
- { key: message.key, update: { message: message.message } }
1514
- ])
1515
-
1455
+
1456
+ ev.emit('messages.update', [{ key: message.key, update: { message: message.message } }])
1457
+
1516
1458
  return message
1517
1459
  },
1518
1460
  sendStatusMentions: async (content, jids = []) => {
@@ -1522,7 +1464,7 @@ const makeMessagesSocket = (config) => {
1522
1464
 
1523
1465
  for (const id of jids) {
1524
1466
  const isGroup = isJidGroup(id)
1525
- const isPrivate = isJidUser(id)
1467
+ const isPrivate = isPnUser(id)
1526
1468
 
1527
1469
  if (isGroup) {
1528
1470
  try {
@@ -1578,7 +1520,10 @@ const makeMessagesSocket = (config) => {
1578
1520
  userJid,
1579
1521
  getUrlInfo: text => getUrlInfo(text, {
1580
1522
  thumbnailWidth: linkPreviewImageThumbnailWidth,
1581
- fetchOpts: { timeout: 3000, ...axiosOptions || {} },
1523
+ fetchOpts: {
1524
+ timeout: 3000,
1525
+ ...(httpRequestOptions || {})
1526
+ },
1582
1527
  logger,
1583
1528
  uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined
1584
1529
  }),
@@ -1621,7 +1566,7 @@ const makeMessagesSocket = (config) => {
1621
1566
  for (const id of jids) {
1622
1567
  try {
1623
1568
  const normalizedId = jidNormalizedUser(id)
1624
- const isPrivate = isJidUser(normalizedId)
1569
+ const isPrivate = isPnUser(normalizedId)
1625
1570
  const type = isPrivate ? 'statusMentionMessage' : 'groupStatusMentionMessage'
1626
1571
 
1627
1572
  const protocolMessage = {
@@ -1716,8 +1661,8 @@ const makeMessagesSocket = (config) => {
1716
1661
  getUrlInfo: text => getUrlInfo(text, {
1717
1662
  thumbnailWidth: linkPreviewImageThumbnailWidth,
1718
1663
  fetchOpts: {
1719
- timeout: 3000,
1720
- ...axiosOptions || {}
1664
+ timeout: 3000,
1665
+ ...(httpRequestOptions || {})
1721
1666
  },
1722
1667
  logger,
1723
1668
  uploadImage: generateHighQualityLinkPreview
@@ -1772,8 +1717,8 @@ const makeMessagesSocket = (config) => {
1772
1717
  await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, additionalAttributes, statusJidList: options.statusJidList, additionalNodes: options.additionalNodes, AI: options.ai })
1773
1718
 
1774
1719
  if (config.emitOwnEvents) {
1775
- process.nextTick(() => {
1776
- processingMutex.mutex(() => (upsertMessage(fullMsg, 'append')))
1720
+ process.nextTick(async () => {
1721
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'))
1777
1722
  })
1778
1723
  }
1779
1724