@badzz88/baileys 8.4.9 → 8.5.1

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 (154) hide show
  1. package/README.md +1 -1
  2. package/WAProto/WAProto.proto +6 -347
  3. package/WAProto/fix-imports.js +85 -86
  4. package/WAProto/index.d.ts +25 -4913
  5. package/WAProto/index.js +93 -14069
  6. package/{src → lib}/Defaults/index.js +2 -13
  7. package/{src → lib}/Signal/Group/group_cipher.js +5 -5
  8. package/{src → lib}/Signal/Group/keyhelper.js +4 -4
  9. package/{src → lib}/Signal/Group/sender-chain-key.js +2 -2
  10. package/{src → lib}/Signal/Group/sender-key-distribution-message.js +1 -1
  11. package/{src → lib}/Signal/Group/sender-key-message.js +3 -3
  12. package/{src → lib}/Signal/Group/sender-key-record.js +1 -1
  13. package/{src → lib}/Signal/Group/sender-message-key.js +2 -2
  14. package/{src → lib}/Signal/libsignal.js +26 -6
  15. package/{src → lib}/Socket/chats.js +58 -616
  16. package/{src → lib}/Socket/communities.js +28 -59
  17. package/{src → lib}/Socket/graphql.js +103 -362
  18. package/{src → lib}/Socket/groups.js +29 -77
  19. package/{src → lib}/Socket/index.js +5 -3
  20. package/{src → lib}/Socket/interactive-handler.js +60 -117
  21. package/{src → lib}/Socket/interop.js +19 -36
  22. package/lib/Socket/luxu.js +387 -0
  23. package/{src → lib}/Socket/managed-account.js +27 -58
  24. package/{src → lib}/Socket/messages-recv.js +144 -715
  25. package/{src → lib}/Socket/messages-send.js +102 -412
  26. package/lib/Socket/mex.js +60 -0
  27. package/{src → lib}/Socket/newsletter.js +23 -266
  28. package/{src → lib}/Socket/privacy.js +38 -41
  29. package/{src → lib}/Socket/registration.js +106 -113
  30. package/{src → lib}/Socket/socket.js +101 -88
  31. package/{src → lib}/Socket/text-router.js +17 -1
  32. package/lib/Socket/username.js +243 -0
  33. package/lib/Store/keyed-db.js +108 -0
  34. package/{src → lib}/Store/make-in-memory-store.js +104 -6
  35. package/lib/Types/Mex.js +112 -0
  36. package/{src → lib}/Types/Newsletter.js +41 -72
  37. package/{src → lib}/Types/index.js +1 -0
  38. package/{src → lib}/Utils/chat-utils.js +9 -73
  39. package/lib/Utils/command-loader.d.ts +31 -0
  40. package/lib/Utils/command-loader.js +100 -0
  41. package/lib/Utils/companion-reg-client-utils.js +42 -0
  42. package/lib/Utils/crypto.js +155 -0
  43. package/lib/Utils/curve25519-js.js +275 -0
  44. package/{src → lib}/Utils/decode-wa-message.js +36 -284
  45. package/{src → lib}/Utils/event-buffer.js +27 -6
  46. package/{src → lib}/Utils/generics.js +37 -114
  47. package/{src → lib}/Utils/index.js +5 -1
  48. package/lib/Utils/lt-hash.js +39 -0
  49. package/{src → lib}/Utils/message-retry-manager.js +39 -0
  50. package/{src → lib}/Utils/messages-media.js +58 -105
  51. package/{src → lib}/Utils/messages.js +529 -115
  52. package/lib/Utils/meta-ai-msmsg.js +262 -0
  53. package/lib/Utils/native-bridge.js +82 -0
  54. package/{src → lib}/Utils/noise-handler.js +15 -13
  55. package/{src → lib}/Utils/process-message.js +55 -54
  56. package/{src → lib}/Utils/reporting-utils.js +2 -2
  57. package/lib/Utils/session-pool.d.ts +16 -0
  58. package/lib/Utils/session-pool.js +94 -0
  59. package/{src → lib}/Utils/signal.js +51 -19
  60. package/{src → lib}/Utils/stanza-ack.js +2 -36
  61. package/lib/Utils/sticker.d.ts +17 -0
  62. package/lib/Utils/sticker.js +145 -0
  63. package/{src → lib}/Utils/tc-token-utils.js +1 -1
  64. package/{src → lib}/Utils/validate-connection.js +11 -40
  65. package/lib/Utils/view-once-cache.d.ts +9 -0
  66. package/lib/Utils/view-once-cache.js +79 -0
  67. package/{src → lib}/WABinary/decode.js +4 -38
  68. package/lib/WABinary/encode.js +228 -0
  69. package/{src → lib}/WABinary/generic-utils.js +93 -1
  70. package/{src → lib}/WABinary/jid-utils.js +3 -16
  71. package/{src → lib}/WAM/encode.js +1 -1
  72. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +43 -0
  73. package/{src → lib}/WAUSync/Protocols/USyncContactProtocol.js +1 -7
  74. package/{src → lib}/WAUSync/Protocols/USyncDeviceProtocol.js +5 -13
  75. package/{src → lib}/WAUSync/Protocols/USyncDisappearingModeProtocol.js +4 -1
  76. package/{src → lib}/WAUSync/Protocols/USyncFeatureProtocol.js +2 -17
  77. package/{src → lib}/WAUSync/Protocols/USyncStatusProtocol.js +2 -7
  78. package/{src → lib}/WAUSync/Protocols/USyncUsernameProtocol.js +1 -2
  79. package/{src → lib}/WAUSync/Protocols/index.js +0 -1
  80. package/{src → lib}/WAUSync/USyncQuery.js +6 -77
  81. package/{src → lib}/WAUSync/USyncUser.js +8 -16
  82. package/{src → lib}/WAUSync/index.js +0 -1
  83. package/{src → lib}/antiban.js +266 -355
  84. package/{src → lib}/index.js +2 -2
  85. package/package.json +19 -16
  86. package/WAProto/fix-import.js +0 -38
  87. package/src/Socket/mex.js +0 -47
  88. package/src/Socket/username.js +0 -234
  89. package/src/Utils/crypto.js +0 -125
  90. package/src/Utils/lt-hash.js +0 -10
  91. package/src/Utils/message-inspect.js +0 -400
  92. package/src/Utils/meta-ai-msmsg.js +0 -133
  93. package/src/WABinary/encode.js +0 -58
  94. package/src/WAUSync/Protocols/USyncBusinessProtocol.js +0 -100
  95. package/src/WAUSync/USyncBackoff.js +0 -31
  96. /package/{src → lib}/Defaults/phonenumber-mcc.json +0 -0
  97. /package/{src → lib}/Signal/Group/ciphertext-message.js +0 -0
  98. /package/{src → lib}/Signal/Group/group-session-builder.js +0 -0
  99. /package/{src → lib}/Signal/Group/index.js +0 -0
  100. /package/{src → lib}/Signal/Group/sender-key-name.js +0 -0
  101. /package/{src → lib}/Signal/Group/sender-key-state.js +0 -0
  102. /package/{src → lib}/Signal/lid-mapping.js +0 -0
  103. /package/{src → lib}/Socket/Client/index.js +0 -0
  104. /package/{src → lib}/Socket/Client/types.js +0 -0
  105. /package/{src → lib}/Socket/Client/websocket.js +0 -0
  106. /package/{src → lib}/Socket/aigroups.js +0 -0
  107. /package/{src → lib}/Socket/business.js +0 -0
  108. /package/{src → lib}/Store/index.js +0 -0
  109. /package/{src → lib}/Store/make-cache-manager-store.js +0 -0
  110. /package/{src → lib}/Store/make-ordered-dictionary.js +0 -0
  111. /package/{src → lib}/Store/object-repository.js +0 -0
  112. /package/{src → lib}/Types/Auth.js +0 -0
  113. /package/{src → lib}/Types/Bussines.js +0 -0
  114. /package/{src → lib}/Types/Call.js +0 -0
  115. /package/{src → lib}/Types/Chat.js +0 -0
  116. /package/{src → lib}/Types/Contact.js +0 -0
  117. /package/{src → lib}/Types/Events.js +0 -0
  118. /package/{src → lib}/Types/GroupMetadata.js +0 -0
  119. /package/{src → lib}/Types/Label.js +0 -0
  120. /package/{src → lib}/Types/LabelAssociation.js +0 -0
  121. /package/{src → lib}/Types/Message.js +0 -0
  122. /package/{src → lib}/Types/Product.js +0 -0
  123. /package/{src → lib}/Types/Signal.js +0 -0
  124. /package/{src → lib}/Types/Socket.js +0 -0
  125. /package/{src → lib}/Types/State.js +0 -0
  126. /package/{src → lib}/Types/USync.js +0 -0
  127. /package/{src → lib}/Utils/auth-utils.js +0 -0
  128. /package/{src → lib}/Utils/browser-utils.js +0 -0
  129. /package/{src → lib}/Utils/business.js +0 -0
  130. /package/{src → lib}/Utils/consumer-application.js +0 -0
  131. /package/{src → lib}/Utils/group-history.js +0 -0
  132. /package/{src → lib}/Utils/history.js +0 -0
  133. /package/{src → lib}/Utils/identity-change-handler.js +0 -0
  134. /package/{src → lib}/Utils/jid-display-normalization.js +0 -0
  135. /package/{src → lib}/Utils/link-preview.js +0 -0
  136. /package/{src → lib}/Utils/logger.js +0 -0
  137. /package/{src → lib}/Utils/make-mutex.js +0 -0
  138. /package/{src → lib}/Utils/message-composer.js +0 -0
  139. /package/{src → lib}/Utils/offline-node-processor.js +0 -0
  140. /package/{src → lib}/Utils/pre-key-manager.js +0 -0
  141. /package/{src → lib}/Utils/sync-action-utils.js +0 -0
  142. /package/{src → lib}/Utils/use-multi-file-auth-state.js +0 -0
  143. /package/{src → lib}/Utils/voip-rekey.js +0 -0
  144. /package/{src → lib}/WABinary/constants.js +0 -0
  145. /package/{src → lib}/WABinary/index.js +0 -0
  146. /package/{src → lib}/WABinary/types.js +0 -0
  147. /package/{src → lib}/WAM/BinaryInfo.js +0 -0
  148. /package/{src → lib}/WAM/constants.js +0 -0
  149. /package/{src → lib}/WAM/index.js +0 -0
  150. /package/{src → lib}/WAUSync/Protocols/USyncBotProfileProtocol.js +0 -0
  151. /package/{src → lib}/WAUSync/Protocols/USyncLIDProtocol.js +0 -0
  152. /package/{src → lib}/WAUSync/Protocols/USyncPictureProtocol.js +0 -0
  153. /package/{src → lib}/WAUSync/Protocols/USyncSidelistProtocol.js +0 -0
  154. /package/{src → lib}/WAUSync/Protocols/USyncTextStatusProtocol.js +0 -0
@@ -37,6 +37,7 @@ const makeMessagesRecvSocket = config => {
37
37
  signalRepository,
38
38
  query,
39
39
  upsertMessage,
40
+ getUSyncDevices,
40
41
  resyncAppState,
41
42
  onUnexpectedError,
42
43
  assertSessions,
@@ -46,10 +47,7 @@ const makeMessagesRecvSocket = config => {
46
47
  uploadPreKeys,
47
48
  sendPeerDataOperationMessage,
48
49
  messageRetryManager,
49
- issuePrivacyTokens,
50
- getUSyncDevices,
51
- createParticipantNodes,
52
- newsletterServerIdCache
50
+ issuePrivacyTokens
53
51
  } = sock
54
52
  const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
55
53
  // Track when the socket fully opens so pending pre-connect messages are treated as history
@@ -147,14 +145,13 @@ const makeMessagesRecvSocket = config => {
147
145
  *
148
146
  * @param {string} [registrationTraceId] - Optional trace ID for this registration attempt.
149
147
  */
150
- const requestCompanionCanonicalNonce = async registrationTraceId => {
148
+ const requestCompanionCanonicalNonce = async (registrationTraceId) => {
151
149
  if (!authState.creds.me?.id) {
152
150
  throw new boom_1.Boom('Not authenticated')
153
151
  }
154
152
  return sendPeerDataOperationMessage({
155
153
  companionCanonicalUserNonceFetchRequest: registrationTraceId ? { registrationTraceId } : {},
156
- peerDataOperationRequestType:
157
- index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_CANONICAL_USER_NONCE_FETCH
154
+ peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_CANONICAL_USER_NONCE_FETCH
158
155
  })
159
156
  }
160
157
  /**
@@ -170,135 +167,54 @@ const makeMessagesRecvSocket = config => {
170
167
  peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_META_NONCE_FETCH
171
168
  })
172
169
  }
173
- // Handles mex notifications (newsletter and group XWA2 property updates).
174
- // Wire format: notification[type=mex] → <update op_name="..."> → JSON payload in content bytes.
175
- const handleMexNotification = async node => {
176
- const updateNodes = (0, WABinary_1.getBinaryNodeChildren)(node, 'update')
177
- if (!updateNodes.length) {
178
- logger.debug({ node }, 'mex notification with no update children')
170
+ // Handles mex newsletter notifications
171
+ const handleMexNewsletterNotification = async node => {
172
+ const mexNode = (0, WABinary_1.getBinaryNodeChild)(node, 'mex')
173
+ if (!mexNode?.content) {
174
+ logger.warn({ node }, 'Invalid mex newsletter notification')
179
175
  return
180
176
  }
181
- for (const updateNode of updateNodes) {
182
- const opName = updateNode.attrs?.op_name
183
- if (!opName) continue
184
- let payload
185
- try {
186
- const raw = updateNode.content?.text ?? updateNode.content?.toString?.()
187
- payload = raw ? JSON.parse(raw) : null
188
- } catch (e) {
189
- logger.error({ err: e, opName }, 'failed to parse mex update payload')
190
- continue
191
- }
192
- if (!payload?.data) {
193
- logger.debug({ opName }, 'mex update with no data field')
194
- continue
195
- }
196
- const d = payload.data
197
- switch (opName) {
198
- case 'NotificationNewsletterUpdate': {
199
- // d.xwa2_notify_newsletter_on_metadata_update: { id, thread_metadata: { settings } }
200
- const upd = d.xwa2_notify_newsletter_on_metadata_update
201
- if (upd?.id) {
202
- ev.emit('newsletter-settings.update', {
203
- id: upd.id,
204
- update: upd.thread_metadata?.settings ?? {}
205
- })
206
- }
207
- break
208
- }
209
- case 'NotificationNewsletterJoin': {
210
- // d.xwa2_notify_newsletter_on_join: full newsletter metadata on subscribe
211
- const upd = d.xwa2_notify_newsletter_on_join
212
- if (upd?.id) {
213
- ev.emit('newsletter-participants.update', {
214
- id: upd.id,
215
- author: node.attrs.from,
216
- user: (0, WABinary_1.jidNormalizedUser)(node.attrs.from),
217
- new_role: upd.viewer_metadata?.role ?? 'SUBSCRIBER',
218
- action: 'join',
219
- metadata: upd.thread_metadata
220
- })
221
- }
222
- break
223
- }
224
- case 'NotificationNewsletterMuteChange': {
225
- // d.xwa2_notify_newsletter_on_mute_change: { id, mute: "ON"|"OFF" }
226
- const upd = d.xwa2_notify_newsletter_on_mute_change
227
- if (upd?.id) {
228
- ev.emit('newsletter-settings.update', {
229
- id: upd.id,
230
- update: { mute: upd.mute }
231
- })
232
- }
233
- break
234
- }
235
- case 'NotificationNewsletterUserSettingChange': {
236
- // d.xwa2_notify_newsletter_on_user_setting_change: { id, setting: { type, value } }
237
- const upd = d.xwa2_notify_newsletter_on_user_setting_change
238
- if (upd?.id && upd.setting) {
177
+ let data
178
+ try {
179
+ data = JSON.parse(mexNode.content.toString())
180
+ } catch (error) {
181
+ logger.error({ err: error, node }, 'Failed to parse mex newsletter notification')
182
+ return
183
+ }
184
+ const operation = data?.operation
185
+ const updates = data?.updates
186
+ if (!updates || !operation) {
187
+ logger.warn({ data }, 'Invalid mex newsletter notification content')
188
+ return
189
+ }
190
+ logger.info({ operation, updates }, 'got mex newsletter notification')
191
+ switch (operation) {
192
+ case 'NotificationNewsletterUpdate':
193
+ for (const update of updates) {
194
+ if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
239
195
  ev.emit('newsletter-settings.update', {
240
- id: upd.id,
241
- update: { userSetting: upd.setting }
196
+ id: update.jid,
197
+ update: update.settings
242
198
  })
243
199
  }
244
- break
245
200
  }
246
- case 'NotificationNewsletterAdminPromote': {
247
- // legacy format kept for compat
248
- const upd = d.xwa2_notify_newsletter_on_admin_promote
249
- if (upd?.id) {
201
+ break
202
+ case 'NotificationNewsletterAdminPromote':
203
+ for (const update of updates) {
204
+ if (update.jid && update.user) {
250
205
  ev.emit('newsletter-participants.update', {
251
- id: upd.id,
206
+ id: update.jid,
252
207
  author: node.attrs.from,
253
- user: upd.user,
208
+ user: update.user,
254
209
  new_role: 'ADMIN',
255
210
  action: 'promote'
256
211
  })
257
212
  }
258
- break
259
- }
260
- case 'NotificationGroupMemberLinkPropertyUpdate': {
261
- // d.xwa2_notify_group_on_prop_change: { id, properties: { member_link_mode } }
262
- const upd = d.xwa2_notify_group_on_prop_change
263
- if (upd?.id && upd.properties?.member_link_mode !== undefined) {
264
- ev.emit('groups.update', [
265
- {
266
- id: upd.id,
267
- memberAddMode: upd.properties.member_link_mode
268
- }
269
- ])
270
- }
271
- break
272
- }
273
- case 'NotificationGroupLimitSharingPropertyUpdate': {
274
- // d.xwa2_notify_group_on_prop_change: { id, properties: { limit_sharing } }
275
- const upd = d.xwa2_notify_group_on_prop_change
276
- if (upd?.id && upd.properties?.limit_sharing !== undefined) {
277
- ev.emit('groups.update', [
278
- {
279
- id: upd.id,
280
- limitSharing: upd.properties.limit_sharing
281
- }
282
- ])
283
- }
284
- break
285
213
  }
286
- case 'NotificationGroupMemberShareGroupHistoryModePropertyUpdate': {
287
- // d.xwa2_notify_group_on_prop_change: { id, properties: { member_share_group_history_mode } }
288
- const upd = d.xwa2_notify_group_on_prop_change
289
- if (upd?.id && upd.properties?.member_share_group_history_mode !== undefined) {
290
- ev.emit('groups.update', [
291
- {
292
- id: upd.id,
293
- memberShareHistoryMode: upd.properties.member_share_group_history_mode
294
- }
295
- ])
296
- }
297
- break
298
- }
299
- default:
300
- logger.debug({ opName, from: node.attrs.from }, 'unhandled mex op')
301
- }
214
+ break
215
+ default:
216
+ logger.info({ operation, data }, 'Unhandled mex newsletter notification')
217
+ break
302
218
  }
303
219
  }
304
220
  // Handles newsletter notifications
@@ -351,9 +267,7 @@ const makeMessagesRecvSocket = config => {
351
267
  })
352
268
  }
353
269
  break
354
- case 'message': {
355
- const viewCount = child.attrs.view_count !== undefined ? +child.attrs.view_count : undefined
356
- const impressionCount = child.attrs.impression_count !== undefined ? +child.attrs.impression_count : undefined
270
+ case 'message':
357
271
  const plaintextNode = (0, WABinary_1.getBinaryNodeChild)(child, 'plaintext')
358
272
  if (plaintextNode?.content) {
359
273
  try {
@@ -366,16 +280,11 @@ const makeMessagesRecvSocket = config => {
366
280
  key: {
367
281
  remoteJid: from,
368
282
  id: child.attrs.message_id || child.attrs.server_id,
369
- // always false: this branch only runs for inbound plaintext newsletter posts,
370
- // which always originate from the channel, never from us
371
- fromMe: false
283
+ fromMe: false // TODO: is this really true though
372
284
  },
373
285
  message: messageProto,
374
286
  messageTimestamp: +child.attrs.t
375
287
  }).toJSON()
376
- // Attach insight counters when the server includes them
377
- if (viewCount !== undefined) fullMessage.views = viewCount
378
- if (impressionCount !== undefined) fullMessage.impressions = impressionCount
379
288
  await upsertMessage(fullMessage, 'append')
380
289
  logger.info('Processed plaintext newsletter message')
381
290
  } catch (error) {
@@ -383,44 +292,23 @@ const makeMessagesRecvSocket = config => {
383
292
  }
384
293
  }
385
294
  break
386
- }
387
- case 'live_updates': {
388
- // Live engagement updates: reactions + forwards per message.
389
- // Wire: <live_updates> → <messages t="..."> → <message server_id="...">
390
- // → <forwards_count count="N">, <reactions> → <reaction code="X" count="N">
391
- const messagesNode = (0, WABinary_1.getBinaryNodeChild)(child, 'messages')
392
- const msgTs = messagesNode?.attrs?.t ? +messagesNode.attrs.t : undefined
393
- for (const msgNode of (0, WABinary_1.getBinaryNodeChildren)(messagesNode ?? child, 'message')) {
394
- const serverId = msgNode.attrs.server_id
395
- const fwdNode = (0, WABinary_1.getBinaryNodeChild)(msgNode, 'forwards_count')
396
- const forwardsCount = fwdNode?.attrs?.count !== undefined ? +fwdNode.attrs.count : undefined
397
- const reactionsNode = (0, WABinary_1.getBinaryNodeChild)(msgNode, 'reactions')
398
- const reactions = (0, WABinary_1.getBinaryNodeChildren)(reactionsNode ?? msgNode, 'reaction').map(r => ({
399
- code: r.attrs.code,
400
- count: +r.attrs.count
401
- }))
402
- ev.emit('newsletter.live-update', {
403
- id: from,
404
- server_id: serverId,
405
- timestamp: msgTs,
406
- forwardsCount,
407
- reactions
408
- })
409
- }
295
+ case 'live_updates':
296
+ // Live view count / engagement update
297
+ const liveCount = parseInt(child.attrs.count || child.content?.toString() || '0', 10)
298
+ ev.emit('newsletter.live-update', {
299
+ id: from,
300
+ server_id: child.attrs.message_id || child.attrs.server_id,
301
+ liveViewers: liveCount,
302
+ timestamp: child.attrs.t ? +child.attrs.t : undefined
303
+ })
410
304
  break
411
- }
412
- case 'pin': {
413
- const pinnedServerId = child.attrs.message_id || child.attrs.server_id
414
- const isPinned = child.attrs.action !== 'unpin'
305
+ case 'pin':
415
306
  ev.emit('newsletter.pin', {
416
307
  id: from,
417
- server_id: pinnedServerId,
418
- pinned: isPinned
308
+ server_id: child.attrs.message_id || child.attrs.server_id,
309
+ pinned: child.attrs.action !== 'unpin'
419
310
  })
420
- // Sync the pinnedMessage field on the newsletter metadata object
421
- ev.emit('newsletters.update', [{ id: from, pinnedMessage: isPinned ? pinnedServerId : null }])
422
311
  break
423
- }
424
312
  case 'category':
425
313
  ev.emit('newsletter-settings.update', {
426
314
  id: from,
@@ -440,88 +328,6 @@ const makeMessagesRecvSocket = config => {
440
328
  break
441
329
  }
442
330
  }
443
- // Handles incoming <status> stanzas pushed by the server for newsletter posts.
444
- // These arrive on the CB:status channel (AB-gated by status_e2ee_recv_over_status_stanza).
445
- const handleNewsletterStatus = async node => {
446
- const { id, from, server_id, t, is_sender, offline, type, edit } = node.attrs
447
- const serverId = server_id ? +server_id : undefined
448
- const timestamp = t ? +t : undefined
449
- const isSender = is_sender === 'true'
450
- const offlineIndex = offline !== undefined ? +offline : undefined
451
- // Cache messageId → serverId so the bot can look up server_id when a message is quoted
452
- if (id && serverId != null) {
453
- newsletterServerIdCache?.set(id, serverId)
454
- }
455
-
456
- // Parse optional <meta> child: edit timestamps, interaction type, admin profile
457
- const metaNode = (0, WABinary_1.getBinaryNodeChild)(node, 'meta')
458
- const meta = metaNode
459
- ? {
460
- ...(metaNode.attrs.msg_edit_t ? { editedAt: +metaNode.attrs.msg_edit_t } : {}),
461
- ...(metaNode.attrs.original_msg_t ? { originalTimestamp: +metaNode.attrs.original_msg_t } : {}),
462
- ...(metaNode.attrs.interaction_type ? { interactionType: metaNode.attrs.interaction_type } : {}),
463
- ...(metaNode.attrs.parent_server_id ? { parentServerId: +metaNode.attrs.parent_server_id } : {}),
464
- ...(metaNode.attrs.response_server_id ? { responseServerId: +metaNode.attrs.response_server_id } : {})
465
- }
466
- : undefined
467
-
468
- // Parse engagement counters
469
- const viewsNode = (0, WABinary_1.getBinaryNodeChild)(node, 'views_count')
470
- const viewsCount = viewsNode?.attrs?.count !== undefined ? +viewsNode.attrs.count : undefined
471
- const responsesNode = (0, WABinary_1.getBinaryNodeChild)(node, 'responses_count')
472
- const responsesCount = responsesNode?.attrs?.count !== undefined ? +responsesNode.attrs.count : undefined
473
- const reactionsNode = (0, WABinary_1.getBinaryNodeChild)(node, 'reactions')
474
- const reactionCounts = (0, WABinary_1.getBinaryNodeChildren)(reactionsNode ?? { content: [] }, 'reaction').map(
475
- r => ({ code: r.attrs.code, count: +r.attrs.count })
476
- )
477
-
478
- let content = null
479
- let mediaType = undefined
480
-
481
- if (type === 'reaction') {
482
- const reactionNode = (0, WABinary_1.getBinaryNodeChild)(node, 'reaction')
483
- content = { type: 'reaction', code: reactionNode?.attrs?.code }
484
- } else if (type === 'text' || type === 'media') {
485
- const plaintextNode = (0, WABinary_1.getBinaryNodeChild)(node, 'plaintext')
486
- if (type === 'media') mediaType = plaintextNode?.attrs?.mediatype
487
- if (edit === '7' || edit === '8') {
488
- content = { type: 'revoke', edit }
489
- } else if (plaintextNode?.content) {
490
- try {
491
- const buf = Buffer.isBuffer(plaintextNode.content)
492
- ? plaintextNode.content
493
- : Buffer.from(plaintextNode.content)
494
- const message = index_js_1.proto.Message.decode(buf).toJSON()
495
- content = { type, message, ...(mediaType ? { mediaType } : {}) }
496
- } catch (err) {
497
- logger.error({ err }, 'Failed to decode newsletter status plaintext')
498
- content = { type, raw: true, ...(mediaType ? { mediaType } : {}) }
499
- }
500
- }
501
- }
502
-
503
- // Send ACK back to server
504
- const ackType = type === 'reaction' ? 'reaction' : edit ? 'revoke' : type
505
- await sendNode({
506
- tag: 'ack',
507
- attrs: { id, to: from, class: 'status', type: ackType || 'text' },
508
- content: undefined
509
- })
510
-
511
- ev.emit('newsletter.status', {
512
- id: from,
513
- messageId: id,
514
- serverId,
515
- timestamp,
516
- isSender,
517
- ...(offlineIndex !== undefined ? { offlineIndex } : {}),
518
- ...(meta ? { meta } : {}),
519
- ...(viewsCount !== undefined ? { viewsCount } : {}),
520
- ...(responsesCount !== undefined ? { responsesCount } : {}),
521
- ...(reactionCounts.length ? { reactionCounts } : {}),
522
- content
523
- })
524
- }
525
331
  const sendMessageAck = async (node, errorCode) => {
526
332
  const stanza = (0, stanza_ack_1.buildAckStanza)(node, errorCode, authState.creds.me.id)
527
333
  logger.debug({ recv: { tag: node.tag, attrs: node.attrs }, sent: stanza.attrs }, 'sent ack')
@@ -871,9 +677,8 @@ const makeMessagesRecvSocket = config => {
871
677
  id: node.attrs.id,
872
678
  t: node.attrs.t,
873
679
  v: '1',
874
- // Confirmed from live capture: real retry receipts carry error="1"
875
- // in the overwhelming majority of samples (1270/1273); "0" never observed.
876
- error: '1'
680
+ // ADD ERROR FIELD
681
+ error: '0'
877
682
  }
878
683
  },
879
684
  {
@@ -921,7 +726,7 @@ const makeMessagesRecvSocket = config => {
921
726
  const tcJid = await (0, tc_token_utils_1.resolveTcTokenJid)(normalizedJid, getLIDForPN)
922
727
  const tcTokenData = await authState.keys.get('tctoken', [tcJid])
923
728
  const senderTs = tcTokenData?.[tcJid]?.senderTimestamp
924
- if (senderTs == null || (0, tc_token_utils_1.isTcTokenExpired)(senderTs)) {
729
+ if (senderTs === null || senderTs === undefined || (0, tc_token_utils_1.isTcTokenExpired)(senderTs)) {
925
730
  return
926
731
  }
927
732
  logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken')
@@ -969,21 +774,8 @@ const makeMessagesRecvSocket = config => {
969
774
  }
970
775
  }
971
776
  }
972
- // Group participant nodes carry authoritative LID<->PN pairs (jid + phone_number/lid
973
- // attrs). Persist them so PN<->LID resolution elsewhere doesn't need a usync round trip.
974
- const storeParticipantLidPairs = participants => {
975
- const pairs = []
976
- for (const p of participants || []) {
977
- if (p.phoneNumber) pairs.push({ lid: p.id, pn: p.phoneNumber })
978
- else if (p.lid) pairs.push({ lid: p.lid, pn: p.id })
979
- }
980
- if (!pairs.length) return
981
- signalRepository.lidMapping
982
- .storeLIDPNMappings(pairs)
983
- .catch(error => logger.debug({ error }, 'failed to store LID/PN mappings from group notification'))
984
- }
985
777
  const handleGroupNotification = (fullNode, child, msg) => {
986
- // PN/LID: acting + affected participant already resolved below via participant_pn/lid attrs
778
+ // TODO: Support PN/LID (Here is only LID now)
987
779
  const actingParticipantLid = fullNode.attrs.participant
988
780
  const actingParticipantPn = fullNode.attrs.participant_pn
989
781
  const actingParticipantUsername = fullNode.attrs.participant_username
@@ -997,7 +789,6 @@ const makeMessagesRecvSocket = config => {
997
789
  msg.messageStubType = Types_1.WAMessageStubType.GROUP_CREATE
998
790
  msg.messageStubParameters = [metadata.subject]
999
791
  msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn }
1000
- storeParticipantLidPairs(metadata.participants)
1001
792
  ev.emit('chats.upsert', [
1002
793
  {
1003
794
  id: metadata.id,
@@ -1036,6 +827,7 @@ const makeMessagesRecvSocket = config => {
1036
827
  const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`
1037
828
  msg.messageStubType = Types_1.WAMessageStubType[stubType]
1038
829
  const participants = (0, WABinary_1.getBinaryNodeChildren)(child, 'participant').map(({ attrs }) => {
830
+ // TODO: Store LID MAPPINGS
1039
831
  return {
1040
832
  id: attrs.jid,
1041
833
  phoneNumber:
@@ -1044,8 +836,7 @@ const makeMessagesRecvSocket = config => {
1044
836
  : undefined,
1045
837
  lid: (0, WABinary_1.isPnUser)(attrs.jid) && (0, WABinary_1.isLidUser)(attrs.lid) ? attrs.lid : undefined,
1046
838
  username: attrs.participant_username || attrs.username || undefined,
1047
- admin: attrs.type || null,
1048
- uuid: attrs.uuid || attrs.participant_uuid || undefined
839
+ admin: attrs.type || null
1049
840
  }
1050
841
  })
1051
842
  if (
@@ -1058,7 +849,6 @@ const makeMessagesRecvSocket = config => {
1058
849
  ) {
1059
850
  msg.messageStubType = Types_1.WAMessageStubType.GROUP_PARTICIPANT_LEAVE
1060
851
  }
1061
- storeParticipantLidPairs(participants)
1062
852
  msg.messageStubParameters = participants.map(a => JSON.stringify(a))
1063
853
  break
1064
854
  case 'subject':
@@ -1108,51 +898,13 @@ const makeMessagesRecvSocket = config => {
1108
898
  break
1109
899
  case 'revoked_membership_requests':
1110
900
  const isDenied = (0, WABinary_1.areJidsSameUser)(affectedParticipantLid, actingParticipantLid)
1111
- if ((0, WABinary_1.isLidUser)(affectedParticipantLid) && (0, WABinary_1.isPnUser)(affectedParticipantPn)) {
1112
- storeParticipantLidPairs([{ id: affectedParticipantLid, phoneNumber: affectedParticipantPn }])
1113
- }
901
+ // TODO: LIDMAPPING SUPPORT
1114
902
  msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
1115
903
  msg.messageStubParameters = [
1116
904
  JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
1117
905
  isDenied ? 'revoked' : 'rejected'
1118
906
  ]
1119
907
  break
1120
- case 'sibling_link': {
1121
- const linkedGroupJid = (0, WABinary_1.getBinaryNodeChild)(child, 'group')?.attrs?.jid || child.attrs?.jid
1122
- ev.emit('groups.update', [
1123
- {
1124
- id: fullNode.attrs.from,
1125
- siblingGroupLinked: linkedGroupJid || true,
1126
- author: actingParticipantLid,
1127
- authorPn: actingParticipantPn
1128
- }
1129
- ])
1130
- break
1131
- }
1132
- case 'sibling_unlink': {
1133
- const unlinkedGroupJid = (0, WABinary_1.getBinaryNodeChild)(child, 'group')?.attrs?.jid || child.attrs?.jid
1134
- ev.emit('groups.update', [
1135
- {
1136
- id: fullNode.attrs.from,
1137
- siblingGroupUnlinked: unlinkedGroupJid || true,
1138
- author: actingParticipantLid,
1139
- authorPn: actingParticipantPn
1140
- }
1141
- ])
1142
- break
1143
- }
1144
- case 'clear_history': {
1145
- const historyClearTimestamp = child.attrs?.t ? +child.attrs.t : (0, Date)()
1146
- ev.emit('groups.update', [
1147
- {
1148
- id: fullNode.attrs.from,
1149
- historyClearTimestamp,
1150
- author: actingParticipantLid,
1151
- authorPn: actingParticipantPn
1152
- }
1153
- ])
1154
- break
1155
- }
1156
908
  }
1157
909
  }
1158
910
  const normalizeNotificationParticipant = async (jid, groupData) => {
@@ -1326,17 +1078,15 @@ const makeMessagesRecvSocket = config => {
1326
1078
  if (childTag === 'peer_device_presence') {
1327
1079
  const jid = attrs.jid || (0, WABinary_1.jidNormalizedUser)(node.attrs.from)
1328
1080
  const presence = attrs.type === 'unavailable' ? 'unavailable' : 'available'
1329
- const isIosInterop = !!authState.creds.interopIosEnabled
1330
- logger.debug({ jid, presence, isIosInterop }, '[interop] peer_device_presence')
1331
- ev.emit('presence.update', { id: jid, presences: { [jid]: { lastKnownPresence: presence } }, isIosInterop })
1081
+ logger.debug({ jid, presence }, '[interop] peer_device_presence')
1082
+ ev.emit('presence.update', { id: jid, presences: { [jid]: { lastKnownPresence: presence } } })
1332
1083
  return
1333
1084
  }
1334
1085
 
1335
1086
  // fbid:thread / fbid:devices — Meta thread or device list association
1336
1087
  if (childTag === 'fbid_thread' || childTag === 'fbid_devices') {
1337
- const isIosInterop = !!authState.creds.interopIosEnabled
1338
- logger.debug({ childTag, attrs, from: node.attrs.from, isIosInterop }, '[interop] fbid association update')
1339
- ev.emit('interop.fbid-update', { type: childTag, jid: node.attrs.from, attrs, isIosInterop })
1088
+ logger.debug({ childTag, attrs, from: node.attrs.from }, '[interop] fbid association update')
1089
+ ev.emit('interop.fbid-update', { type: childTag, jid: node.attrs.from, attrs })
1340
1090
  return
1341
1091
  }
1342
1092
 
@@ -1363,24 +1113,10 @@ const makeMessagesRecvSocket = config => {
1363
1113
  await handleNewsletterNotification(node)
1364
1114
  break
1365
1115
  case 'mex':
1366
- await handleMexNotification(node)
1116
+ await handleMexNewsletterNotification(node)
1367
1117
  break
1368
1118
  case 'w:gp2':
1369
- if (child?.tag === 'groups_dirty') {
1370
- // Bulk "these groups are stale, refetch" signal — not a per-group stub message,
1371
- // so it doesn't go through handleGroupNotification (which assumes a single group).
1372
- const dirtyGroupJids = (0, WABinary_1.getBinaryNodeChildren)(child, 'group')
1373
- .map(g => g.attrs.jid)
1374
- .filter(Boolean)
1375
- if (dirtyGroupJids.length) {
1376
- ev.emit(
1377
- 'groups.update',
1378
- dirtyGroupJids.map(jid => ({ id: jid }))
1379
- )
1380
- }
1381
- break
1382
- }
1383
- // PN/LID resolution for participant fields happens below via normalizeNotificationResult
1119
+ // TODO: HANDLE PARTICIPANT_PN
1384
1120
  const groupData = await getNotificationGroupData(node)
1385
1121
  handleGroupNotification(node, child, result)
1386
1122
 
@@ -1393,39 +1129,26 @@ const makeMessagesRecvSocket = config => {
1393
1129
  case 'encrypt':
1394
1130
  await handleEncryptNotification(node)
1395
1131
  break
1396
- case 'devices': {
1397
- // child = <add> or <remove> — neither carries jid/lid, owner is node.attrs.from.
1398
- // A third variant, <update hash="..."/>, carries no device list at all (just a
1399
- // device-list hash refresh) — don't misreport it as "removed nothing".
1400
- const addNode = (0, WABinary_1.getBinaryNodeChild)(node, 'add')
1401
- const removeNode = (0, WABinary_1.getBinaryNodeChild)(node, 'remove')
1402
- const changedNode = addNode || removeNode
1403
- if (!changedNode) {
1404
- logger.debug({ node }, 'devices hash refresh, no add/remove list to report')
1405
- break
1406
- }
1407
- const isAdded = !!addNode
1408
- const devices = (0, WABinary_1.getBinaryNodeChildren)(changedNode, 'device')
1409
- const deviceOwnerJid = from
1132
+ case 'devices':
1133
+ const devices = (0, WABinary_1.getBinaryNodeChildren)(child, 'device')
1134
+ const deviceOwnerJid = child.attrs.jid || child.attrs.lid
1410
1135
  const deviceData = devices.map(d => ({
1411
1136
  id: d.attrs.jid,
1412
1137
  lid: d.attrs.lid,
1413
- // wire attr is "key-index" (hyphen), not "key_index"
1414
- keyIndex: d.attrs['key-index'] ? +d.attrs['key-index'] : undefined,
1138
+ keyIndex: d.attrs.key_index ? +d.attrs.key_index : undefined,
1415
1139
  platform: d.attrs.platform || undefined,
1416
1140
  isCompanion: d.attrs.companion === 'true' || undefined
1417
1141
  }))
1418
- const isSelf =
1419
- (0, WABinary_1.areJidsSameUser)(from, authState.creds.me?.id) ||
1420
- (0, WABinary_1.areJidsSameUser)(from, authState.creds.me?.lid)
1421
- if (isSelf) {
1422
- logger.info({ deviceData, isAdded }, 'my own devices changed')
1423
- }
1424
- if (deviceOwnerJid) {
1425
- ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf, added: isAdded })
1142
+ if (
1143
+ (0, WABinary_1.areJidsSameUser)(child.attrs.jid, authState.creds.me.id) ||
1144
+ (0, WABinary_1.areJidsSameUser)(child.attrs.lid, authState.creds.me.lid)
1145
+ ) {
1146
+ logger.info({ deviceData }, 'my own devices changed')
1147
+ ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf: true })
1148
+ } else if (deviceOwnerJid) {
1149
+ ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf: false })
1426
1150
  }
1427
1151
  break
1428
- }
1429
1152
  case 'server_sync':
1430
1153
  const update = (0, WABinary_1.getBinaryNodeChild)(node, 'collection')
1431
1154
  if (update) {
@@ -1433,18 +1156,13 @@ const makeMessagesRecvSocket = config => {
1433
1156
  await resyncAppState([name], false)
1434
1157
  }
1435
1158
  break
1436
- case 'picture': {
1159
+ case 'picture':
1437
1160
  const setPicture = (0, WABinary_1.getBinaryNodeChild)(node, 'set')
1438
1161
  const delPicture = (0, WABinary_1.getBinaryNodeChild)(node, 'delete')
1439
- const pictureOwnerJid = (0, WABinary_1.jidNormalizedUser)(node?.attrs?.from)
1440
- if (!pictureOwnerJid) {
1441
- // a picture content hash is not a valid contact id — nothing usable to emit without "from"
1442
- logger.debug({ node }, 'picture notification missing "from", skipping contacts.update')
1443
- break
1444
- }
1162
+ // TODO: WAJIDHASH stuff proper support inhouse
1445
1163
  ev.emit('contacts.update', [
1446
1164
  {
1447
- id: pictureOwnerJid,
1165
+ id: (0, WABinary_1.jidNormalizedUser)(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
1448
1166
  imgUrl: setPicture ? 'changed' : 'removed'
1449
1167
  }
1450
1168
  ])
@@ -1461,7 +1179,6 @@ const makeMessagesRecvSocket = config => {
1461
1179
  }
1462
1180
  }
1463
1181
  break
1464
- }
1465
1182
  case 'account_sync':
1466
1183
  if (child.tag === 'disappearing_mode') {
1467
1184
  const newDuration = +child.attrs.duration
@@ -1477,60 +1194,18 @@ const makeMessagesRecvSocket = config => {
1477
1194
  }
1478
1195
  })
1479
1196
  } else if (child.tag === 'blocklist') {
1480
- // Real wire shape is <blocklist action="modify" addressing_mode="lid"/> with
1481
- // no per-entry children — it's a "your blocklist changed, go refetch it" signal,
1482
- // not an inline diff. Re-fetch and let fetchBlocklist emit the refreshed list.
1483
- sock.fetchBlocklist().catch(error => logger.warn({ error }, 'failed to refresh blocklist after account_sync'))
1484
- } else if (child.tag === 'devices') {
1485
- // Full device-list sync: all linked devices + signed key-index-list.
1486
- // Sent when a device is added, removed, or key index changes.
1487
- const dhash = child.attrs.dhash
1488
- const deviceNodes = (0, WABinary_1.getBinaryNodeChildren)(child, 'device')
1489
- const keyIndexListNode = (0, WABinary_1.getBinaryNodeChild)(child, 'key-index-list')
1490
- const devices = deviceNodes.map(d => ({
1491
- jid: d.attrs.jid ? String(d.attrs.jid) : undefined,
1492
- keyIndex: d.attrs['key-index'] ? +d.attrs['key-index'] : undefined
1493
- }))
1494
- logger.info({ dhash, deviceCount: devices.length }, 'account devices list synced')
1495
- ev.emit('account.devices-synced', {
1496
- dhash,
1497
- devices,
1498
- keyIndexListTimestamp: keyIndexListNode?.attrs?.ts ? +keyIndexListNode.attrs.ts : undefined,
1499
- keyIndexList: keyIndexListNode?.content ? Buffer.from(keyIndexListNode.content) : undefined
1500
- })
1501
- }
1502
- break
1503
- case 'disappearing_mode': {
1504
- // Same payload as the account_sync-nested variant above, but WhatsApp also
1505
- // pushes it as its own top-level notification type on some accounts.
1506
- const newDuration = +child.attrs.duration
1507
- const timestamp = +child.attrs.t
1508
- logger.info({ newDuration }, 'updated account disappearing mode')
1509
- ev.emit('creds.update', {
1510
- accountSettings: {
1511
- ...authState.creds.accountSettings,
1512
- defaultDisappearingMode: {
1513
- ephemeralExpiration: newDuration,
1514
- ephemeralSettingTimestamp: timestamp
1515
- }
1197
+ const blocklists = (0, WABinary_1.getBinaryNodeChildren)(child, 'item')
1198
+ for (const { attrs } of blocklists) {
1199
+ const blocklist = [attrs.jid]
1200
+ const type = attrs.action === 'block' ? 'add' : 'remove'
1201
+ ev.emit('blocklist.update', { blocklist, type })
1516
1202
  }
1517
- })
1518
- break
1519
- }
1520
- case 'contacts': {
1521
- // <update jid="..."/> = single contact touched; <update hash="..."/> or a
1522
- // sibling <sync after="..."/> = list-level signal with no actionable jid.
1523
- const updateChild = (0, WABinary_1.getBinaryNodeChild)(node, 'update')
1524
- if (updateChild?.attrs?.jid) {
1525
- ev.emit('contacts.update', [{ id: (0, WABinary_1.jidNormalizedUser)(updateChild.attrs.jid) }])
1526
- } else {
1527
- logger.debug({ node }, 'contacts list changed (hash/sync signal), no per-contact jid to update')
1528
1203
  }
1529
1204
  break
1530
- }
1531
1205
  case 'business':
1206
+ // SMB privacy / data-sharing settings sync push
1207
+ // (WhatsApp Web: WASmaxInBizSettingsSyncPrivacySettingRequest)
1532
1208
  if (child?.tag === 'privacy') {
1533
- // SMB privacy / data-sharing settings sync push
1534
1209
  ev.emit('business.privacy-settings-sync', {
1535
1210
  jid: from,
1536
1211
  categories: (0, WABinary_1.getBinaryNodeChildren)(child, 'category').map(c => ({
@@ -1539,32 +1214,6 @@ const makeMessagesRecvSocket = config => {
1539
1214
  })),
1540
1215
  attrs: child.attrs
1541
1216
  })
1542
- } else if (child?.tag === 'profile') {
1543
- // Business profile updated for a contact (tag = profile version hash)
1544
- ev.emit('contacts.update', [
1545
- {
1546
- id: (0, WABinary_1.jidNormalizedUser)(child.attrs.jid || from),
1547
- businessProfileTag: child.attrs.tag
1548
- }
1549
- ])
1550
- } else if (child?.tag === 'verified_name') {
1551
- // Verified business name changed — content is a proto payload
1552
- ev.emit('contacts.update', [
1553
- {
1554
- id: (0, WABinary_1.jidNormalizedUser)(child.attrs.jid || from),
1555
- verifiedName: {
1556
- verifiedLevel: child.attrs.verified_level,
1557
- serial: child.attrs.serial,
1558
- version: child.attrs.v
1559
- }
1560
- }
1561
- ])
1562
- } else if (child?.tag === 'remove') {
1563
- // Business account downgraded/removed. <remove jid="..."/> names the account;
1564
- // <remove hash="..."/> is a list-level signal with no actionable jid.
1565
- if (child.attrs.jid) {
1566
- ev.emit('contacts.update', [{ id: (0, WABinary_1.jidNormalizedUser)(child.attrs.jid), isBusiness: false }])
1567
- }
1568
1217
  }
1569
1218
  break
1570
1219
  case 'hosted':
@@ -1838,10 +1487,11 @@ const makeMessagesRecvSocket = config => {
1838
1487
  const newValue = ((await msgRetryCache.get(key)) || 0) + 1
1839
1488
  await msgRetryCache.set(key, newValue)
1840
1489
  }
1841
- const sendMessagesAgain = async (key, ids, retryNode) => {
1490
+ const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
1842
1491
  const remoteJid = key.remoteJid
1843
1492
  const participant = key.participant || remoteJid
1844
1493
  const retryCount = +retryNode.attrs.count || 1
1494
+ const msgId = ids[0]
1845
1495
  // Try to get messages from cache first, then fallback to getMessage
1846
1496
  const msgs = []
1847
1497
  for (const id of ids) {
@@ -1873,12 +1523,53 @@ const makeMessagesRecvSocket = config => {
1873
1523
  // just re-send the message to everyone
1874
1524
  // prevents the first message decryption failure
1875
1525
  const sendToAll = !(0, WABinary_1.jidDecode)(participant)?.device
1526
+ const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
1527
+ let injectedFromBundle = false
1528
+ if (typeof signalRepository.injectE2ESession === 'function' && typeof Utils_1.extractE2ESessionFromRetryReceipt === 'function') {
1529
+ const bundle = Utils_1.extractE2ESessionFromRetryReceipt(receiptNode)
1530
+ if (bundle) {
1531
+ try {
1532
+ await signalRepository.injectE2ESession({ jid: participant, session: bundle })
1533
+ injectedFromBundle = true
1534
+ logger.debug({ participant, retryCount }, 'injected session from retry receipt key bundle')
1535
+ } catch (error) {
1536
+ logger.warn({ error, participant }, 'failed to inject session from retry receipt')
1537
+ }
1538
+ }
1539
+ }
1540
+ if (!injectedFromBundle && signalRepository.getSessionInfo) {
1541
+ const receivedRegId = (0, WABinary_1.getBinaryNodeChildUInt)(receiptNode, 'registration', 4)
1542
+ if (typeof receivedRegId === 'number' && Number.isInteger(receivedRegId)) {
1543
+ const info = await signalRepository.getSessionInfo(participant)
1544
+ if (info && info.registrationId !== 0 && info.registrationId !== receivedRegId) {
1545
+ logger.info(
1546
+ { participant, stored: info.registrationId, received: receivedRegId },
1547
+ 'reg id mismatch on retry without bundle, deleting session'
1548
+ )
1549
+ await authState.keys.set({ session: { [sessionId]: null } })
1550
+ }
1551
+ }
1552
+ }
1553
+ const BASE_KEY_CHECK_RETRY = 2
1554
+ if (msgId && messageRetryManager && signalRepository.getSessionInfo) {
1555
+ const info = await signalRepository.getSessionInfo(participant)
1556
+ if (info) {
1557
+ if (retryCount === BASE_KEY_CHECK_RETRY) {
1558
+ messageRetryManager.saveBaseKey(sessionId, msgId, info.baseKey)
1559
+ } else if (retryCount > BASE_KEY_CHECK_RETRY) {
1560
+ if (messageRetryManager.hasSameBaseKey(sessionId, msgId, info.baseKey)) {
1561
+ logger.warn({ participant, retryCount }, 'base key collision on retry, forcing fresh session')
1562
+ await authState.keys.set({ session: { [sessionId]: null } })
1563
+ }
1564
+ messageRetryManager.deleteBaseKey(sessionId, msgId)
1565
+ }
1566
+ }
1567
+ }
1876
1568
  // Check if we should recreate session for this retry
1877
1569
  let shouldRecreateSession = false
1878
1570
  let recreateReason = ''
1879
- if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
1571
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
1880
1572
  try {
1881
- const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
1882
1573
  const hasSession = await signalRepository.validateSession(participant)
1883
1574
  const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists)
1884
1575
  shouldRecreateSession = result.recreate
@@ -1891,7 +1582,9 @@ const makeMessagesRecvSocket = config => {
1891
1582
  logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry')
1892
1583
  }
1893
1584
  }
1894
- await assertSessions([participant], true)
1585
+ if (!injectedFromBundle) {
1586
+ await assertSessions([participant], true)
1587
+ }
1895
1588
  if ((0, WABinary_1.isJidGroup)(remoteJid)) {
1896
1589
  await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } })
1897
1590
  }
@@ -1940,51 +1633,6 @@ const makeMessagesRecvSocket = config => {
1940
1633
  const items = (0, WABinary_1.getBinaryNodeChildren)(content[0], 'item')
1941
1634
  ids.push(...items.map(i => i.attrs.id))
1942
1635
  }
1943
- // E. Media Retry Notification Receipt
1944
- // Server tells us a specific message's media should be re-fetched
1945
- if (attrs.type === 'media-retry') {
1946
- const mediaRetryNode =
1947
- (0, WABinary_1.getBinaryNodeChild)(node, 'media-retry') ||
1948
- (0, WABinary_1.getBinaryNodeChild)(node, 'media_retry')
1949
- ev.emit('messages.media-retry', {
1950
- ids,
1951
- from: attrs.from,
1952
- participant: attrs.participant,
1953
- t: attrs.t,
1954
- retryAttrs: mediaRetryNode?.attrs
1955
- })
1956
- await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack media-retry receipt'))
1957
- return
1958
- }
1959
- // F. Server Error Receipt
1960
- // Server signals a delivery error for one or more messages
1961
- if (attrs.type === 'server-error') {
1962
- const errorNode = (0, WABinary_1.getBinaryNodeChild)(node, 'error')
1963
- ev.emit('messages.server-error', {
1964
- ids,
1965
- from: attrs.from,
1966
- participant: attrs.participant,
1967
- t: attrs.t,
1968
- errorCode: errorNode?.attrs?.code || attrs.error_code,
1969
- errorText: errorNode?.attrs?.text || errorNode?.content?.toString?.()
1970
- })
1971
- await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack server-error receipt'))
1972
- return
1973
- }
1974
- // D. Receipt aggregation
1975
- // Server sent a batched-receipt (receipt_agg attr) — emit dedicated event AND fall through
1976
- // to the normal status-update pipeline so all IDs in the batch get their status updated.
1977
- if (attrs.receipt_agg) {
1978
- ev.emit('receipt.batched', {
1979
- ids,
1980
- from: attrs.from,
1981
- participant: attrs.participant,
1982
- type: attrs.type,
1983
- t: attrs.t,
1984
- receiptAgg: attrs.receipt_agg
1985
- })
1986
- // Do not return — let all IDs be processed by the normal receipt path below.
1987
- }
1988
1636
  try {
1989
1637
  await Promise.all([
1990
1638
  receiptMutex.mutex(async () => {
@@ -2029,7 +1677,7 @@ const makeMessagesRecvSocket = config => {
2029
1677
  try {
2030
1678
  await updateSendMessageAgainCount(ids[0], key.participant)
2031
1679
  logger.debug({ attrs, key }, 'recv retry request')
2032
- await sendMessagesAgain(key, ids, retryNode)
1680
+ await sendMessagesAgain(key, ids, retryNode, node)
2033
1681
  } catch (error) {
2034
1682
  logger.error(
2035
1683
  { key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' },
@@ -2419,19 +2067,6 @@ const makeMessagesRecvSocket = config => {
2419
2067
  }
2420
2068
  }
2421
2069
  }
2422
- // group_info roster: <group_info><user jid=.. state=.. user_pn=..><device jid=..
2423
- // platform=..><capability ver=../></device></user>...</group_info>. Baileys previously
2424
- // only kept the flat group_info-level attrs (call.payload), dropping the roster entirely.
2425
- const parseGroupCallRoster = groupInfoNode =>
2426
- (0, WABinary_1.getBinaryNodeChildren)(groupInfoNode, 'user').map(userNode => ({
2427
- jid: userNode.attrs.jid,
2428
- state: userNode.attrs.state,
2429
- userPn: userNode.attrs.user_pn,
2430
- devices: (0, WABinary_1.getBinaryNodeChildren)(userNode, 'device').map(d => ({
2431
- jid: d.attrs.jid,
2432
- platform: d.attrs.platform
2433
- }))
2434
- }))
2435
2070
  const handleCall = async node => {
2436
2071
  try {
2437
2072
  const { attrs } = node
@@ -2459,45 +2094,10 @@ const makeMessagesRecvSocket = config => {
2459
2094
  }
2460
2095
  }
2461
2096
  if (status === 'offer') {
2462
- const videoNode = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'video')
2463
- const audioNodes = (0, WABinary_1.getBinaryNodeChildren)(infoChild, 'audio')
2464
- const silenceNode = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'silence')
2465
- call.isVideo = !!videoNode
2097
+ call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(infoChild, 'video')
2466
2098
  call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid']
2467
2099
  call.groupJid = infoChild.attrs['group-jid']
2468
- // lightweight=1 marks a silent/wave-style group ring (no full ringing UX expected)
2469
- call.isLightweight = infoChild.attrs.lightweight === '1'
2470
- if (silenceNode?.attrs?.reason) {
2471
- call.silenceReason = silenceNode.attrs.reason
2472
- }
2473
- // Wire attrs are "enc"/"rate" (audio) and "dec" (video) — not "codec".
2474
- // <audio> can repeat once per supported sample rate.
2475
- if (audioNodes.length) {
2476
- call.audioCodecs = audioNodes.map(n => ({ enc: n.attrs.enc, rate: n.attrs.rate ? +n.attrs.rate : undefined }))
2477
- call.audioCodec = audioNodes[0].attrs.enc
2478
- }
2479
- if (videoNode?.attrs?.dec) {
2480
- call.videoCodec = videoNode.attrs.dec
2481
- }
2482
- // Decrypt Signal-encrypted callKey from <enc> child
2483
- const encNode = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'enc')
2484
- if (encNode?.content) {
2485
- try {
2486
- const encType = encNode.attrs.type || 'msg'
2487
- const ciphertext = Buffer.isBuffer(encNode.content) ? encNode.content : Buffer.from(encNode.content)
2488
- const decrypted = await signalRepository.decryptMessage({ jid: attrs.from, type: encType, ciphertext })
2489
- const unpadded = (0, Utils_1.unpadRandomMax16)(decrypted)
2490
- const callMsg = index_js_1.proto.Message.decode(unpadded)
2491
- if (callMsg?.call?.callKey?.length) {
2492
- call.callKey = Buffer.from(callMsg.call.callKey)
2493
- }
2494
- } catch (e) {
2495
- logger.debug({ e }, 'failed to decrypt call enc')
2496
- }
2497
- }
2498
2100
  await callOfferCache.set(call.id, call)
2499
- } else if (status === 'waiting_room_request') {
2500
- call.peerJid = infoChild.attrs.from || infoChild.attrs['peer-jid'] || infoChild.attrs['user-jid']
2501
2101
  }
2502
2102
  const existingCall = await callOfferCache.get(call.id)
2503
2103
  // use existing call info to populate this event
@@ -2506,41 +2106,8 @@ const makeMessagesRecvSocket = config => {
2506
2106
  call.isGroup = existingCall.isGroup
2507
2107
  call.callerPn = call.callerPn || existingCall.callerPn
2508
2108
  }
2509
- // Enrich event payload for signalling-only statuses
2510
- if (status === 'peer_state') {
2511
- const stateChild = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'peer_state')
2512
- call.state = stateChild?.attrs?.state || infoChild.attrs?.state
2513
- } else if (status === 'group_info') {
2514
- call.payload = infoChild.attrs
2515
- call.participants = parseGroupCallRoster(infoChild)
2516
- } else if (status === 'video_state') {
2517
- const vsChild = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'video_state')
2518
- const enabledRaw = vsChild?.attrs?.enabled ?? infoChild.attrs?.enabled
2519
- call.enabled = enabledRaw === 'true' || enabledRaw === '1'
2520
- } else if (status === 'enc_rekey') {
2521
- const rekeyChild =
2522
- (0, WABinary_1.getBinaryNodeChild)(infoChild, 'enc-rekey') ||
2523
- (0, WABinary_1.getBinaryNodeChild)(infoChild, 'enc_rekey')
2524
- if (rekeyChild?.content && Buffer.isBuffer(rekeyChild.content)) {
2525
- try {
2526
- call.rekeyPayload = (0, Utils_1.decodeE2eRekeyPayload)(rekeyChild.content)
2527
- } catch (e) {
2528
- logger.debug({ e }, 'failed to decode enc-rekey payload in handleCall')
2529
- }
2530
- }
2531
- }
2532
2109
  // delete data once call has ended
2533
- if (
2534
- status === 'reject' ||
2535
- status === 'accept' ||
2536
- status === 'timeout' ||
2537
- status === 'terminate' ||
2538
- status === 'reject_do_not_disturb' ||
2539
- status === 'mic_permission_denied' ||
2540
- status === 'camera_permission_denied' ||
2541
- status === 'remote_busy' ||
2542
- status === 'remote_offline'
2543
- ) {
2110
+ if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
2544
2111
  await callOfferCache.del(call.id)
2545
2112
  }
2546
2113
  await normalizeCallEventJids(call, infoChild)
@@ -2555,24 +2122,7 @@ const makeMessagesRecvSocket = config => {
2555
2122
  // <mute_v2>, <transport>, … each with a call-id) instead of wrapped in <call>.
2556
2123
  // This additively handles those: emit a 'call' event for state stanzas and ack ALL
2557
2124
  // of them (otherwise WhatsApp keeps redelivering). The <call> path above is untouched.
2558
- const CALL_STATE_TAGS = new Set([
2559
- 'offer',
2560
- 'offer_notice',
2561
- 'terminate',
2562
- 'accept',
2563
- 'reject',
2564
- 'preaccept',
2565
- 'accept_ack',
2566
- 'enc-rekey',
2567
- 'enc_rekey',
2568
- 'peer_state',
2569
- 'group_info',
2570
- 'video_state',
2571
- 'video_state_ack',
2572
- 'flow_control',
2573
- 'mute_v2',
2574
- 'waiting_room_request'
2575
- ])
2125
+ const CALL_STATE_TAGS = new Set(['offer', 'offer_notice', 'terminate', 'accept', 'reject', 'preaccept'])
2576
2126
  const handleStandaloneCallStanza = async node => {
2577
2127
  try {
2578
2128
  if (!CALL_STATE_TAGS.has(node.tag)) {
@@ -2592,24 +2142,9 @@ const makeMessagesRecvSocket = config => {
2592
2142
  status
2593
2143
  }
2594
2144
  if (status === 'offer') {
2595
- const videoNode = (0, WABinary_1.getBinaryNodeChild)(node, 'video')
2596
- const audioNodes = (0, WABinary_1.getBinaryNodeChildren)(node, 'audio')
2597
- const silenceNode = (0, WABinary_1.getBinaryNodeChild)(node, 'silence')
2598
- call.isVideo = !!videoNode
2145
+ call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(node, 'video')
2599
2146
  call.isGroup = attrs.type === 'group' || !!attrs['group-jid']
2600
2147
  call.groupJid = attrs['group-jid']
2601
- call.isLightweight = attrs.lightweight === '1'
2602
- if (silenceNode?.attrs?.reason) {
2603
- call.silenceReason = silenceNode.attrs.reason
2604
- }
2605
- // Wire attrs are "enc"/"rate" (audio) and "dec" (video) — not "codec".
2606
- if (audioNodes.length) {
2607
- call.audioCodecs = audioNodes.map(n => ({ enc: n.attrs.enc, rate: n.attrs.rate ? +n.attrs.rate : undefined }))
2608
- call.audioCodec = audioNodes[0].attrs.enc
2609
- }
2610
- if (videoNode?.attrs?.dec) {
2611
- call.videoCodec = videoNode.attrs.dec
2612
- }
2613
2148
  if (callId) {
2614
2149
  await callOfferCache.set(callId, call)
2615
2150
  }
@@ -2620,45 +2155,7 @@ const makeMessagesRecvSocket = config => {
2620
2155
  call.isGroup = existingCall.isGroup
2621
2156
  call.callerPn = call.callerPn || existingCall.callerPn
2622
2157
  }
2623
- // Enrich event payload for signalling-only statuses
2624
- if (status === 'peer_state') {
2625
- const stateChild = (0, WABinary_1.getBinaryNodeChild)(node, 'peer_state')
2626
- call.state = stateChild?.attrs?.state || attrs?.state
2627
- } else if (status === 'group_info') {
2628
- call.payload = attrs
2629
- call.participants = parseGroupCallRoster(node)
2630
- } else if (status === 'video_state') {
2631
- const vsChild = (0, WABinary_1.getBinaryNodeChild)(node, 'video_state')
2632
- const enabledRaw = vsChild?.attrs?.enabled ?? attrs?.enabled
2633
- call.enabled = enabledRaw === 'true' || enabledRaw === '1'
2634
- } else if (status === 'enc_rekey') {
2635
- const rekeyChild =
2636
- (0, WABinary_1.getBinaryNodeChild)(node, 'enc-rekey') || (0, WABinary_1.getBinaryNodeChild)(node, 'enc_rekey')
2637
- if (rekeyChild?.content && Buffer.isBuffer(rekeyChild.content)) {
2638
- try {
2639
- call.rekeyPayload = (0, Utils_1.decodeE2eRekeyPayload)(rekeyChild.content)
2640
- } catch (e) {
2641
- logger.debug({ e }, 'failed to decode enc-rekey payload in standalone call stanza')
2642
- }
2643
- }
2644
- } else if (status === 'mute') {
2645
- // mute_v2 IS the top-level tag on real traffic (no wrapping/nested child) —
2646
- // attrs are { call-id, call-creator, mute-state: "0"|"1" }, never
2647
- // muted/audio_muted, and there's no nested <audio> child.
2648
- call.muted = node.attrs['mute-state'] === '1'
2649
- }
2650
- if (
2651
- callId &&
2652
- (status === 'reject' ||
2653
- status === 'accept' ||
2654
- status === 'timeout' ||
2655
- status === 'terminate' ||
2656
- status === 'reject_do_not_disturb' ||
2657
- status === 'mic_permission_denied' ||
2658
- status === 'camera_permission_denied' ||
2659
- status === 'remote_busy' ||
2660
- status === 'remote_offline')
2661
- ) {
2158
+ if (callId && (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate')) {
2662
2159
  await callOfferCache.del(callId)
2663
2160
  }
2664
2161
  await normalizeCallEventJids(call, node)
@@ -2727,37 +2224,6 @@ const makeMessagesRecvSocket = config => {
2727
2224
  // }
2728
2225
  }
2729
2226
  }
2730
- const handleChatstate = async node => {
2731
- const { from, state } = node.attrs
2732
- if (!from) return
2733
- const isTyping = state === 'typing'
2734
- ev.emit('presence.update', [
2735
- {
2736
- id: from,
2737
- presences: {
2738
- [from]: isTyping ? 'composing' : 'paused'
2739
- }
2740
- }
2741
- ])
2742
- }
2743
- const handlePresence = async node => {
2744
- const { from, type } = node.attrs
2745
- if (!from || !type) return
2746
- const presenceMap = {
2747
- available: 'available',
2748
- unavailable: 'unavailable'
2749
- }
2750
- const presence = presenceMap[type] || type
2751
- ev.emit('presence.update', [
2752
- {
2753
- id: from,
2754
- presences: {
2755
- [from]: presence
2756
- },
2757
- lastSeen: node.attrs.t ? Number(node.attrs.t) * 1000 : undefined
2758
- }
2759
- ])
2760
- }
2761
2227
  /// processes a node with the given function
2762
2228
  /// and adds the task to the existing buffer if we're buffering events
2763
2229
  const processNodeWithBuffer = async (node, identifier, exec) => {
@@ -2809,33 +2275,8 @@ const makeMessagesRecvSocket = config => {
2809
2275
  })
2810
2276
  // additive: top-level call-signalling stanzas (some accounts send these instead of <call>)
2811
2277
  for (const callTag of [
2812
- 'offer',
2813
- 'offer_notice',
2814
- 'terminate',
2815
- 'accept',
2816
- 'reject',
2817
- 'preaccept',
2818
- 'accept_ack',
2819
- 'enc-rekey',
2820
- 'enc_rekey',
2821
- 'peer_state',
2822
- 'group_info',
2823
- 'video_state',
2824
- 'video_state_ack',
2825
- 'flow_control',
2826
- 'transport',
2827
- 'video',
2828
- 'duration',
2829
- 'mute_v2',
2830
- 'lobby',
2831
- 'heartbeat',
2832
- 'relaylatency',
2833
- 'link_query',
2834
- 'waiting_room_request',
2835
- // Present in Utils/stanza-ack.js's CALL_STANZA_TAGS (ack builder already treats it as a
2836
- // call stanza) but was missing here — meant it was never routed/acked, so real traffic
2837
- // (confirmed via live capture) would just get redelivered forever.
2838
- 'group_update'
2278
+ 'offer', 'offer_notice', 'terminate', 'accept', 'reject', 'preaccept',
2279
+ 'transport', 'video', 'duration', 'mute_v2', 'lobby', 'heartbeat', 'relaylatency', 'link_query'
2839
2280
  ]) {
2840
2281
  ws.on('CB:' + callTag, node => {
2841
2282
  nodelogger(node)
@@ -2850,18 +2291,6 @@ const makeMessagesRecvSocket = config => {
2850
2291
  nodelogger(node)
2851
2292
  await processNode('notification', node, 'handling notification', handleNotification)
2852
2293
  })
2853
- ws.on('CB:status', async node => {
2854
- nodelogger(node)
2855
- await handleNewsletterStatus(node).catch(error => onUnexpectedError(error, 'handling newsletter status'))
2856
- })
2857
- ws.on('CB:chatstate', async node => {
2858
- nodelogger(node)
2859
- await handleChatstate(node).catch(error => onUnexpectedError(error, 'handling chatstate'))
2860
- })
2861
- ws.on('CB:presence', async node => {
2862
- nodelogger(node)
2863
- await handlePresence(node).catch(error => onUnexpectedError(error, 'handling presence'))
2864
- })
2865
2294
  ws.on('CB:ack,class:message', node => {
2866
2295
  nodelogger(node)
2867
2296
  handleBadAck(node).catch(error => onUnexpectedError(error, 'handling bad ack'))