@badzz88/baileys 8.4.9 → 8.5.0

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 (153) hide show
  1. package/WAProto/WAProto.proto +6 -347
  2. package/WAProto/fix-imports.js +85 -86
  3. package/WAProto/index.d.ts +25 -4913
  4. package/WAProto/index.js +93 -14069
  5. package/{src → lib}/Defaults/index.js +2 -13
  6. package/{src → lib}/Signal/Group/group_cipher.js +5 -5
  7. package/{src → lib}/Signal/Group/keyhelper.js +4 -4
  8. package/{src → lib}/Signal/Group/sender-chain-key.js +2 -2
  9. package/{src → lib}/Signal/Group/sender-key-distribution-message.js +1 -1
  10. package/{src → lib}/Signal/Group/sender-key-message.js +3 -3
  11. package/{src → lib}/Signal/Group/sender-key-record.js +1 -1
  12. package/{src → lib}/Signal/Group/sender-message-key.js +2 -2
  13. package/{src → lib}/Signal/libsignal.js +26 -6
  14. package/{src → lib}/Socket/chats.js +58 -616
  15. package/{src → lib}/Socket/communities.js +28 -59
  16. package/{src → lib}/Socket/graphql.js +103 -362
  17. package/{src → lib}/Socket/groups.js +29 -77
  18. package/{src → lib}/Socket/index.js +5 -3
  19. package/{src → lib}/Socket/interactive-handler.js +60 -117
  20. package/{src → lib}/Socket/interop.js +19 -36
  21. package/lib/Socket/luxu.js +387 -0
  22. package/{src → lib}/Socket/managed-account.js +27 -58
  23. package/{src → lib}/Socket/messages-recv.js +143 -715
  24. package/{src → lib}/Socket/messages-send.js +102 -412
  25. package/lib/Socket/mex.js +60 -0
  26. package/{src → lib}/Socket/newsletter.js +23 -266
  27. package/{src → lib}/Socket/privacy.js +38 -41
  28. package/{src → lib}/Socket/registration.js +106 -113
  29. package/{src → lib}/Socket/socket.js +101 -88
  30. package/{src → lib}/Socket/text-router.js +17 -1
  31. package/lib/Socket/username.js +243 -0
  32. package/lib/Store/keyed-db.js +108 -0
  33. package/{src → lib}/Store/make-in-memory-store.js +104 -6
  34. package/lib/Types/Mex.js +112 -0
  35. package/{src → lib}/Types/Newsletter.js +41 -72
  36. package/{src → lib}/Types/index.js +1 -0
  37. package/{src → lib}/Utils/chat-utils.js +9 -73
  38. package/lib/Utils/command-loader.d.ts +31 -0
  39. package/lib/Utils/command-loader.js +100 -0
  40. package/lib/Utils/companion-reg-client-utils.js +42 -0
  41. package/lib/Utils/crypto.js +155 -0
  42. package/lib/Utils/curve25519-js.js +275 -0
  43. package/{src → lib}/Utils/decode-wa-message.js +36 -284
  44. package/{src → lib}/Utils/event-buffer.js +27 -6
  45. package/{src → lib}/Utils/generics.js +37 -114
  46. package/{src → lib}/Utils/index.js +5 -1
  47. package/lib/Utils/lt-hash.js +39 -0
  48. package/{src → lib}/Utils/message-retry-manager.js +39 -0
  49. package/{src → lib}/Utils/messages-media.js +58 -105
  50. package/{src → lib}/Utils/messages.js +529 -115
  51. package/lib/Utils/meta-ai-msmsg.js +262 -0
  52. package/lib/Utils/native-bridge.js +82 -0
  53. package/{src → lib}/Utils/noise-handler.js +15 -13
  54. package/{src → lib}/Utils/process-message.js +55 -54
  55. package/{src → lib}/Utils/reporting-utils.js +2 -2
  56. package/lib/Utils/session-pool.d.ts +16 -0
  57. package/lib/Utils/session-pool.js +94 -0
  58. package/{src → lib}/Utils/signal.js +51 -19
  59. package/{src → lib}/Utils/stanza-ack.js +2 -36
  60. package/lib/Utils/sticker.d.ts +17 -0
  61. package/lib/Utils/sticker.js +145 -0
  62. package/{src → lib}/Utils/tc-token-utils.js +1 -1
  63. package/{src → lib}/Utils/validate-connection.js +11 -40
  64. package/lib/Utils/view-once-cache.d.ts +9 -0
  65. package/lib/Utils/view-once-cache.js +79 -0
  66. package/{src → lib}/WABinary/decode.js +4 -38
  67. package/lib/WABinary/encode.js +228 -0
  68. package/{src → lib}/WABinary/generic-utils.js +93 -1
  69. package/{src → lib}/WABinary/jid-utils.js +3 -16
  70. package/{src → lib}/WAM/encode.js +1 -1
  71. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +43 -0
  72. package/{src → lib}/WAUSync/Protocols/USyncContactProtocol.js +1 -7
  73. package/{src → lib}/WAUSync/Protocols/USyncDeviceProtocol.js +5 -13
  74. package/{src → lib}/WAUSync/Protocols/USyncDisappearingModeProtocol.js +4 -1
  75. package/{src → lib}/WAUSync/Protocols/USyncFeatureProtocol.js +2 -17
  76. package/{src → lib}/WAUSync/Protocols/USyncStatusProtocol.js +2 -7
  77. package/{src → lib}/WAUSync/Protocols/USyncUsernameProtocol.js +1 -2
  78. package/{src → lib}/WAUSync/Protocols/index.js +0 -1
  79. package/{src → lib}/WAUSync/USyncQuery.js +6 -77
  80. package/{src → lib}/WAUSync/USyncUser.js +8 -16
  81. package/{src → lib}/WAUSync/index.js +0 -1
  82. package/{src → lib}/antiban.js +266 -355
  83. package/{src → lib}/index.js +2 -2
  84. package/package.json +18 -15
  85. package/WAProto/fix-import.js +0 -38
  86. package/src/Socket/mex.js +0 -47
  87. package/src/Socket/username.js +0 -234
  88. package/src/Utils/crypto.js +0 -125
  89. package/src/Utils/lt-hash.js +0 -10
  90. package/src/Utils/message-inspect.js +0 -400
  91. package/src/Utils/meta-ai-msmsg.js +0 -133
  92. package/src/WABinary/encode.js +0 -58
  93. package/src/WAUSync/Protocols/USyncBusinessProtocol.js +0 -100
  94. package/src/WAUSync/USyncBackoff.js +0 -31
  95. /package/{src → lib}/Defaults/phonenumber-mcc.json +0 -0
  96. /package/{src → lib}/Signal/Group/ciphertext-message.js +0 -0
  97. /package/{src → lib}/Signal/Group/group-session-builder.js +0 -0
  98. /package/{src → lib}/Signal/Group/index.js +0 -0
  99. /package/{src → lib}/Signal/Group/sender-key-name.js +0 -0
  100. /package/{src → lib}/Signal/Group/sender-key-state.js +0 -0
  101. /package/{src → lib}/Signal/lid-mapping.js +0 -0
  102. /package/{src → lib}/Socket/Client/index.js +0 -0
  103. /package/{src → lib}/Socket/Client/types.js +0 -0
  104. /package/{src → lib}/Socket/Client/websocket.js +0 -0
  105. /package/{src → lib}/Socket/aigroups.js +0 -0
  106. /package/{src → lib}/Socket/business.js +0 -0
  107. /package/{src → lib}/Store/index.js +0 -0
  108. /package/{src → lib}/Store/make-cache-manager-store.js +0 -0
  109. /package/{src → lib}/Store/make-ordered-dictionary.js +0 -0
  110. /package/{src → lib}/Store/object-repository.js +0 -0
  111. /package/{src → lib}/Types/Auth.js +0 -0
  112. /package/{src → lib}/Types/Bussines.js +0 -0
  113. /package/{src → lib}/Types/Call.js +0 -0
  114. /package/{src → lib}/Types/Chat.js +0 -0
  115. /package/{src → lib}/Types/Contact.js +0 -0
  116. /package/{src → lib}/Types/Events.js +0 -0
  117. /package/{src → lib}/Types/GroupMetadata.js +0 -0
  118. /package/{src → lib}/Types/Label.js +0 -0
  119. /package/{src → lib}/Types/LabelAssociation.js +0 -0
  120. /package/{src → lib}/Types/Message.js +0 -0
  121. /package/{src → lib}/Types/Product.js +0 -0
  122. /package/{src → lib}/Types/Signal.js +0 -0
  123. /package/{src → lib}/Types/Socket.js +0 -0
  124. /package/{src → lib}/Types/State.js +0 -0
  125. /package/{src → lib}/Types/USync.js +0 -0
  126. /package/{src → lib}/Utils/auth-utils.js +0 -0
  127. /package/{src → lib}/Utils/browser-utils.js +0 -0
  128. /package/{src → lib}/Utils/business.js +0 -0
  129. /package/{src → lib}/Utils/consumer-application.js +0 -0
  130. /package/{src → lib}/Utils/group-history.js +0 -0
  131. /package/{src → lib}/Utils/history.js +0 -0
  132. /package/{src → lib}/Utils/identity-change-handler.js +0 -0
  133. /package/{src → lib}/Utils/jid-display-normalization.js +0 -0
  134. /package/{src → lib}/Utils/link-preview.js +0 -0
  135. /package/{src → lib}/Utils/logger.js +0 -0
  136. /package/{src → lib}/Utils/make-mutex.js +0 -0
  137. /package/{src → lib}/Utils/message-composer.js +0 -0
  138. /package/{src → lib}/Utils/offline-node-processor.js +0 -0
  139. /package/{src → lib}/Utils/pre-key-manager.js +0 -0
  140. /package/{src → lib}/Utils/sync-action-utils.js +0 -0
  141. /package/{src → lib}/Utils/use-multi-file-auth-state.js +0 -0
  142. /package/{src → lib}/Utils/voip-rekey.js +0 -0
  143. /package/{src → lib}/WABinary/constants.js +0 -0
  144. /package/{src → lib}/WABinary/index.js +0 -0
  145. /package/{src → lib}/WABinary/types.js +0 -0
  146. /package/{src → lib}/WAM/BinaryInfo.js +0 -0
  147. /package/{src → lib}/WAM/constants.js +0 -0
  148. /package/{src → lib}/WAM/index.js +0 -0
  149. /package/{src → lib}/WAUSync/Protocols/USyncBotProfileProtocol.js +0 -0
  150. /package/{src → lib}/WAUSync/Protocols/USyncLIDProtocol.js +0 -0
  151. /package/{src → lib}/WAUSync/Protocols/USyncPictureProtocol.js +0 -0
  152. /package/{src → lib}/WAUSync/Protocols/USyncSidelistProtocol.js +0 -0
  153. /package/{src → lib}/WAUSync/Protocols/USyncTextStatusProtocol.js +0 -0
@@ -46,10 +46,7 @@ const makeMessagesRecvSocket = config => {
46
46
  uploadPreKeys,
47
47
  sendPeerDataOperationMessage,
48
48
  messageRetryManager,
49
- issuePrivacyTokens,
50
- getUSyncDevices,
51
- createParticipantNodes,
52
- newsletterServerIdCache
49
+ issuePrivacyTokens
53
50
  } = sock
54
51
  const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
55
52
  // Track when the socket fully opens so pending pre-connect messages are treated as history
@@ -147,14 +144,13 @@ const makeMessagesRecvSocket = config => {
147
144
  *
148
145
  * @param {string} [registrationTraceId] - Optional trace ID for this registration attempt.
149
146
  */
150
- const requestCompanionCanonicalNonce = async registrationTraceId => {
147
+ const requestCompanionCanonicalNonce = async (registrationTraceId) => {
151
148
  if (!authState.creds.me?.id) {
152
149
  throw new boom_1.Boom('Not authenticated')
153
150
  }
154
151
  return sendPeerDataOperationMessage({
155
152
  companionCanonicalUserNonceFetchRequest: registrationTraceId ? { registrationTraceId } : {},
156
- peerDataOperationRequestType:
157
- index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_CANONICAL_USER_NONCE_FETCH
153
+ peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_CANONICAL_USER_NONCE_FETCH
158
154
  })
159
155
  }
160
156
  /**
@@ -170,135 +166,54 @@ const makeMessagesRecvSocket = config => {
170
166
  peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_META_NONCE_FETCH
171
167
  })
172
168
  }
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')
169
+ // Handles mex newsletter notifications
170
+ const handleMexNewsletterNotification = async node => {
171
+ const mexNode = (0, WABinary_1.getBinaryNodeChild)(node, 'mex')
172
+ if (!mexNode?.content) {
173
+ logger.warn({ node }, 'Invalid mex newsletter notification')
179
174
  return
180
175
  }
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) {
176
+ let data
177
+ try {
178
+ data = JSON.parse(mexNode.content.toString())
179
+ } catch (error) {
180
+ logger.error({ err: error, node }, 'Failed to parse mex newsletter notification')
181
+ return
182
+ }
183
+ const operation = data?.operation
184
+ const updates = data?.updates
185
+ if (!updates || !operation) {
186
+ logger.warn({ data }, 'Invalid mex newsletter notification content')
187
+ return
188
+ }
189
+ logger.info({ operation, updates }, 'got mex newsletter notification')
190
+ switch (operation) {
191
+ case 'NotificationNewsletterUpdate':
192
+ for (const update of updates) {
193
+ if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
239
194
  ev.emit('newsletter-settings.update', {
240
- id: upd.id,
241
- update: { userSetting: upd.setting }
195
+ id: update.jid,
196
+ update: update.settings
242
197
  })
243
198
  }
244
- break
245
199
  }
246
- case 'NotificationNewsletterAdminPromote': {
247
- // legacy format kept for compat
248
- const upd = d.xwa2_notify_newsletter_on_admin_promote
249
- if (upd?.id) {
200
+ break
201
+ case 'NotificationNewsletterAdminPromote':
202
+ for (const update of updates) {
203
+ if (update.jid && update.user) {
250
204
  ev.emit('newsletter-participants.update', {
251
- id: upd.id,
205
+ id: update.jid,
252
206
  author: node.attrs.from,
253
- user: upd.user,
207
+ user: update.user,
254
208
  new_role: 'ADMIN',
255
209
  action: 'promote'
256
210
  })
257
211
  }
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
212
  }
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
- }
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
- }
213
+ break
214
+ default:
215
+ logger.info({ operation, data }, 'Unhandled mex newsletter notification')
216
+ break
302
217
  }
303
218
  }
304
219
  // Handles newsletter notifications
@@ -351,9 +266,7 @@ const makeMessagesRecvSocket = config => {
351
266
  })
352
267
  }
353
268
  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
269
+ case 'message':
357
270
  const plaintextNode = (0, WABinary_1.getBinaryNodeChild)(child, 'plaintext')
358
271
  if (plaintextNode?.content) {
359
272
  try {
@@ -366,16 +279,11 @@ const makeMessagesRecvSocket = config => {
366
279
  key: {
367
280
  remoteJid: from,
368
281
  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
282
+ fromMe: false // TODO: is this really true though
372
283
  },
373
284
  message: messageProto,
374
285
  messageTimestamp: +child.attrs.t
375
286
  }).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
287
  await upsertMessage(fullMessage, 'append')
380
288
  logger.info('Processed plaintext newsletter message')
381
289
  } catch (error) {
@@ -383,44 +291,23 @@ const makeMessagesRecvSocket = config => {
383
291
  }
384
292
  }
385
293
  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
- }
294
+ case 'live_updates':
295
+ // Live view count / engagement update
296
+ const liveCount = parseInt(child.attrs.count || child.content?.toString() || '0', 10)
297
+ ev.emit('newsletter.live-update', {
298
+ id: from,
299
+ server_id: child.attrs.message_id || child.attrs.server_id,
300
+ liveViewers: liveCount,
301
+ timestamp: child.attrs.t ? +child.attrs.t : undefined
302
+ })
410
303
  break
411
- }
412
- case 'pin': {
413
- const pinnedServerId = child.attrs.message_id || child.attrs.server_id
414
- const isPinned = child.attrs.action !== 'unpin'
304
+ case 'pin':
415
305
  ev.emit('newsletter.pin', {
416
306
  id: from,
417
- server_id: pinnedServerId,
418
- pinned: isPinned
307
+ server_id: child.attrs.message_id || child.attrs.server_id,
308
+ pinned: child.attrs.action !== 'unpin'
419
309
  })
420
- // Sync the pinnedMessage field on the newsletter metadata object
421
- ev.emit('newsletters.update', [{ id: from, pinnedMessage: isPinned ? pinnedServerId : null }])
422
310
  break
423
- }
424
311
  case 'category':
425
312
  ev.emit('newsletter-settings.update', {
426
313
  id: from,
@@ -440,88 +327,6 @@ const makeMessagesRecvSocket = config => {
440
327
  break
441
328
  }
442
329
  }
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
330
  const sendMessageAck = async (node, errorCode) => {
526
331
  const stanza = (0, stanza_ack_1.buildAckStanza)(node, errorCode, authState.creds.me.id)
527
332
  logger.debug({ recv: { tag: node.tag, attrs: node.attrs }, sent: stanza.attrs }, 'sent ack')
@@ -871,9 +676,8 @@ const makeMessagesRecvSocket = config => {
871
676
  id: node.attrs.id,
872
677
  t: node.attrs.t,
873
678
  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'
679
+ // ADD ERROR FIELD
680
+ error: '0'
877
681
  }
878
682
  },
879
683
  {
@@ -921,7 +725,7 @@ const makeMessagesRecvSocket = config => {
921
725
  const tcJid = await (0, tc_token_utils_1.resolveTcTokenJid)(normalizedJid, getLIDForPN)
922
726
  const tcTokenData = await authState.keys.get('tctoken', [tcJid])
923
727
  const senderTs = tcTokenData?.[tcJid]?.senderTimestamp
924
- if (senderTs == null || (0, tc_token_utils_1.isTcTokenExpired)(senderTs)) {
728
+ if (senderTs === null || senderTs === undefined || (0, tc_token_utils_1.isTcTokenExpired)(senderTs)) {
925
729
  return
926
730
  }
927
731
  logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken')
@@ -969,21 +773,8 @@ const makeMessagesRecvSocket = config => {
969
773
  }
970
774
  }
971
775
  }
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
776
  const handleGroupNotification = (fullNode, child, msg) => {
986
- // PN/LID: acting + affected participant already resolved below via participant_pn/lid attrs
777
+ // TODO: Support PN/LID (Here is only LID now)
987
778
  const actingParticipantLid = fullNode.attrs.participant
988
779
  const actingParticipantPn = fullNode.attrs.participant_pn
989
780
  const actingParticipantUsername = fullNode.attrs.participant_username
@@ -997,7 +788,6 @@ const makeMessagesRecvSocket = config => {
997
788
  msg.messageStubType = Types_1.WAMessageStubType.GROUP_CREATE
998
789
  msg.messageStubParameters = [metadata.subject]
999
790
  msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn }
1000
- storeParticipantLidPairs(metadata.participants)
1001
791
  ev.emit('chats.upsert', [
1002
792
  {
1003
793
  id: metadata.id,
@@ -1036,6 +826,7 @@ const makeMessagesRecvSocket = config => {
1036
826
  const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`
1037
827
  msg.messageStubType = Types_1.WAMessageStubType[stubType]
1038
828
  const participants = (0, WABinary_1.getBinaryNodeChildren)(child, 'participant').map(({ attrs }) => {
829
+ // TODO: Store LID MAPPINGS
1039
830
  return {
1040
831
  id: attrs.jid,
1041
832
  phoneNumber:
@@ -1044,8 +835,7 @@ const makeMessagesRecvSocket = config => {
1044
835
  : undefined,
1045
836
  lid: (0, WABinary_1.isPnUser)(attrs.jid) && (0, WABinary_1.isLidUser)(attrs.lid) ? attrs.lid : undefined,
1046
837
  username: attrs.participant_username || attrs.username || undefined,
1047
- admin: attrs.type || null,
1048
- uuid: attrs.uuid || attrs.participant_uuid || undefined
838
+ admin: attrs.type || null
1049
839
  }
1050
840
  })
1051
841
  if (
@@ -1058,7 +848,6 @@ const makeMessagesRecvSocket = config => {
1058
848
  ) {
1059
849
  msg.messageStubType = Types_1.WAMessageStubType.GROUP_PARTICIPANT_LEAVE
1060
850
  }
1061
- storeParticipantLidPairs(participants)
1062
851
  msg.messageStubParameters = participants.map(a => JSON.stringify(a))
1063
852
  break
1064
853
  case 'subject':
@@ -1108,51 +897,13 @@ const makeMessagesRecvSocket = config => {
1108
897
  break
1109
898
  case 'revoked_membership_requests':
1110
899
  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
- }
900
+ // TODO: LIDMAPPING SUPPORT
1114
901
  msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
1115
902
  msg.messageStubParameters = [
1116
903
  JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
1117
904
  isDenied ? 'revoked' : 'rejected'
1118
905
  ]
1119
906
  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
907
  }
1157
908
  }
1158
909
  const normalizeNotificationParticipant = async (jid, groupData) => {
@@ -1326,17 +1077,15 @@ const makeMessagesRecvSocket = config => {
1326
1077
  if (childTag === 'peer_device_presence') {
1327
1078
  const jid = attrs.jid || (0, WABinary_1.jidNormalizedUser)(node.attrs.from)
1328
1079
  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 })
1080
+ logger.debug({ jid, presence }, '[interop] peer_device_presence')
1081
+ ev.emit('presence.update', { id: jid, presences: { [jid]: { lastKnownPresence: presence } } })
1332
1082
  return
1333
1083
  }
1334
1084
 
1335
1085
  // fbid:thread / fbid:devices — Meta thread or device list association
1336
1086
  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 })
1087
+ logger.debug({ childTag, attrs, from: node.attrs.from }, '[interop] fbid association update')
1088
+ ev.emit('interop.fbid-update', { type: childTag, jid: node.attrs.from, attrs })
1340
1089
  return
1341
1090
  }
1342
1091
 
@@ -1363,24 +1112,10 @@ const makeMessagesRecvSocket = config => {
1363
1112
  await handleNewsletterNotification(node)
1364
1113
  break
1365
1114
  case 'mex':
1366
- await handleMexNotification(node)
1115
+ await handleMexNewsletterNotification(node)
1367
1116
  break
1368
1117
  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
1118
+ // TODO: HANDLE PARTICIPANT_PN
1384
1119
  const groupData = await getNotificationGroupData(node)
1385
1120
  handleGroupNotification(node, child, result)
1386
1121
 
@@ -1393,39 +1128,26 @@ const makeMessagesRecvSocket = config => {
1393
1128
  case 'encrypt':
1394
1129
  await handleEncryptNotification(node)
1395
1130
  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
1131
+ case 'devices':
1132
+ const devices = (0, WABinary_1.getBinaryNodeChildren)(child, 'device')
1133
+ const deviceOwnerJid = child.attrs.jid || child.attrs.lid
1410
1134
  const deviceData = devices.map(d => ({
1411
1135
  id: d.attrs.jid,
1412
1136
  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,
1137
+ keyIndex: d.attrs.key_index ? +d.attrs.key_index : undefined,
1415
1138
  platform: d.attrs.platform || undefined,
1416
1139
  isCompanion: d.attrs.companion === 'true' || undefined
1417
1140
  }))
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 })
1141
+ if (
1142
+ (0, WABinary_1.areJidsSameUser)(child.attrs.jid, authState.creds.me.id) ||
1143
+ (0, WABinary_1.areJidsSameUser)(child.attrs.lid, authState.creds.me.lid)
1144
+ ) {
1145
+ logger.info({ deviceData }, 'my own devices changed')
1146
+ ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf: true })
1147
+ } else if (deviceOwnerJid) {
1148
+ ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf: false })
1426
1149
  }
1427
1150
  break
1428
- }
1429
1151
  case 'server_sync':
1430
1152
  const update = (0, WABinary_1.getBinaryNodeChild)(node, 'collection')
1431
1153
  if (update) {
@@ -1433,18 +1155,13 @@ const makeMessagesRecvSocket = config => {
1433
1155
  await resyncAppState([name], false)
1434
1156
  }
1435
1157
  break
1436
- case 'picture': {
1158
+ case 'picture':
1437
1159
  const setPicture = (0, WABinary_1.getBinaryNodeChild)(node, 'set')
1438
1160
  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
- }
1161
+ // TODO: WAJIDHASH stuff proper support inhouse
1445
1162
  ev.emit('contacts.update', [
1446
1163
  {
1447
- id: pictureOwnerJid,
1164
+ id: (0, WABinary_1.jidNormalizedUser)(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
1448
1165
  imgUrl: setPicture ? 'changed' : 'removed'
1449
1166
  }
1450
1167
  ])
@@ -1461,7 +1178,6 @@ const makeMessagesRecvSocket = config => {
1461
1178
  }
1462
1179
  }
1463
1180
  break
1464
- }
1465
1181
  case 'account_sync':
1466
1182
  if (child.tag === 'disappearing_mode') {
1467
1183
  const newDuration = +child.attrs.duration
@@ -1477,60 +1193,18 @@ const makeMessagesRecvSocket = config => {
1477
1193
  }
1478
1194
  })
1479
1195
  } 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
- }
1196
+ const blocklists = (0, WABinary_1.getBinaryNodeChildren)(child, 'item')
1197
+ for (const { attrs } of blocklists) {
1198
+ const blocklist = [attrs.jid]
1199
+ const type = attrs.action === 'block' ? 'add' : 'remove'
1200
+ ev.emit('blocklist.update', { blocklist, type })
1516
1201
  }
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
1202
  }
1529
1203
  break
1530
- }
1531
1204
  case 'business':
1205
+ // SMB privacy / data-sharing settings sync push
1206
+ // (WhatsApp Web: WASmaxInBizSettingsSyncPrivacySettingRequest)
1532
1207
  if (child?.tag === 'privacy') {
1533
- // SMB privacy / data-sharing settings sync push
1534
1208
  ev.emit('business.privacy-settings-sync', {
1535
1209
  jid: from,
1536
1210
  categories: (0, WABinary_1.getBinaryNodeChildren)(child, 'category').map(c => ({
@@ -1539,32 +1213,6 @@ const makeMessagesRecvSocket = config => {
1539
1213
  })),
1540
1214
  attrs: child.attrs
1541
1215
  })
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
1216
  }
1569
1217
  break
1570
1218
  case 'hosted':
@@ -1838,10 +1486,11 @@ const makeMessagesRecvSocket = config => {
1838
1486
  const newValue = ((await msgRetryCache.get(key)) || 0) + 1
1839
1487
  await msgRetryCache.set(key, newValue)
1840
1488
  }
1841
- const sendMessagesAgain = async (key, ids, retryNode) => {
1489
+ const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
1842
1490
  const remoteJid = key.remoteJid
1843
1491
  const participant = key.participant || remoteJid
1844
1492
  const retryCount = +retryNode.attrs.count || 1
1493
+ const msgId = ids[0]
1845
1494
  // Try to get messages from cache first, then fallback to getMessage
1846
1495
  const msgs = []
1847
1496
  for (const id of ids) {
@@ -1873,12 +1522,53 @@ const makeMessagesRecvSocket = config => {
1873
1522
  // just re-send the message to everyone
1874
1523
  // prevents the first message decryption failure
1875
1524
  const sendToAll = !(0, WABinary_1.jidDecode)(participant)?.device
1525
+ const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
1526
+ let injectedFromBundle = false
1527
+ if (typeof signalRepository.injectE2ESession === 'function' && typeof Utils_1.extractE2ESessionFromRetryReceipt === 'function') {
1528
+ const bundle = Utils_1.extractE2ESessionFromRetryReceipt(receiptNode)
1529
+ if (bundle) {
1530
+ try {
1531
+ await signalRepository.injectE2ESession({ jid: participant, session: bundle })
1532
+ injectedFromBundle = true
1533
+ logger.debug({ participant, retryCount }, 'injected session from retry receipt key bundle')
1534
+ } catch (error) {
1535
+ logger.warn({ error, participant }, 'failed to inject session from retry receipt')
1536
+ }
1537
+ }
1538
+ }
1539
+ if (!injectedFromBundle && signalRepository.getSessionInfo) {
1540
+ const receivedRegId = (0, WABinary_1.getBinaryNodeChildUInt)(receiptNode, 'registration', 4)
1541
+ if (typeof receivedRegId === 'number' && Number.isInteger(receivedRegId)) {
1542
+ const info = await signalRepository.getSessionInfo(participant)
1543
+ if (info && info.registrationId !== 0 && info.registrationId !== receivedRegId) {
1544
+ logger.info(
1545
+ { participant, stored: info.registrationId, received: receivedRegId },
1546
+ 'reg id mismatch on retry without bundle, deleting session'
1547
+ )
1548
+ await authState.keys.set({ session: { [sessionId]: null } })
1549
+ }
1550
+ }
1551
+ }
1552
+ const BASE_KEY_CHECK_RETRY = 2
1553
+ if (msgId && messageRetryManager && signalRepository.getSessionInfo) {
1554
+ const info = await signalRepository.getSessionInfo(participant)
1555
+ if (info) {
1556
+ if (retryCount === BASE_KEY_CHECK_RETRY) {
1557
+ messageRetryManager.saveBaseKey(sessionId, msgId, info.baseKey)
1558
+ } else if (retryCount > BASE_KEY_CHECK_RETRY) {
1559
+ if (messageRetryManager.hasSameBaseKey(sessionId, msgId, info.baseKey)) {
1560
+ logger.warn({ participant, retryCount }, 'base key collision on retry, forcing fresh session')
1561
+ await authState.keys.set({ session: { [sessionId]: null } })
1562
+ }
1563
+ messageRetryManager.deleteBaseKey(sessionId, msgId)
1564
+ }
1565
+ }
1566
+ }
1876
1567
  // Check if we should recreate session for this retry
1877
1568
  let shouldRecreateSession = false
1878
1569
  let recreateReason = ''
1879
- if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
1570
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
1880
1571
  try {
1881
- const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
1882
1572
  const hasSession = await signalRepository.validateSession(participant)
1883
1573
  const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists)
1884
1574
  shouldRecreateSession = result.recreate
@@ -1891,7 +1581,9 @@ const makeMessagesRecvSocket = config => {
1891
1581
  logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry')
1892
1582
  }
1893
1583
  }
1894
- await assertSessions([participant], true)
1584
+ if (!injectedFromBundle) {
1585
+ await assertSessions([participant], true)
1586
+ }
1895
1587
  if ((0, WABinary_1.isJidGroup)(remoteJid)) {
1896
1588
  await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } })
1897
1589
  }
@@ -1940,51 +1632,6 @@ const makeMessagesRecvSocket = config => {
1940
1632
  const items = (0, WABinary_1.getBinaryNodeChildren)(content[0], 'item')
1941
1633
  ids.push(...items.map(i => i.attrs.id))
1942
1634
  }
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
1635
  try {
1989
1636
  await Promise.all([
1990
1637
  receiptMutex.mutex(async () => {
@@ -2029,7 +1676,7 @@ const makeMessagesRecvSocket = config => {
2029
1676
  try {
2030
1677
  await updateSendMessageAgainCount(ids[0], key.participant)
2031
1678
  logger.debug({ attrs, key }, 'recv retry request')
2032
- await sendMessagesAgain(key, ids, retryNode)
1679
+ await sendMessagesAgain(key, ids, retryNode, node)
2033
1680
  } catch (error) {
2034
1681
  logger.error(
2035
1682
  { key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' },
@@ -2419,19 +2066,6 @@ const makeMessagesRecvSocket = config => {
2419
2066
  }
2420
2067
  }
2421
2068
  }
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
2069
  const handleCall = async node => {
2436
2070
  try {
2437
2071
  const { attrs } = node
@@ -2459,45 +2093,10 @@ const makeMessagesRecvSocket = config => {
2459
2093
  }
2460
2094
  }
2461
2095
  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
2096
+ call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(infoChild, 'video')
2466
2097
  call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid']
2467
2098
  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
2099
  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
2100
  }
2502
2101
  const existingCall = await callOfferCache.get(call.id)
2503
2102
  // use existing call info to populate this event
@@ -2506,41 +2105,8 @@ const makeMessagesRecvSocket = config => {
2506
2105
  call.isGroup = existingCall.isGroup
2507
2106
  call.callerPn = call.callerPn || existingCall.callerPn
2508
2107
  }
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
2108
  // 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
- ) {
2109
+ if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
2544
2110
  await callOfferCache.del(call.id)
2545
2111
  }
2546
2112
  await normalizeCallEventJids(call, infoChild)
@@ -2555,24 +2121,7 @@ const makeMessagesRecvSocket = config => {
2555
2121
  // <mute_v2>, <transport>, … each with a call-id) instead of wrapped in <call>.
2556
2122
  // This additively handles those: emit a 'call' event for state stanzas and ack ALL
2557
2123
  // 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
- ])
2124
+ const CALL_STATE_TAGS = new Set(['offer', 'offer_notice', 'terminate', 'accept', 'reject', 'preaccept'])
2576
2125
  const handleStandaloneCallStanza = async node => {
2577
2126
  try {
2578
2127
  if (!CALL_STATE_TAGS.has(node.tag)) {
@@ -2592,24 +2141,9 @@ const makeMessagesRecvSocket = config => {
2592
2141
  status
2593
2142
  }
2594
2143
  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
2144
+ call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(node, 'video')
2599
2145
  call.isGroup = attrs.type === 'group' || !!attrs['group-jid']
2600
2146
  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
2147
  if (callId) {
2614
2148
  await callOfferCache.set(callId, call)
2615
2149
  }
@@ -2620,45 +2154,7 @@ const makeMessagesRecvSocket = config => {
2620
2154
  call.isGroup = existingCall.isGroup
2621
2155
  call.callerPn = call.callerPn || existingCall.callerPn
2622
2156
  }
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
- ) {
2157
+ if (callId && (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate')) {
2662
2158
  await callOfferCache.del(callId)
2663
2159
  }
2664
2160
  await normalizeCallEventJids(call, node)
@@ -2727,37 +2223,6 @@ const makeMessagesRecvSocket = config => {
2727
2223
  // }
2728
2224
  }
2729
2225
  }
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
2226
  /// processes a node with the given function
2762
2227
  /// and adds the task to the existing buffer if we're buffering events
2763
2228
  const processNodeWithBuffer = async (node, identifier, exec) => {
@@ -2809,33 +2274,8 @@ const makeMessagesRecvSocket = config => {
2809
2274
  })
2810
2275
  // additive: top-level call-signalling stanzas (some accounts send these instead of <call>)
2811
2276
  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'
2277
+ 'offer', 'offer_notice', 'terminate', 'accept', 'reject', 'preaccept',
2278
+ 'transport', 'video', 'duration', 'mute_v2', 'lobby', 'heartbeat', 'relaylatency', 'link_query'
2839
2279
  ]) {
2840
2280
  ws.on('CB:' + callTag, node => {
2841
2281
  nodelogger(node)
@@ -2850,18 +2290,6 @@ const makeMessagesRecvSocket = config => {
2850
2290
  nodelogger(node)
2851
2291
  await processNode('notification', node, 'handling notification', handleNotification)
2852
2292
  })
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
2293
  ws.on('CB:ack,class:message', node => {
2866
2294
  nodelogger(node)
2867
2295
  handleBadAck(node).catch(error => onUnexpectedError(error, 'handling bad ack'))