@badzz88/baileys 8.5.4 → 8.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +1 -1
  2. package/WAProto/WAProto.proto +75 -1256
  3. package/WAProto/index.d.ts +19729 -36122
  4. package/WAProto/index.js +132193 -178538
  5. package/engine-requirements.js +15 -7
  6. package/lib/Defaults/index.js +14 -22
  7. package/lib/Signal/Group/group_cipher.js +5 -5
  8. package/lib/Signal/Group/keyhelper.js +4 -4
  9. package/lib/Signal/Group/sender-chain-key.js +2 -2
  10. package/lib/Signal/Group/sender-key-distribution-message.js +1 -1
  11. package/lib/Signal/Group/sender-key-message.js +3 -3
  12. package/lib/Signal/Group/sender-key-record.js +1 -1
  13. package/lib/Signal/Group/sender-message-key.js +5 -5
  14. package/lib/Signal/libsignal.js +28 -39
  15. package/lib/Signal/lid-mapping.js +8 -28
  16. package/lib/Socket/aigroups.js +5 -5
  17. package/lib/Socket/business.js +11 -3
  18. package/lib/Socket/chats.js +319 -668
  19. package/lib/Socket/communities.js +89 -85
  20. package/lib/Socket/graphql.js +228 -435
  21. package/lib/Socket/groups.js +211 -132
  22. package/lib/Socket/index.js +7 -9
  23. package/lib/Socket/interactive-handler.js +60 -117
  24. package/lib/Socket/interop.js +175 -56
  25. package/lib/Socket/luxu.js +387 -0
  26. package/lib/Socket/managed-account.js +48 -79
  27. package/lib/Socket/messages-recv.js +340 -898
  28. package/lib/Socket/messages-send.js +160 -462
  29. package/lib/Socket/mex.js +53 -40
  30. package/lib/Socket/newsletter.js +295 -291
  31. package/lib/Socket/privacy.js +265 -77
  32. package/lib/Socket/registration.js +157 -164
  33. package/lib/Socket/socket.js +199 -181
  34. package/lib/Socket/text-router.js +83 -0
  35. package/lib/Socket/username.js +227 -144
  36. package/lib/Store/keyed-db.js +108 -0
  37. package/lib/Store/make-cache-manager-store.js +2 -2
  38. package/lib/Store/make-in-memory-store.js +141 -25
  39. package/lib/Types/Auth.js +3 -3
  40. package/lib/Types/Label.js +1 -1
  41. package/lib/Types/LabelAssociation.js +1 -1
  42. package/lib/Types/Mex.js +112 -0
  43. package/lib/Types/Newsletter.js +65 -96
  44. package/lib/Types/State.js +4 -4
  45. package/lib/Types/index.js +1 -1
  46. package/lib/Utils/auth-utils.js +57 -52
  47. package/lib/Utils/browser-utils.js +1 -1
  48. package/lib/Utils/business.js +6 -2
  49. package/lib/Utils/chat-utils.js +38 -102
  50. package/lib/Utils/command-loader.d.ts +31 -0
  51. package/lib/Utils/command-loader.js +100 -0
  52. package/lib/Utils/companion-reg-client-utils.js +42 -0
  53. package/lib/Utils/crypto.js +84 -54
  54. package/lib/Utils/curve25519-js.js +275 -0
  55. package/lib/Utils/decode-wa-message.js +83 -316
  56. package/lib/Utils/event-buffer.js +61 -37
  57. package/lib/Utils/generics.js +103 -157
  58. package/lib/Utils/group-history.js +12 -3
  59. package/lib/Utils/history.js +15 -15
  60. package/lib/Utils/index.js +5 -1
  61. package/lib/Utils/link-preview.js +8 -3
  62. package/lib/Utils/lt-hash.js +35 -2
  63. package/lib/Utils/make-mutex.js +1 -1
  64. package/lib/Utils/message-retry-manager.js +92 -27
  65. package/lib/Utils/messages-media.js +130 -159
  66. package/lib/Utils/messages.js +566 -144
  67. package/lib/Utils/meta-ai-msmsg.js +202 -62
  68. package/lib/Utils/native-bridge.js +82 -0
  69. package/lib/Utils/noise-handler.js +21 -19
  70. package/lib/Utils/offline-node-processor.js +8 -3
  71. package/lib/Utils/pre-key-manager.js +23 -13
  72. package/lib/Utils/process-message.js +106 -197
  73. package/lib/Utils/reporting-utils.js +2 -2
  74. package/lib/Utils/session-pool.d.ts +16 -0
  75. package/lib/Utils/session-pool.js +94 -0
  76. package/lib/Utils/signal.js +63 -28
  77. package/lib/Utils/stanza-ack.js +12 -39
  78. package/lib/Utils/sticker.d.ts +17 -0
  79. package/lib/Utils/sticker.js +145 -0
  80. package/lib/Utils/sync-action-utils.js +7 -4
  81. package/lib/Utils/tc-token-utils.js +25 -20
  82. package/lib/Utils/use-multi-file-auth-state.js +14 -30
  83. package/lib/Utils/validate-connection.js +19 -59
  84. package/lib/Utils/view-once-cache.d.ts +9 -0
  85. package/lib/Utils/view-once-cache.js +79 -0
  86. package/lib/WABinary/decode.js +16 -50
  87. package/lib/WABinary/encode.js +224 -54
  88. package/lib/WABinary/generic-utils.js +97 -7
  89. package/lib/WABinary/jid-utils.js +14 -27
  90. package/lib/WAM/encode.js +7 -7
  91. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +1 -58
  92. package/lib/WAUSync/Protocols/USyncContactProtocol.js +1 -7
  93. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +5 -13
  94. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +4 -1
  95. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +7 -20
  96. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +1 -1
  97. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +2 -2
  98. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +2 -7
  99. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +1 -1
  100. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +1 -2
  101. package/lib/WAUSync/Protocols/index.js +0 -1
  102. package/lib/WAUSync/USyncQuery.js +6 -77
  103. package/lib/WAUSync/USyncUser.js +8 -16
  104. package/lib/WAUSync/index.js +0 -1
  105. package/lib/antiban.js +854 -608
  106. package/lib/index.js +1 -1
  107. package/package.json +20 -16
@@ -37,6 +37,8 @@ const makeMessagesRecvSocket = config => {
37
37
  signalRepository,
38
38
  query,
39
39
  upsertMessage,
40
+ getUSyncDevices,
41
+ createParticipantNodes,
40
42
  resyncAppState,
41
43
  onUnexpectedError,
42
44
  assertSessions,
@@ -46,36 +48,33 @@ const makeMessagesRecvSocket = config => {
46
48
  uploadPreKeys,
47
49
  sendPeerDataOperationMessage,
48
50
  messageRetryManager,
49
- issuePrivacyTokens,
50
- getUSyncDevices,
51
- createParticipantNodes,
52
- newsletterServerIdCache
51
+ issuePrivacyTokens
53
52
  } = sock
54
53
  const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
55
-
54
+ // Track when the socket fully opens so pending pre-connect messages are treated as history
56
55
  const socketCreatedAt = Math.floor(Date.now() / 1000)
57
56
  let isConnected = false
58
-
57
+ /** this mutex ensures that each retryRequest will wait for the previous one to finish */
59
58
  const retryMutex = (0, make_mutex_1.makeMutex)()
60
59
  const msgRetryCache =
61
60
  config.msgRetryCounterCache ||
62
61
  new node_cache_1.default({
63
- stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY,
62
+ stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
64
63
  useClones: false
65
64
  })
66
65
  const callOfferCache =
67
66
  config.callOfferCache ||
68
67
  new node_cache_1.default({
69
- stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.CALL_OFFER,
68
+ stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
70
69
  useClones: false
71
70
  })
72
71
  const placeholderResendCache =
73
72
  config.placeholderResendCache ||
74
73
  new node_cache_1.default({
75
- stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY,
74
+ stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
76
75
  useClones: false
77
76
  })
78
-
77
+ // Debounce identity-change session refreshes per JID to avoid bursts
79
78
  const identityAssertDebounce = new node_cache_1.default({ stdTTL: 5, useClones: false })
80
79
  let sendActiveReceipts = false
81
80
  const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
@@ -102,8 +101,8 @@ const makeMessagesRecvSocket = config => {
102
101
  logger.debug({ messageKey }, 'already requested resend')
103
102
  return
104
103
  } else {
105
-
106
-
104
+ // Store original message data so PDO response handler can preserve
105
+ // metadata (LID details, timestamps, etc.) that the phone may omit
107
106
  await placeholderResendCache.set(messageKey?.id, msgData || true)
108
107
  }
109
108
  await (0, Utils_1.delay)(2000)
@@ -127,7 +126,11 @@ const makeMessagesRecvSocket = config => {
127
126
  }, 8000)
128
127
  return sendPeerDataOperationMessage(pdoMessage)
129
128
  }
130
-
129
+ /**
130
+ * Request a Waffle (Meta-account) linking nonce from the paired phone.
131
+ * The phone responds via a PeerDataOperationRequestResponseMessage containing
132
+ * a WaffleNonceFetchResponse with the nonce needed for Meta account linking.
133
+ */
131
134
  const requestWaffleNonce = async () => {
132
135
  if (!authState.creds.me?.id) {
133
136
  throw new boom_1.Boom('Not authenticated')
@@ -136,18 +139,27 @@ const makeMessagesRecvSocket = config => {
136
139
  peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.WAFFLE_LINKING_NONCE_FETCH
137
140
  })
138
141
  }
139
-
140
- const requestCompanionCanonicalNonce = async registrationTraceId => {
142
+ /**
143
+ * Request a Companion Canonical User nonce from the paired phone.
144
+ * Used during companion linking to canonicalize the user identity across devices.
145
+ * The phone responds with a CompanionCanonicalUserNonceFetchResponse (nonce + waFbid).
146
+ *
147
+ * @param {string} [registrationTraceId] - Optional trace ID for this registration attempt.
148
+ */
149
+ const requestCompanionCanonicalNonce = async (registrationTraceId) => {
141
150
  if (!authState.creds.me?.id) {
142
151
  throw new boom_1.Boom('Not authenticated')
143
152
  }
144
153
  return sendPeerDataOperationMessage({
145
154
  companionCanonicalUserNonceFetchRequest: registrationTraceId ? { registrationTraceId } : {},
146
- peerDataOperationRequestType:
147
- index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_CANONICAL_USER_NONCE_FETCH
155
+ peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_CANONICAL_USER_NONCE_FETCH
148
156
  })
149
157
  }
150
-
158
+ /**
159
+ * Request a Companion Meta nonce from the paired phone.
160
+ * Used during Meta-account companion linking flow.
161
+ * The phone responds with a CompanionMetaNonceFetchResponse (nonce).
162
+ */
151
163
  const requestCompanionMetaNonce = async () => {
152
164
  if (!authState.creds.me?.id) {
153
165
  throw new boom_1.Boom('Not authenticated')
@@ -156,138 +168,57 @@ const makeMessagesRecvSocket = config => {
156
168
  peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_META_NONCE_FETCH
157
169
  })
158
170
  }
159
-
160
-
161
- const handleMexNotification = async node => {
162
- const updateNodes = (0, WABinary_1.getBinaryNodeChildren)(node, 'update')
163
- if (!updateNodes.length) {
164
- logger.debug({ node }, 'mex notification with no update children')
171
+ // Handles mex newsletter notifications
172
+ const handleMexNewsletterNotification = async node => {
173
+ const mexNode = (0, WABinary_1.getBinaryNodeChild)(node, 'mex')
174
+ if (!mexNode?.content) {
175
+ logger.warn({ node }, 'Invalid mex newsletter notification')
165
176
  return
166
177
  }
167
- for (const updateNode of updateNodes) {
168
- const opName = updateNode.attrs?.op_name
169
- if (!opName) continue
170
- let payload
171
- try {
172
- const raw = updateNode.content?.text ?? updateNode.content?.toString?.()
173
- payload = raw ? JSON.parse(raw) : null
174
- } catch (e) {
175
- logger.error({ err: e, opName }, 'failed to parse mex update payload')
176
- continue
177
- }
178
- if (!payload?.data) {
179
- logger.debug({ opName }, 'mex update with no data field')
180
- continue
181
- }
182
- const d = payload.data
183
- switch (opName) {
184
- case 'NotificationNewsletterUpdate': {
185
-
186
- const upd = d.xwa2_notify_newsletter_on_metadata_update
187
- if (upd?.id) {
188
- ev.emit('newsletter-settings.update', {
189
- id: upd.id,
190
- update: upd.thread_metadata?.settings ?? {}
191
- })
192
- }
193
- break
194
- }
195
- case 'NotificationNewsletterJoin': {
196
-
197
- const upd = d.xwa2_notify_newsletter_on_join
198
- if (upd?.id) {
199
- ev.emit('newsletter-participants.update', {
200
- id: upd.id,
201
- author: node.attrs.from,
202
- user: (0, WABinary_1.jidNormalizedUser)(node.attrs.from),
203
- new_role: upd.viewer_metadata?.role ?? 'SUBSCRIBER',
204
- action: 'join',
205
- metadata: upd.thread_metadata
206
- })
207
- }
208
- break
209
- }
210
- case 'NotificationNewsletterMuteChange': {
211
-
212
- const upd = d.xwa2_notify_newsletter_on_mute_change
213
- if (upd?.id) {
214
- ev.emit('newsletter-settings.update', {
215
- id: upd.id,
216
- update: { mute: upd.mute }
217
- })
218
- }
219
- break
220
- }
221
- case 'NotificationNewsletterUserSettingChange': {
222
-
223
- const upd = d.xwa2_notify_newsletter_on_user_setting_change
224
- if (upd?.id && upd.setting) {
178
+ let data
179
+ try {
180
+ data = JSON.parse(mexNode.content.toString())
181
+ } catch (error) {
182
+ logger.error({ err: error, node }, 'Failed to parse mex newsletter notification')
183
+ return
184
+ }
185
+ const operation = data?.operation
186
+ const updates = data?.updates
187
+ if (!updates || !operation) {
188
+ logger.warn({ data }, 'Invalid mex newsletter notification content')
189
+ return
190
+ }
191
+ logger.info({ operation, updates }, 'got mex newsletter notification')
192
+ switch (operation) {
193
+ case 'NotificationNewsletterUpdate':
194
+ for (const update of updates) {
195
+ if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
225
196
  ev.emit('newsletter-settings.update', {
226
- id: upd.id,
227
- update: { userSetting: upd.setting }
197
+ id: update.jid,
198
+ update: update.settings
228
199
  })
229
200
  }
230
- break
231
201
  }
232
- case 'NotificationNewsletterAdminPromote': {
233
-
234
- const upd = d.xwa2_notify_newsletter_on_admin_promote
235
- if (upd?.id) {
202
+ break
203
+ case 'NotificationNewsletterAdminPromote':
204
+ for (const update of updates) {
205
+ if (update.jid && update.user) {
236
206
  ev.emit('newsletter-participants.update', {
237
- id: upd.id,
207
+ id: update.jid,
238
208
  author: node.attrs.from,
239
- user: upd.user,
209
+ user: update.user,
240
210
  new_role: 'ADMIN',
241
211
  action: 'promote'
242
212
  })
243
213
  }
244
- break
245
- }
246
- case 'NotificationGroupMemberLinkPropertyUpdate': {
247
-
248
- const upd = d.xwa2_notify_group_on_prop_change
249
- if (upd?.id && upd.properties?.member_link_mode !== undefined) {
250
- ev.emit('groups.update', [
251
- {
252
- id: upd.id,
253
- memberAddMode: upd.properties.member_link_mode
254
- }
255
- ])
256
- }
257
- break
258
- }
259
- case 'NotificationGroupLimitSharingPropertyUpdate': {
260
-
261
- const upd = d.xwa2_notify_group_on_prop_change
262
- if (upd?.id && upd.properties?.limit_sharing !== undefined) {
263
- ev.emit('groups.update', [
264
- {
265
- id: upd.id,
266
- limitSharing: upd.properties.limit_sharing
267
- }
268
- ])
269
- }
270
- break
271
214
  }
272
- case 'NotificationGroupMemberShareGroupHistoryModePropertyUpdate': {
273
-
274
- const upd = d.xwa2_notify_group_on_prop_change
275
- if (upd?.id && upd.properties?.member_share_group_history_mode !== undefined) {
276
- ev.emit('groups.update', [
277
- {
278
- id: upd.id,
279
- memberShareHistoryMode: upd.properties.member_share_group_history_mode
280
- }
281
- ])
282
- }
283
- break
284
- }
285
- default:
286
- logger.debug({ opName, from: node.attrs.from }, 'unhandled mex op')
287
- }
215
+ break
216
+ default:
217
+ logger.info({ operation, data }, 'Unhandled mex newsletter notification')
218
+ break
288
219
  }
289
220
  }
290
-
221
+ // Handles newsletter notifications
291
222
  const handleNewsletterNotification = async node => {
292
223
  const from = node.attrs.from
293
224
  const child = (0, WABinary_1.getAllBinaryNodeChildren)(node)[0]
@@ -337,9 +268,7 @@ const makeMessagesRecvSocket = config => {
337
268
  })
338
269
  }
339
270
  break
340
- case 'message': {
341
- const viewCount = child.attrs.view_count !== undefined ? +child.attrs.view_count : undefined
342
- const impressionCount = child.attrs.impression_count !== undefined ? +child.attrs.impression_count : undefined
271
+ case 'message':
343
272
  const plaintextNode = (0, WABinary_1.getBinaryNodeChild)(child, 'plaintext')
344
273
  if (plaintextNode?.content) {
345
274
  try {
@@ -352,16 +281,11 @@ const makeMessagesRecvSocket = config => {
352
281
  key: {
353
282
  remoteJid: from,
354
283
  id: child.attrs.message_id || child.attrs.server_id,
355
-
356
-
357
- fromMe: false
284
+ fromMe: false // TODO: is this really true though
358
285
  },
359
286
  message: messageProto,
360
287
  messageTimestamp: +child.attrs.t
361
288
  }).toJSON()
362
-
363
- if (viewCount !== undefined) fullMessage.views = viewCount
364
- if (impressionCount !== undefined) fullMessage.impressions = impressionCount
365
289
  await upsertMessage(fullMessage, 'append')
366
290
  logger.info('Processed plaintext newsletter message')
367
291
  } catch (error) {
@@ -369,44 +293,23 @@ const makeMessagesRecvSocket = config => {
369
293
  }
370
294
  }
371
295
  break
372
- }
373
- case 'live_updates': {
374
-
375
-
376
-
377
- const messagesNode = (0, WABinary_1.getBinaryNodeChild)(child, 'messages')
378
- const msgTs = messagesNode?.attrs?.t ? +messagesNode.attrs.t : undefined
379
- for (const msgNode of (0, WABinary_1.getBinaryNodeChildren)(messagesNode ?? child, 'message')) {
380
- const serverId = msgNode.attrs.server_id
381
- const fwdNode = (0, WABinary_1.getBinaryNodeChild)(msgNode, 'forwards_count')
382
- const forwardsCount = fwdNode?.attrs?.count !== undefined ? +fwdNode.attrs.count : undefined
383
- const reactionsNode = (0, WABinary_1.getBinaryNodeChild)(msgNode, 'reactions')
384
- const reactions = (0, WABinary_1.getBinaryNodeChildren)(reactionsNode ?? msgNode, 'reaction').map(r => ({
385
- code: r.attrs.code,
386
- count: +r.attrs.count
387
- }))
388
- ev.emit('newsletter.live-update', {
389
- id: from,
390
- server_id: serverId,
391
- timestamp: msgTs,
392
- forwardsCount,
393
- reactions
394
- })
395
- }
296
+ case 'live_updates':
297
+ // Live view count / engagement update
298
+ const liveCount = parseInt(child.attrs.count || child.content?.toString() || '0', 10)
299
+ ev.emit('newsletter.live-update', {
300
+ id: from,
301
+ server_id: child.attrs.message_id || child.attrs.server_id,
302
+ liveViewers: liveCount,
303
+ timestamp: child.attrs.t ? +child.attrs.t : undefined
304
+ })
396
305
  break
397
- }
398
- case 'pin': {
399
- const pinnedServerId = child.attrs.message_id || child.attrs.server_id
400
- const isPinned = child.attrs.action !== 'unpin'
306
+ case 'pin':
401
307
  ev.emit('newsletter.pin', {
402
308
  id: from,
403
- server_id: pinnedServerId,
404
- pinned: isPinned
309
+ server_id: child.attrs.message_id || child.attrs.server_id,
310
+ pinned: child.attrs.action !== 'unpin'
405
311
  })
406
-
407
- ev.emit('newsletters.update', [{ id: from, pinnedMessage: isPinned ? pinnedServerId : null }])
408
312
  break
409
- }
410
313
  case 'category':
411
314
  ev.emit('newsletter-settings.update', {
412
315
  id: from,
@@ -426,92 +329,8 @@ const makeMessagesRecvSocket = config => {
426
329
  break
427
330
  }
428
331
  }
429
-
430
-
431
- const handleNewsletterStatus = async node => {
432
- const { id, from, server_id, t, is_sender, offline, type, edit } = node.attrs
433
- const serverId = server_id ? +server_id : undefined
434
- const timestamp = t ? +t : undefined
435
- const isSender = is_sender === 'true'
436
- const offlineIndex = offline !== undefined ? +offline : undefined
437
-
438
- if (id && serverId != null) {
439
- newsletterServerIdCache?.set(id, serverId)
440
- }
441
-
442
-
443
- const metaNode = (0, WABinary_1.getBinaryNodeChild)(node, 'meta')
444
- const meta = metaNode
445
- ? {
446
- ...(metaNode.attrs.msg_edit_t ? { editedAt: +metaNode.attrs.msg_edit_t } : {}),
447
- ...(metaNode.attrs.original_msg_t ? { originalTimestamp: +metaNode.attrs.original_msg_t } : {}),
448
- ...(metaNode.attrs.interaction_type ? { interactionType: metaNode.attrs.interaction_type } : {}),
449
- ...(metaNode.attrs.parent_server_id ? { parentServerId: +metaNode.attrs.parent_server_id } : {}),
450
- ...(metaNode.attrs.response_server_id ? { responseServerId: +metaNode.attrs.response_server_id } : {})
451
- }
452
- : undefined
453
-
454
-
455
- const viewsNode = (0, WABinary_1.getBinaryNodeChild)(node, 'views_count')
456
- const viewsCount = viewsNode?.attrs?.count !== undefined ? +viewsNode.attrs.count : undefined
457
- const responsesNode = (0, WABinary_1.getBinaryNodeChild)(node, 'responses_count')
458
- const responsesCount = responsesNode?.attrs?.count !== undefined ? +responsesNode.attrs.count : undefined
459
- const reactionsNode = (0, WABinary_1.getBinaryNodeChild)(node, 'reactions')
460
- const reactionCounts = (0, WABinary_1.getBinaryNodeChildren)(reactionsNode ?? { content: [] }, 'reaction').map(
461
- r => ({ code: r.attrs.code, count: +r.attrs.count })
462
- )
463
-
464
- let content = null
465
- let mediaType = undefined
466
-
467
- if (type === 'reaction') {
468
- const reactionNode = (0, WABinary_1.getBinaryNodeChild)(node, 'reaction')
469
- content = { type: 'reaction', code: reactionNode?.attrs?.code }
470
- } else if (type === 'text' || type === 'media') {
471
- const plaintextNode = (0, WABinary_1.getBinaryNodeChild)(node, 'plaintext')
472
- if (type === 'media') mediaType = plaintextNode?.attrs?.mediatype
473
- if (edit === '7' || edit === '8') {
474
- content = { type: 'revoke', edit }
475
- } else if (plaintextNode?.content) {
476
- try {
477
- const buf = Buffer.isBuffer(plaintextNode.content)
478
- ? plaintextNode.content
479
- : Buffer.from(plaintextNode.content)
480
- const message = index_js_1.proto.Message.decode(buf).toJSON()
481
- content = { type, message, ...(mediaType ? { mediaType } : {}) }
482
- } catch (err) {
483
- logger.error({ err }, 'Failed to decode newsletter status plaintext')
484
- content = { type, raw: true, ...(mediaType ? { mediaType } : {}) }
485
- }
486
- }
487
- }
488
-
489
-
490
- const ackType = type === 'reaction' ? 'reaction' : edit ? 'revoke' : type
491
- await sendNode({
492
- tag: 'ack',
493
- attrs: { id, to: from, class: 'status', type: ackType || 'text' },
494
- content: undefined
495
- })
496
-
497
- ev.emit('newsletter.status', {
498
- id: from,
499
- messageId: id,
500
- serverId,
501
- timestamp,
502
- isSender,
503
- ...(offlineIndex !== undefined ? { offlineIndex } : {}),
504
- ...(meta ? { meta } : {}),
505
- ...(viewsCount !== undefined ? { viewsCount } : {}),
506
- ...(responsesCount !== undefined ? { responsesCount } : {}),
507
- ...(reactionCounts.length ? { reactionCounts } : {}),
508
- content
509
- })
510
- }
511
332
  const sendMessageAck = async (node, errorCode) => {
512
-
513
-
514
- const stanza = (0, stanza_ack_1.buildAckStanza)(node, errorCode, authState.creds.me?.id)
333
+ const stanza = (0, stanza_ack_1.buildAckStanza)(node, errorCode, authState.creds.me.id)
515
334
  logger.debug({ recv: { tag: node.tag, attrs: node.attrs }, sent: stanza.attrs }, 'sent ack')
516
335
  await sendNode(stanza)
517
336
  }
@@ -678,7 +497,15 @@ const makeMessagesRecvSocket = config => {
678
497
  await query(stanza)
679
498
  }
680
499
 
681
-
500
+ /**
501
+ * Re-encrypt call key for a device that reconnected mid-call.
502
+ * Source: OutgoingSignalingHandler.java enc_rekey / rekeyEncryptionTask
503
+ *
504
+ * @param {string} callId - Active call ID
505
+ * @param {string} callFrom - JID of the call creator
506
+ * @param {Buffer} encryptedKeyBytes - Re-encrypted call session key bytes
507
+ * @param {number} [count=0] - Retry counter (0–4)
508
+ */
682
509
  const rekeyCall = async (callId, callFrom, encryptedKeyBytes, count = 0) => {
683
510
  const stanza = {
684
511
  tag: 'call',
@@ -707,7 +534,14 @@ const makeMessagesRecvSocket = config => {
707
534
  await query(stanza)
708
535
  }
709
536
 
710
-
537
+ /**
538
+ * Join a call via an invite link.
539
+ * Source: OutgoingSignalingHandler.java link_join tag
540
+ *
541
+ * @param {string} callId - Call ID from the link
542
+ * @param {string} callCreator - JID of the call creator
543
+ * @param {string} linkToken - Token from the call link
544
+ */
711
545
  const joinCallLink = async (callId, callCreator, linkToken) => {
712
546
  const stanza = {
713
547
  tag: 'call',
@@ -730,7 +564,13 @@ const makeMessagesRecvSocket = config => {
730
564
  await query(stanza)
731
565
  }
732
566
 
733
-
567
+ /**
568
+ * Query info about a call link before joining.
569
+ * Source: OutgoingSignalingHandler.java link_query tag
570
+ *
571
+ * @param {string} callLinkCode - The call link code to query
572
+ * @param {string} to - JID to send the query to
573
+ */
734
574
  const queryCallLink = async (callLinkCode, to) => {
735
575
  const stanza = {
736
576
  tag: 'call',
@@ -753,19 +593,19 @@ const makeMessagesRecvSocket = config => {
753
593
  const { key: msgKey } = fullMessage
754
594
  const msgId = msgKey.id
755
595
  if (messageRetryManager) {
756
-
596
+ // Check if we've exceeded max retries using the new system
757
597
  if (messageRetryManager.hasExceededMaxRetries(msgId)) {
758
598
  logger.debug({ msgId }, 'reached retry limit with new retry manager, clearing')
759
599
  messageRetryManager.markRetryFailed(msgId)
760
600
  return
761
601
  }
762
-
602
+ // Increment retry count using new system
763
603
  const retryCount = messageRetryManager.incrementRetryCount(msgId)
764
-
604
+ // Use the new retry count for the rest of the logic
765
605
  const key = `${msgId}:${msgKey?.participant}`
766
606
  await msgRetryCache.set(key, retryCount)
767
607
  } else {
768
-
608
+ // Fallback to old system
769
609
  const key = `${msgId}:${msgKey?.participant}`
770
610
  let retryCount = (await msgRetryCache.get(key)) || 0
771
611
  if (retryCount >= maxMsgRetryCount) {
@@ -780,12 +620,12 @@ const makeMessagesRecvSocket = config => {
780
620
  const retryCount = (await msgRetryCache.get(key)) || 1
781
621
  const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
782
622
  const fromJid = node.attrs.from
783
-
623
+ // Check if we should recreate the session
784
624
  let shouldRecreateSession = false
785
625
  let recreateReason = ''
786
626
  if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
787
627
  try {
788
-
628
+ // Check if we have a session with this JID
789
629
  const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
790
630
  const hasSession = await signalRepository.validateSession(fromJid)
791
631
  const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists)
@@ -793,7 +633,7 @@ const makeMessagesRecvSocket = config => {
793
633
  recreateReason = result.reason
794
634
  if (shouldRecreateSession) {
795
635
  logger.debug({ fromJid, retryCount, reason: recreateReason }, 'recreating session for retry')
796
-
636
+ // Delete existing session to force recreation
797
637
  await authState.keys.set({ session: { [sessionId]: null } })
798
638
  forceIncludeKeys = true
799
639
  }
@@ -802,9 +642,9 @@ const makeMessagesRecvSocket = config => {
802
642
  }
803
643
  }
804
644
  if (retryCount <= 2) {
805
-
645
+ // Use new retry manager for phone requests if available
806
646
  if (messageRetryManager) {
807
-
647
+ // Schedule phone request with delay (like whatsmeow)
808
648
  messageRetryManager.schedulePhoneRequest(msgId, async () => {
809
649
  try {
810
650
  const requestId = await requestPlaceholderResend(msgKey)
@@ -816,7 +656,7 @@ const makeMessagesRecvSocket = config => {
816
656
  }
817
657
  })
818
658
  } else {
819
-
659
+ // Fallback to immediate request
820
660
  const msgId = await requestPlaceholderResend(msgKey)
821
661
  logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`)
822
662
  }
@@ -838,9 +678,8 @@ const makeMessagesRecvSocket = config => {
838
678
  id: node.attrs.id,
839
679
  t: node.attrs.t,
840
680
  v: '1',
841
-
842
-
843
- error: '1'
681
+ // ADD ERROR FIELD
682
+ error: '0'
844
683
  }
845
684
  },
846
685
  {
@@ -878,14 +717,17 @@ const makeMessagesRecvSocket = config => {
878
717
  logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt')
879
718
  }, authState?.creds?.me?.id || 'sendRetryRequest')
880
719
  }
881
-
720
+ /**
721
+ * Fire-and-forget tctoken re-issuance after a peer's device identity changed.
722
+ * Runs in parallel with the session refresh (not after it).
723
+ */
882
724
  const reissueTcTokenAfterIdentityChange = from => {
883
725
  void (async () => {
884
726
  const normalizedJid = (0, WABinary_1.jidNormalizedUser)(from)
885
727
  const tcJid = await (0, tc_token_utils_1.resolveTcTokenJid)(normalizedJid, getLIDForPN)
886
728
  const tcTokenData = await authState.keys.get('tctoken', [tcJid])
887
729
  const senderTs = tcTokenData?.[tcJid]?.senderTimestamp
888
- if (senderTs == null || (0, tc_token_utils_1.isTcTokenExpired)(senderTs)) {
730
+ if (senderTs === null || senderTs === undefined || (0, tc_token_utils_1.isTcTokenExpired)(senderTs)) {
889
731
  return
890
732
  }
891
733
  logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken')
@@ -933,21 +775,8 @@ const makeMessagesRecvSocket = config => {
933
775
  }
934
776
  }
935
777
  }
936
-
937
-
938
- const storeParticipantLidPairs = participants => {
939
- const pairs = []
940
- for (const p of participants || []) {
941
- if (p.phoneNumber) pairs.push({ lid: p.id, pn: p.phoneNumber })
942
- else if (p.lid) pairs.push({ lid: p.lid, pn: p.id })
943
- }
944
- if (!pairs.length) return
945
- signalRepository.lidMapping
946
- .storeLIDPNMappings(pairs)
947
- .catch(error => logger.debug({ error }, 'failed to store LID/PN mappings from group notification'))
948
- }
949
778
  const handleGroupNotification = (fullNode, child, msg) => {
950
-
779
+ // TODO: Support PN/LID (Here is only LID now)
951
780
  const actingParticipantLid = fullNode.attrs.participant
952
781
  const actingParticipantPn = fullNode.attrs.participant_pn
953
782
  const actingParticipantUsername = fullNode.attrs.participant_username
@@ -961,7 +790,6 @@ const makeMessagesRecvSocket = config => {
961
790
  msg.messageStubType = Types_1.WAMessageStubType.GROUP_CREATE
962
791
  msg.messageStubParameters = [metadata.subject]
963
792
  msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn }
964
- storeParticipantLidPairs(metadata.participants)
965
793
  ev.emit('chats.upsert', [
966
794
  {
967
795
  id: metadata.id,
@@ -1000,6 +828,7 @@ const makeMessagesRecvSocket = config => {
1000
828
  const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`
1001
829
  msg.messageStubType = Types_1.WAMessageStubType[stubType]
1002
830
  const participants = (0, WABinary_1.getBinaryNodeChildren)(child, 'participant').map(({ attrs }) => {
831
+ // TODO: Store LID MAPPINGS
1003
832
  return {
1004
833
  id: attrs.jid,
1005
834
  phoneNumber:
@@ -1008,21 +837,19 @@ const makeMessagesRecvSocket = config => {
1008
837
  : undefined,
1009
838
  lid: (0, WABinary_1.isPnUser)(attrs.jid) && (0, WABinary_1.isLidUser)(attrs.lid) ? attrs.lid : undefined,
1010
839
  username: attrs.participant_username || attrs.username || undefined,
1011
- admin: attrs.type || null,
1012
- uuid: attrs.uuid || attrs.participant_uuid || undefined
840
+ admin: attrs.type || null
1013
841
  }
1014
842
  })
1015
843
  if (
1016
844
  participants.length === 1 &&
1017
-
1018
-
845
+ // if recv. "remove" message and sender removed themselves
846
+ // mark as left
1019
847
  ((0, WABinary_1.areJidsSameUser)(participants[0].id, actingParticipantLid) ||
1020
848
  (0, WABinary_1.areJidsSameUser)(participants[0].id, actingParticipantPn)) &&
1021
849
  child.tag === 'remove'
1022
850
  ) {
1023
851
  msg.messageStubType = Types_1.WAMessageStubType.GROUP_PARTICIPANT_LEAVE
1024
852
  }
1025
- storeParticipantLidPairs(participants)
1026
853
  msg.messageStubParameters = participants.map(a => JSON.stringify(a))
1027
854
  break
1028
855
  case 'subject':
@@ -1072,51 +899,13 @@ const makeMessagesRecvSocket = config => {
1072
899
  break
1073
900
  case 'revoked_membership_requests':
1074
901
  const isDenied = (0, WABinary_1.areJidsSameUser)(affectedParticipantLid, actingParticipantLid)
1075
- if ((0, WABinary_1.isLidUser)(affectedParticipantLid) && (0, WABinary_1.isPnUser)(affectedParticipantPn)) {
1076
- storeParticipantLidPairs([{ id: affectedParticipantLid, phoneNumber: affectedParticipantPn }])
1077
- }
902
+ // TODO: LIDMAPPING SUPPORT
1078
903
  msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
1079
904
  msg.messageStubParameters = [
1080
905
  JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
1081
906
  isDenied ? 'revoked' : 'rejected'
1082
907
  ]
1083
908
  break
1084
- case 'sibling_link': {
1085
- const linkedGroupJid = (0, WABinary_1.getBinaryNodeChild)(child, 'group')?.attrs?.jid || child.attrs?.jid
1086
- ev.emit('groups.update', [
1087
- {
1088
- id: fullNode.attrs.from,
1089
- siblingGroupLinked: linkedGroupJid || true,
1090
- author: actingParticipantLid,
1091
- authorPn: actingParticipantPn
1092
- }
1093
- ])
1094
- break
1095
- }
1096
- case 'sibling_unlink': {
1097
- const unlinkedGroupJid = (0, WABinary_1.getBinaryNodeChild)(child, 'group')?.attrs?.jid || child.attrs?.jid
1098
- ev.emit('groups.update', [
1099
- {
1100
- id: fullNode.attrs.from,
1101
- siblingGroupUnlinked: unlinkedGroupJid || true,
1102
- author: actingParticipantLid,
1103
- authorPn: actingParticipantPn
1104
- }
1105
- ])
1106
- break
1107
- }
1108
- case 'clear_history': {
1109
- const historyClearTimestamp = child.attrs?.t ? +child.attrs.t : (0, Date)()
1110
- ev.emit('groups.update', [
1111
- {
1112
- id: fullNode.attrs.from,
1113
- historyClearTimestamp,
1114
- author: actingParticipantLid,
1115
- authorPn: actingParticipantPn
1116
- }
1117
- ])
1118
- break
1119
- }
1120
909
  }
1121
910
  }
1122
911
  const normalizeNotificationParticipant = async (jid, groupData) => {
@@ -1250,12 +1039,21 @@ const makeMessagesRecvSocket = config => {
1250
1039
  }
1251
1040
  }
1252
1041
 
1253
-
1042
+ /**
1043
+ * Handle incoming interop notifications (type="interop").
1044
+ *
1045
+ * The APK emits these for:
1046
+ * - stella_interop_enabled / stella_ios_enabled → feature-flag toggles
1047
+ * - ig_professional / ig_handle / followers → Instagram profile data updates
1048
+ * - fbid:thread / fbid:devices → Meta thread/device association
1049
+ * - peer_device_presence → interop contact online/offline
1050
+ * - group membership changes in interop groups → add/remove/promote events
1051
+ */
1254
1052
  const handleInteropNotification = (node, child) => {
1255
1053
  const childTag = child?.tag
1256
1054
  const attrs = child?.attrs || {}
1257
1055
 
1258
-
1056
+ // Feature flag: server toggled stella_interop_enabled or stella_ios_enabled
1259
1057
  if (childTag === 'feature') {
1260
1058
  const feature = attrs.name
1261
1059
  const enabled = attrs.value === 'true' || attrs.value === '1'
@@ -1264,7 +1062,7 @@ const makeMessagesRecvSocket = config => {
1264
1062
  return
1265
1063
  }
1266
1064
 
1267
-
1065
+ // Instagram profile data pushed for an interop contact
1268
1066
  if (childTag === 'ig_profile') {
1269
1067
  const contactUpdate = {
1270
1068
  id: (0, WABinary_1.jidNormalizedUser)(node.attrs.from),
@@ -1277,28 +1075,26 @@ const makeMessagesRecvSocket = config => {
1277
1075
  return
1278
1076
  }
1279
1077
 
1280
-
1078
+ // peer_device_presence — interop contact came online or went offline
1281
1079
  if (childTag === 'peer_device_presence') {
1282
1080
  const jid = attrs.jid || (0, WABinary_1.jidNormalizedUser)(node.attrs.from)
1283
1081
  const presence = attrs.type === 'unavailable' ? 'unavailable' : 'available'
1284
- const isIosInterop = !!authState.creds.interopIosEnabled
1285
- logger.debug({ jid, presence, isIosInterop }, '[interop] peer_device_presence')
1286
- ev.emit('presence.update', { id: jid, presences: { [jid]: { lastKnownPresence: presence } }, isIosInterop })
1082
+ logger.debug({ jid, presence }, '[interop] peer_device_presence')
1083
+ ev.emit('presence.update', { id: jid, presences: { [jid]: { lastKnownPresence: presence } } })
1287
1084
  return
1288
1085
  }
1289
1086
 
1290
-
1087
+ // fbid:thread / fbid:devices — Meta thread or device list association
1291
1088
  if (childTag === 'fbid_thread' || childTag === 'fbid_devices') {
1292
- const isIosInterop = !!authState.creds.interopIosEnabled
1293
- logger.debug({ childTag, attrs, from: node.attrs.from, isIosInterop }, '[interop] fbid association update')
1294
- ev.emit('interop.fbid-update', { type: childTag, jid: node.attrs.from, attrs, isIosInterop })
1089
+ logger.debug({ childTag, attrs, from: node.attrs.from }, '[interop] fbid association update')
1090
+ ev.emit('interop.fbid-update', { type: childTag, jid: node.attrs.from, attrs })
1295
1091
  return
1296
1092
  }
1297
1093
 
1298
-
1094
+ // Interop group membership changes (add / remove / promote / demote)
1299
1095
  if (childTag === 'participants') {
1300
1096
  const groupJid = node.attrs.from
1301
- const action = attrs.type
1097
+ const action = attrs.type // 'add' | 'remove' | 'promote' | 'demote'
1302
1098
  const participants = (0, WABinary_1.getBinaryNodeChildren)(child, 'participant').map(p => p.attrs.jid)
1303
1099
  logger.info({ groupJid, action, participants }, '[interop] group participants update')
1304
1100
  ev.emit('group-participants.update', { id: groupJid, participants, action })
@@ -1318,24 +1114,10 @@ const makeMessagesRecvSocket = config => {
1318
1114
  await handleNewsletterNotification(node)
1319
1115
  break
1320
1116
  case 'mex':
1321
- await handleMexNotification(node)
1117
+ await handleMexNewsletterNotification(node)
1322
1118
  break
1323
1119
  case 'w:gp2':
1324
- if (child?.tag === 'groups_dirty') {
1325
-
1326
-
1327
- const dirtyGroupJids = (0, WABinary_1.getBinaryNodeChildren)(child, 'group')
1328
- .map(g => g.attrs.jid)
1329
- .filter(Boolean)
1330
- if (dirtyGroupJids.length) {
1331
- ev.emit(
1332
- 'groups.update',
1333
- dirtyGroupJids.map(jid => ({ id: jid }))
1334
- )
1335
- }
1336
- break
1337
- }
1338
-
1120
+ // TODO: HANDLE PARTICIPANT_PN
1339
1121
  const groupData = await getNotificationGroupData(node)
1340
1122
  handleGroupNotification(node, child, result)
1341
1123
 
@@ -1348,39 +1130,26 @@ const makeMessagesRecvSocket = config => {
1348
1130
  case 'encrypt':
1349
1131
  await handleEncryptNotification(node)
1350
1132
  break
1351
- case 'devices': {
1352
-
1353
-
1354
-
1355
- const addNode = (0, WABinary_1.getBinaryNodeChild)(node, 'add')
1356
- const removeNode = (0, WABinary_1.getBinaryNodeChild)(node, 'remove')
1357
- const changedNode = addNode || removeNode
1358
- if (!changedNode) {
1359
- logger.debug({ node }, 'devices hash refresh, no add/remove list to report')
1360
- break
1361
- }
1362
- const isAdded = !!addNode
1363
- const devices = (0, WABinary_1.getBinaryNodeChildren)(changedNode, 'device')
1364
- const deviceOwnerJid = from
1133
+ case 'devices':
1134
+ const devices = (0, WABinary_1.getBinaryNodeChildren)(child, 'device')
1135
+ const deviceOwnerJid = child.attrs.jid || child.attrs.lid
1365
1136
  const deviceData = devices.map(d => ({
1366
1137
  id: d.attrs.jid,
1367
1138
  lid: d.attrs.lid,
1368
-
1369
- keyIndex: d.attrs['key-index'] ? +d.attrs['key-index'] : undefined,
1139
+ keyIndex: d.attrs.key_index ? +d.attrs.key_index : undefined,
1370
1140
  platform: d.attrs.platform || undefined,
1371
1141
  isCompanion: d.attrs.companion === 'true' || undefined
1372
1142
  }))
1373
- const isSelf =
1374
- (0, WABinary_1.areJidsSameUser)(from, authState.creds.me?.id) ||
1375
- (0, WABinary_1.areJidsSameUser)(from, authState.creds.me?.lid)
1376
- if (isSelf) {
1377
- logger.info({ deviceData, isAdded }, 'my own devices changed')
1378
- }
1379
- if (deviceOwnerJid) {
1380
- ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf, added: isAdded })
1143
+ if (
1144
+ (0, WABinary_1.areJidsSameUser)(child.attrs.jid, authState.creds.me.id) ||
1145
+ (0, WABinary_1.areJidsSameUser)(child.attrs.lid, authState.creds.me.lid)
1146
+ ) {
1147
+ logger.info({ deviceData }, 'my own devices changed')
1148
+ ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf: true })
1149
+ } else if (deviceOwnerJid) {
1150
+ ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf: false })
1381
1151
  }
1382
1152
  break
1383
- }
1384
1153
  case 'server_sync':
1385
1154
  const update = (0, WABinary_1.getBinaryNodeChild)(node, 'collection')
1386
1155
  if (update) {
@@ -1388,18 +1157,13 @@ const makeMessagesRecvSocket = config => {
1388
1157
  await resyncAppState([name], false)
1389
1158
  }
1390
1159
  break
1391
- case 'picture': {
1160
+ case 'picture':
1392
1161
  const setPicture = (0, WABinary_1.getBinaryNodeChild)(node, 'set')
1393
1162
  const delPicture = (0, WABinary_1.getBinaryNodeChild)(node, 'delete')
1394
- const pictureOwnerJid = (0, WABinary_1.jidNormalizedUser)(node?.attrs?.from)
1395
- if (!pictureOwnerJid) {
1396
-
1397
- logger.debug({ node }, 'picture notification missing "from", skipping contacts.update')
1398
- break
1399
- }
1163
+ // TODO: WAJIDHASH stuff proper support inhouse
1400
1164
  ev.emit('contacts.update', [
1401
1165
  {
1402
- id: pictureOwnerJid,
1166
+ id: (0, WABinary_1.jidNormalizedUser)(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
1403
1167
  imgUrl: setPicture ? 'changed' : 'removed'
1404
1168
  }
1405
1169
  ])
@@ -1416,7 +1180,6 @@ const makeMessagesRecvSocket = config => {
1416
1180
  }
1417
1181
  }
1418
1182
  break
1419
- }
1420
1183
  case 'account_sync':
1421
1184
  if (child.tag === 'disappearing_mode') {
1422
1185
  const newDuration = +child.attrs.duration
@@ -1432,60 +1195,18 @@ const makeMessagesRecvSocket = config => {
1432
1195
  }
1433
1196
  })
1434
1197
  } else if (child.tag === 'blocklist') {
1435
-
1436
-
1437
-
1438
- sock.fetchBlocklist().catch(error => logger.warn({ error }, 'failed to refresh blocklist after account_sync'))
1439
- } else if (child.tag === 'devices') {
1440
-
1441
-
1442
- const dhash = child.attrs.dhash
1443
- const deviceNodes = (0, WABinary_1.getBinaryNodeChildren)(child, 'device')
1444
- const keyIndexListNode = (0, WABinary_1.getBinaryNodeChild)(child, 'key-index-list')
1445
- const devices = deviceNodes.map(d => ({
1446
- jid: d.attrs.jid ? String(d.attrs.jid) : undefined,
1447
- keyIndex: d.attrs['key-index'] ? +d.attrs['key-index'] : undefined
1448
- }))
1449
- logger.info({ dhash, deviceCount: devices.length }, 'account devices list synced')
1450
- ev.emit('account.devices-synced', {
1451
- dhash,
1452
- devices,
1453
- keyIndexListTimestamp: keyIndexListNode?.attrs?.ts ? +keyIndexListNode.attrs.ts : undefined,
1454
- keyIndexList: keyIndexListNode?.content ? Buffer.from(keyIndexListNode.content) : undefined
1455
- })
1456
- }
1457
- break
1458
- case 'disappearing_mode': {
1459
-
1460
-
1461
- const newDuration = +child.attrs.duration
1462
- const timestamp = +child.attrs.t
1463
- logger.info({ newDuration }, 'updated account disappearing mode')
1464
- ev.emit('creds.update', {
1465
- accountSettings: {
1466
- ...authState.creds.accountSettings,
1467
- defaultDisappearingMode: {
1468
- ephemeralExpiration: newDuration,
1469
- ephemeralSettingTimestamp: timestamp
1470
- }
1198
+ const blocklists = (0, WABinary_1.getBinaryNodeChildren)(child, 'item')
1199
+ for (const { attrs } of blocklists) {
1200
+ const blocklist = [attrs.jid]
1201
+ const type = attrs.action === 'block' ? 'add' : 'remove'
1202
+ ev.emit('blocklist.update', { blocklist, type })
1471
1203
  }
1472
- })
1473
- break
1474
- }
1475
- case 'contacts': {
1476
-
1477
-
1478
- const updateChild = (0, WABinary_1.getBinaryNodeChild)(node, 'update')
1479
- if (updateChild?.attrs?.jid) {
1480
- ev.emit('contacts.update', [{ id: (0, WABinary_1.jidNormalizedUser)(updateChild.attrs.jid) }])
1481
- } else {
1482
- logger.debug({ node }, 'contacts list changed (hash/sync signal), no per-contact jid to update')
1483
1204
  }
1484
1205
  break
1485
- }
1486
1206
  case 'business':
1207
+ // SMB privacy / data-sharing settings sync push
1208
+ // (WhatsApp Web: WASmaxInBizSettingsSyncPrivacySettingRequest)
1487
1209
  if (child?.tag === 'privacy') {
1488
-
1489
1210
  ev.emit('business.privacy-settings-sync', {
1490
1211
  jid: from,
1491
1212
  categories: (0, WABinary_1.getBinaryNodeChildren)(child, 'category').map(c => ({
@@ -1494,37 +1215,11 @@ const makeMessagesRecvSocket = config => {
1494
1215
  })),
1495
1216
  attrs: child.attrs
1496
1217
  })
1497
- } else if (child?.tag === 'profile') {
1498
-
1499
- ev.emit('contacts.update', [
1500
- {
1501
- id: (0, WABinary_1.jidNormalizedUser)(child.attrs.jid || from),
1502
- businessProfileTag: child.attrs.tag
1503
- }
1504
- ])
1505
- } else if (child?.tag === 'verified_name') {
1506
-
1507
- ev.emit('contacts.update', [
1508
- {
1509
- id: (0, WABinary_1.jidNormalizedUser)(child.attrs.jid || from),
1510
- verifiedName: {
1511
- verifiedLevel: child.attrs.verified_level,
1512
- serial: child.attrs.serial,
1513
- version: child.attrs.v
1514
- }
1515
- }
1516
- ])
1517
- } else if (child?.tag === 'remove') {
1518
-
1519
-
1520
- if (child.attrs.jid) {
1521
- ev.emit('contacts.update', [{ id: (0, WABinary_1.jidNormalizedUser)(child.attrs.jid), isBusiness: false }])
1522
- }
1523
1218
  }
1524
1219
  break
1525
1220
  case 'hosted':
1526
-
1527
-
1221
+ // Coexistence (WhatsApp <-> Messenger/Instagram) onboarding/offboarding push
1222
+ // (WhatsApp Web: WASmaxInCoexistenceOnboarding/OffboardingNotification)
1528
1223
  if (child?.tag === 'onboarding_status') {
1529
1224
  ev.emit('coexistence.update', {
1530
1225
  jid: from,
@@ -1542,28 +1237,18 @@ const makeMessagesRecvSocket = config => {
1542
1237
  break
1543
1238
  case 'link_code_companion_reg':
1544
1239
  const linkCodeCompanionReg = (0, WABinary_1.getBinaryNodeChild)(node, 'link_code_companion_reg')
1545
- const refBuffer = (0, WABinary_1.getBinaryNodeChildBuffer)(linkCodeCompanionReg, 'link_code_pairing_ref')
1546
- const primaryIdentityPublicKeyBuffer = (0, WABinary_1.getBinaryNodeChildBuffer)(
1547
- linkCodeCompanionReg,
1548
- 'primary_identity_pub'
1240
+ const ref = toRequiredBuffer(
1241
+ (0, WABinary_1.getBinaryNodeChildBuffer)(linkCodeCompanionReg, 'link_code_pairing_ref')
1549
1242
  )
1550
- const primaryEphemeralPublicKeyWrappedBuffer = (0, WABinary_1.getBinaryNodeChildBuffer)(
1551
- linkCodeCompanionReg,
1552
- 'link_code_pairing_wrapped_primary_ephemeral_pub'
1243
+ const primaryIdentityPublicKey = toRequiredBuffer(
1244
+ (0, WABinary_1.getBinaryNodeChildBuffer)(linkCodeCompanionReg, 'primary_identity_pub')
1245
+ )
1246
+ const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(
1247
+ (0, WABinary_1.getBinaryNodeChildBuffer)(
1248
+ linkCodeCompanionReg,
1249
+ 'link_code_pairing_wrapped_primary_ephemeral_pub'
1250
+ )
1553
1251
  )
1554
- if (
1555
- refBuffer === undefined ||
1556
- primaryIdentityPublicKeyBuffer === undefined ||
1557
- primaryEphemeralPublicKeyWrappedBuffer === undefined
1558
- ) {
1559
-
1560
-
1561
- logger.debug({ id: node.attrs.id, type: node.attrs.type }, 'skipping empty link code companion registration')
1562
- break
1563
- }
1564
- const ref = toRequiredBuffer(refBuffer)
1565
- const primaryIdentityPublicKey = toRequiredBuffer(primaryIdentityPublicKeyBuffer)
1566
- const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(primaryEphemeralPublicKeyWrappedBuffer)
1567
1252
  const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped)
1568
1253
  const companionSharedKey = Utils_1.Curve.sharedKey(
1569
1254
  authState.creds.pairingEphemeralKeyPair.private,
@@ -1638,7 +1323,7 @@ const makeMessagesRecvSocket = config => {
1638
1323
  await handlePrivacyTokenNotification(node)
1639
1324
  break
1640
1325
  case 'security':
1641
-
1326
+ // Security notifications (compromised session, location change alerts)
1642
1327
  const securityType = child?.tag || node.attrs.type
1643
1328
  const securityData = {
1644
1329
  type: securityType,
@@ -1650,7 +1335,7 @@ const makeMessagesRecvSocket = config => {
1650
1335
  ev.emit('security.alert', securityData)
1651
1336
  break
1652
1337
  case 'identity':
1653
-
1338
+ // Identity change notifications — peer changed their identity key
1654
1339
  const identityJid = node.attrs.from
1655
1340
  const identityNewKey = child?.content ? Buffer.from(child.content) : undefined
1656
1341
  ev.emit('identity.update', {
@@ -1660,7 +1345,7 @@ const makeMessagesRecvSocket = config => {
1660
1345
  })
1661
1346
  break
1662
1347
  case 'server':
1663
-
1348
+ // Server-issued notifications (config changes, client config refresh)
1664
1349
  const serverTag = child?.tag
1665
1350
  if (serverTag === 'config') {
1666
1351
  const configData = {}
@@ -1669,7 +1354,7 @@ const makeMessagesRecvSocket = config => {
1669
1354
  }
1670
1355
  ev.emit('server.config', configData)
1671
1356
  } else if (serverTag === 'app_state_key') {
1672
-
1357
+ // Server pushed a new app-state key — trigger resync
1673
1358
  logger.info('server pushed app state key update')
1674
1359
  await resyncAppState(['critical_block', 'regular_high', 'regular_low'], false)
1675
1360
  } else {
@@ -1677,7 +1362,7 @@ const makeMessagesRecvSocket = config => {
1677
1362
  }
1678
1363
  break
1679
1364
  case 'status':
1680
-
1365
+ // Contact status (about) change notification
1681
1366
  const statusOwner = node.attrs.from
1682
1367
  const statusText = child?.content ? child.content.toString() : undefined
1683
1368
  if (statusOwner && statusText !== undefined) {
@@ -1690,7 +1375,7 @@ const makeMessagesRecvSocket = config => {
1690
1375
  }
1691
1376
  break
1692
1377
  case 'usync':
1693
-
1378
+ // USync result push from server
1694
1379
  const usyncResults = (0, WABinary_1.getBinaryNodeChildren)(child || node, 'user')
1695
1380
  if (usyncResults.length) {
1696
1381
  const updates = usyncResults
@@ -1707,12 +1392,12 @@ const makeMessagesRecvSocket = config => {
1707
1392
  }
1708
1393
  break
1709
1394
  case 'interop':
1710
-
1711
-
1712
-
1713
-
1714
-
1715
-
1395
+ // Interop-related server notifications — covers:
1396
+ // stella_interop_enabled / stella_ios_enabled feature flags
1397
+ // ig_professional / ig_handle / followers (Instagram account data)
1398
+ // fbid:thread / fbid:devices (Meta thread/device references)
1399
+ // peer_device_presence updates
1400
+ // group membership changes in interop groups
1716
1401
  handleInteropNotification(node, child)
1717
1402
  break
1718
1403
  }
@@ -1720,7 +1405,10 @@ const makeMessagesRecvSocket = config => {
1720
1405
  return result
1721
1406
  }
1722
1407
  }
1723
-
1408
+ /**
1409
+ * In-memory cache of storage JIDs with stored tctokens, seeded from the persisted index.
1410
+ * Used to coalesce writes during a session; pruning always re-reads the persisted index.
1411
+ */
1724
1412
  const tcTokenKnownJids = new Set()
1725
1413
  const tcTokenIndexLoaded = (async () => {
1726
1414
  try {
@@ -1761,7 +1449,7 @@ const makeMessagesRecvSocket = config => {
1761
1449
  const tokensNode = (0, WABinary_1.getBinaryNodeChild)(node, 'tokens')
1762
1450
  if (!tokensNode) return
1763
1451
  const from = (0, WABinary_1.jidNormalizedUser)(node.attrs.from)
1764
-
1452
+ // WA Web uses: senderLid ?? toLid(from) for the storage key
1765
1453
  const senderLid =
1766
1454
  node.attrs.sender_lid && (0, WABinary_1.isLidUser)((0, WABinary_1.jidNormalizedUser)(node.attrs.sender_lid))
1767
1455
  ? (0, WABinary_1.jidNormalizedUser)(node.attrs.sender_lid)
@@ -1800,30 +1488,31 @@ const makeMessagesRecvSocket = config => {
1800
1488
  const newValue = ((await msgRetryCache.get(key)) || 0) + 1
1801
1489
  await msgRetryCache.set(key, newValue)
1802
1490
  }
1803
- const sendMessagesAgain = async (key, ids, retryNode) => {
1491
+ const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
1804
1492
  const remoteJid = key.remoteJid
1805
1493
  const participant = key.participant || remoteJid
1806
1494
  const retryCount = +retryNode.attrs.count || 1
1807
-
1495
+ const msgId = ids[0]
1496
+ // Try to get messages from cache first, then fallback to getMessage
1808
1497
  const msgs = []
1809
1498
  for (const id of ids) {
1810
1499
  let msg
1811
-
1500
+ // Try to get from retry cache first if enabled
1812
1501
  if (messageRetryManager) {
1813
1502
  const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id)
1814
1503
  if (cachedMsg) {
1815
1504
  msg = cachedMsg.message
1816
1505
  logger.debug({ jid: remoteJid, id }, 'found message in retry cache')
1817
-
1506
+ // Mark retry as successful since we found the message
1818
1507
  messageRetryManager.markRetrySuccess(id)
1819
1508
  }
1820
1509
  }
1821
-
1510
+ // Fallback to getMessage if not found in cache
1822
1511
  if (!msg) {
1823
1512
  msg = await getMessage({ ...key, id })
1824
1513
  if (msg) {
1825
1514
  logger.debug({ jid: remoteJid, id }, 'found message via getMessage')
1826
-
1515
+ // Also mark as successful if found via getMessage
1827
1516
  if (messageRetryManager) {
1828
1517
  messageRetryManager.markRetrySuccess(id)
1829
1518
  }
@@ -1831,16 +1520,57 @@ const makeMessagesRecvSocket = config => {
1831
1520
  }
1832
1521
  msgs.push(msg)
1833
1522
  }
1834
-
1835
-
1836
-
1523
+ // if it's the primary jid sending the request
1524
+ // just re-send the message to everyone
1525
+ // prevents the first message decryption failure
1837
1526
  const sendToAll = !(0, WABinary_1.jidDecode)(participant)?.device
1838
-
1527
+ const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
1528
+ let injectedFromBundle = false
1529
+ if (typeof signalRepository.injectE2ESession === 'function' && typeof Utils_1.extractE2ESessionFromRetryReceipt === 'function') {
1530
+ const bundle = Utils_1.extractE2ESessionFromRetryReceipt(receiptNode)
1531
+ if (bundle) {
1532
+ try {
1533
+ await signalRepository.injectE2ESession({ jid: participant, session: bundle })
1534
+ injectedFromBundle = true
1535
+ logger.debug({ participant, retryCount }, 'injected session from retry receipt key bundle')
1536
+ } catch (error) {
1537
+ logger.warn({ error, participant }, 'failed to inject session from retry receipt')
1538
+ }
1539
+ }
1540
+ }
1541
+ if (!injectedFromBundle && signalRepository.getSessionInfo) {
1542
+ const receivedRegId = (0, WABinary_1.getBinaryNodeChildUInt)(receiptNode, 'registration', 4)
1543
+ if (typeof receivedRegId === 'number' && Number.isInteger(receivedRegId)) {
1544
+ const info = await signalRepository.getSessionInfo(participant)
1545
+ if (info && info.registrationId !== 0 && info.registrationId !== receivedRegId) {
1546
+ logger.info(
1547
+ { participant, stored: info.registrationId, received: receivedRegId },
1548
+ 'reg id mismatch on retry without bundle, deleting session'
1549
+ )
1550
+ await authState.keys.set({ session: { [sessionId]: null } })
1551
+ }
1552
+ }
1553
+ }
1554
+ const BASE_KEY_CHECK_RETRY = 2
1555
+ if (msgId && messageRetryManager && signalRepository.getSessionInfo) {
1556
+ const info = await signalRepository.getSessionInfo(participant)
1557
+ if (info) {
1558
+ if (retryCount === BASE_KEY_CHECK_RETRY) {
1559
+ messageRetryManager.saveBaseKey(sessionId, msgId, info.baseKey)
1560
+ } else if (retryCount > BASE_KEY_CHECK_RETRY) {
1561
+ if (messageRetryManager.hasSameBaseKey(sessionId, msgId, info.baseKey)) {
1562
+ logger.warn({ participant, retryCount }, 'base key collision on retry, forcing fresh session')
1563
+ await authState.keys.set({ session: { [sessionId]: null } })
1564
+ }
1565
+ messageRetryManager.deleteBaseKey(sessionId, msgId)
1566
+ }
1567
+ }
1568
+ }
1569
+ // Check if we should recreate session for this retry
1839
1570
  let shouldRecreateSession = false
1840
1571
  let recreateReason = ''
1841
- if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
1572
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
1842
1573
  try {
1843
- const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
1844
1574
  const hasSession = await signalRepository.validateSession(participant)
1845
1575
  const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists)
1846
1576
  shouldRecreateSession = result.recreate
@@ -1853,7 +1583,9 @@ const makeMessagesRecvSocket = config => {
1853
1583
  logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry')
1854
1584
  }
1855
1585
  }
1856
- await assertSessions([participant], true)
1586
+ if (!injectedFromBundle) {
1587
+ await assertSessions([participant], true)
1588
+ }
1857
1589
  if ((0, WABinary_1.isJidGroup)(remoteJid)) {
1858
1590
  await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } })
1859
1591
  }
@@ -1902,59 +1634,14 @@ const makeMessagesRecvSocket = config => {
1902
1634
  const items = (0, WABinary_1.getBinaryNodeChildren)(content[0], 'item')
1903
1635
  ids.push(...items.map(i => i.attrs.id))
1904
1636
  }
1905
-
1906
-
1907
- if (attrs.type === 'media-retry') {
1908
- const mediaRetryNode =
1909
- (0, WABinary_1.getBinaryNodeChild)(node, 'media-retry') ||
1910
- (0, WABinary_1.getBinaryNodeChild)(node, 'media_retry')
1911
- ev.emit('messages.media-retry', {
1912
- ids,
1913
- from: attrs.from,
1914
- participant: attrs.participant,
1915
- t: attrs.t,
1916
- retryAttrs: mediaRetryNode?.attrs
1917
- })
1918
- await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack media-retry receipt'))
1919
- return
1920
- }
1921
-
1922
-
1923
- if (attrs.type === 'server-error') {
1924
- const errorNode = (0, WABinary_1.getBinaryNodeChild)(node, 'error')
1925
- ev.emit('messages.server-error', {
1926
- ids,
1927
- from: attrs.from,
1928
- participant: attrs.participant,
1929
- t: attrs.t,
1930
- errorCode: errorNode?.attrs?.code || attrs.error_code,
1931
- errorText: errorNode?.attrs?.text || errorNode?.content?.toString?.()
1932
- })
1933
- await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack server-error receipt'))
1934
- return
1935
- }
1936
-
1937
-
1938
-
1939
- if (attrs.receipt_agg) {
1940
- ev.emit('receipt.batched', {
1941
- ids,
1942
- from: attrs.from,
1943
- participant: attrs.participant,
1944
- type: attrs.type,
1945
- t: attrs.t,
1946
- receiptAgg: attrs.receipt_agg
1947
- })
1948
-
1949
- }
1950
1637
  try {
1951
1638
  await Promise.all([
1952
1639
  receiptMutex.mutex(async () => {
1953
1640
  const status = (0, Utils_1.getStatusFromReceiptType)(attrs.type)
1954
1641
  if (
1955
1642
  typeof status !== 'undefined' &&
1956
-
1957
-
1643
+ // basically, we only want to know when a message from us has been delivered to/read by the other person
1644
+ // or another device of ours has read some messages
1958
1645
  (status >= index_js_1.proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)
1959
1646
  ) {
1960
1647
  if ((0, WABinary_1.isJidGroup)(remoteJid) || (0, WABinary_1.isJidStatusBroadcast)(remoteJid)) {
@@ -1983,7 +1670,7 @@ const makeMessagesRecvSocket = config => {
1983
1670
  }
1984
1671
  }
1985
1672
  if (attrs.type === 'retry') {
1986
-
1673
+ // correctly set who is asking for the retry
1987
1674
  key.participant = key.participant || attrs.from
1988
1675
  const retryNode = (0, WABinary_1.getBinaryNodeChild)(node, 'retry')
1989
1676
  if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
@@ -1991,7 +1678,7 @@ const makeMessagesRecvSocket = config => {
1991
1678
  try {
1992
1679
  await updateSendMessageAgainCount(ids[0], key.participant)
1993
1680
  logger.debug({ attrs, key }, 'recv retry request')
1994
- await sendMessagesAgain(key, ids, retryNode)
1681
+ await sendMessagesAgain(key, ids, retryNode, node)
1995
1682
  } catch (error) {
1996
1683
  logger.error(
1997
1684
  { key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' },
@@ -2090,11 +1777,11 @@ const makeMessagesRecvSocket = config => {
2090
1777
  const groupJid = node.attrs.from
2091
1778
  const communityJid = linkedParentMap[groupJid]
2092
1779
  const encNode = (0, WABinary_1.getBinaryNodeChild)(node, 'enc')
2093
-
1780
+ // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
2094
1781
  if (encNode?.attrs.type === 'msmsg') {
2095
-
2096
-
2097
-
1782
+ // await sendMessageAck(node, Utils_1.NACK_REASONS.MissingMessageSecret)
1783
+ // return
1784
+ // Pre-populate botMessageSecrets from store so msmsg can be decrypted after restart
2098
1785
  if (getMessage) {
2099
1786
  const metaNode = (0, WABinary_1.getBinaryNodeChild)(node, 'meta')
2100
1787
  const targetId = metaNode?.attrs?.target_id
@@ -2132,7 +1819,7 @@ const makeMessagesRecvSocket = config => {
2132
1819
  )
2133
1820
  }
2134
1821
  const alt = msg.key.participantAlt || msg.key.remoteJidAlt
2135
-
1822
+ // store new mappings we didn't have before
2136
1823
  if (!!alt) {
2137
1824
  const altServer = (0, WABinary_1.jidDecode)(alt)?.server
2138
1825
  const primaryJid = msg.key.participant || msg.key.remoteJid
@@ -2145,32 +1832,6 @@ const makeMessagesRecvSocket = config => {
2145
1832
  await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }])
2146
1833
  await signalRepository.migrateSession(alt, primaryJid)
2147
1834
  }
2148
- } else if (msg.key.addressingMode === 'lid') {
2149
-
2150
-
2151
-
2152
-
2153
- const primaryJid = msg.key.participant || msg.key.remoteJid
2154
- if ((0, WABinary_1.isLidUser)(primaryJid)) {
2155
- try {
2156
- const pn = await signalRepository.lidMapping.getPNForLID(primaryJid)
2157
-
2158
-
2159
-
2160
- const pnJid = pn ? (0, WABinary_1.jidNormalizedUser)(pn) : ''
2161
- if (pnJid) {
2162
- if ((0, WABinary_1.isJidGroup)(msg.key.remoteJid)) {
2163
- msg.key.participantAlt = pnJid
2164
- } else {
2165
- msg.key.remoteJidAlt = pnJid
2166
- }
2167
- logger.debug({ lid: primaryJid, pn: pnJid }, 'recovered alt JID from LID mapping store')
2168
- }
2169
- } catch (err) {
2170
-
2171
- logger.warn({ err, lid: primaryJid }, 'failed to recover alt JID from LID mapping store')
2172
- }
2173
- }
2174
1835
  }
2175
1836
  await messageMutex.mutex(async () => {
2176
1837
  await decrypt()
@@ -2191,7 +1852,7 @@ const makeMessagesRecvSocket = config => {
2191
1852
  if (msg.key?.remoteJid && msg.key?.id && msg.message && messageRetryManager) {
2192
1853
  messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message)
2193
1854
  }
2194
-
1855
+ // message failed to decrypt
2195
1856
  if (msg.messageStubType === index_js_1.proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
2196
1857
  if (msg?.messageStubParameters?.[0] === Utils_1.MISSING_KEYS_ERROR_TEXT) {
2197
1858
  if (isInteropNode) logger.warn({ id: msg.key.id }, '[interop] decrypt failed: MISSING_KEYS')
@@ -2199,8 +1860,8 @@ const makeMessagesRecvSocket = config => {
2199
1860
  return sendMessageAck(node, Utils_1.NACK_REASONS.ParsingError)
2200
1861
  }
2201
1862
  if (msg.messageStubParameters?.[0] === Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT) {
2202
-
2203
-
1863
+ // Message arrived without encryption (e.g. CTWA ads messages).
1864
+ // Check if this is eligible for placeholder resend (matching WA Web filters).
2204
1865
  const unavailableNode = (0, WABinary_1.getBinaryNodeChild)(node, 'unavailable')
2205
1866
  const unavailableType = unavailableNode?.attrs?.type
2206
1867
  if (
@@ -2221,18 +1882,18 @@ const makeMessagesRecvSocket = config => {
2221
1882
  acked = true
2222
1883
  return sendMessageAck(node)
2223
1884
  }
2224
-
2225
-
2226
-
2227
-
1885
+ // Request the real content from the phone via placeholder resend PDO.
1886
+ // Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg),
1887
+ // and store the requestId in stubParameters[1] so users can correlate
1888
+ // with the incoming PDO response event.
2228
1889
  const cleanKey = {
2229
1890
  remoteJid: msg.key.remoteJid,
2230
1891
  fromMe: msg.key.fromMe,
2231
1892
  id: msg.key.id,
2232
1893
  participant: msg.key.participant
2233
1894
  }
2234
-
2235
-
1895
+ // Cache the original message metadata so the PDO response handler
1896
+ // can preserve key fields (LID details etc.) that the phone may omit
2236
1897
  const msgData = {
2237
1898
  key: msg.key,
2238
1899
  messageTimestamp: msg.messageTimestamp,
@@ -2257,9 +1918,9 @@ const makeMessagesRecvSocket = config => {
2257
1918
  })
2258
1919
  acked = true
2259
1920
  await sendMessageAck(node)
2260
-
1921
+ // Don't return — fall through to upsertMessage so the stub is emitted
2261
1922
  } else {
2262
-
1923
+ // Skip retry for expired status messages (>24h old)
2263
1924
  if ((0, WABinary_1.isJidStatusBroadcast)(msg.key.remoteJid)) {
2264
1925
  const messageAge = (0, Utils_1.unixTimestampSeconds)() - (0, Utils_1.toNumber)(msg.messageTimestamp)
2265
1926
  if (messageAge > Defaults_1.STATUS_EXPIRY_SECONDS) {
@@ -2274,14 +1935,14 @@ const makeMessagesRecvSocket = config => {
2274
1935
  const errorMessage = msg?.messageStubParameters?.[0] || ''
2275
1936
  const isPreKeyError = errorMessage.includes('PreKey')
2276
1937
  logger.debug(`[handleMessage] Attempting retry request for failed decryption`)
2277
-
1938
+ // Handle both pre-key and normal retries in single mutex
2278
1939
  await retryMutex.mutex(async () => {
2279
1940
  try {
2280
1941
  if (!ws.isOpen) {
2281
1942
  logger.debug({ node }, 'Connection closed, skipping retry')
2282
1943
  return
2283
1944
  }
2284
-
1945
+ // Handle pre-key errors with upload and delay
2285
1946
  if (isPreKeyError) {
2286
1947
  logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying')
2287
1948
  try {
@@ -2300,7 +1961,7 @@ const makeMessagesRecvSocket = config => {
2300
1961
  }
2301
1962
  } catch (err) {
2302
1963
  logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry')
2303
-
1964
+ // Still attempt retry even if pre-key upload failed
2304
1965
  try {
2305
1966
  const encNode = (0, WABinary_1.getBinaryNodeChild)(node, 'enc')
2306
1967
  await sendRetryRequest(node, !encNode)
@@ -2318,34 +1979,34 @@ const makeMessagesRecvSocket = config => {
2318
1979
  }
2319
1980
  const isNewsletter = (0, WABinary_1.isJidNewsletter)(msg.key.remoteJid)
2320
1981
  if (!isNewsletter) {
2321
-
1982
+ // no type in the receipt => message delivered
2322
1983
  let type = undefined
2323
1984
  let participant = msg.key.participant
2324
1985
  if (communityJid) {
2325
1986
  msg.communityJid = communityJid
2326
1987
  }
2327
1988
  if (category === 'peer') {
2328
-
1989
+ // special peer message
2329
1990
  type = 'peer_msg'
2330
1991
  } else if (msg.key.fromMe) {
2331
-
1992
+ // message was sent by us from a different device
2332
1993
  type = 'sender'
2333
-
1994
+ // need to specially handle this case
2334
1995
  if ((0, WABinary_1.isLidUser)(msg.key.remoteJid) || (0, WABinary_1.isLidUser)(msg.key.remoteJidAlt)) {
2335
- participant = author
1996
+ participant = author // TODO: investigate sending receipts to LIDs and not PNs
2336
1997
  }
2337
1998
  } else if (!sendActiveReceipts) {
2338
1999
  type = 'inactive'
2339
2000
  }
2340
2001
  acked = true
2341
-
2002
+ // Pass sts from the original stanza for interop contacts (BirdyChat/Haiket)
2342
2003
  const interopSts = (0, WABinary_1.isInteropUser)(msg.key.remoteJid) ? node.attrs.sts : undefined
2343
2004
  await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type, interopSts)
2344
-
2005
+ // send ack for history message
2345
2006
  const isAnyHistoryMsg = (0, Utils_1.getHistoryMsg)(msg.message)
2346
2007
  if (isAnyHistoryMsg) {
2347
2008
  const jid = (0, WABinary_1.jidNormalizedUser)(msg.key.remoteJid)
2348
- await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync')
2009
+ await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync') // TODO: investigate
2349
2010
  }
2350
2011
  } else {
2351
2012
  acked = true
@@ -2407,19 +2068,6 @@ const makeMessagesRecvSocket = config => {
2407
2068
  }
2408
2069
  }
2409
2070
  }
2410
-
2411
-
2412
-
2413
- const parseGroupCallRoster = groupInfoNode =>
2414
- (0, WABinary_1.getBinaryNodeChildren)(groupInfoNode, 'user').map(userNode => ({
2415
- jid: userNode.attrs.jid,
2416
- state: userNode.attrs.state,
2417
- userPn: userNode.attrs.user_pn,
2418
- devices: (0, WABinary_1.getBinaryNodeChildren)(userNode, 'device').map(d => ({
2419
- jid: d.attrs.jid,
2420
- platform: d.attrs.platform
2421
- }))
2422
- }))
2423
2071
  const handleCall = async node => {
2424
2072
  try {
2425
2073
  const { attrs } = node
@@ -2447,88 +2095,20 @@ const makeMessagesRecvSocket = config => {
2447
2095
  }
2448
2096
  }
2449
2097
  if (status === 'offer') {
2450
- const videoNode = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'video')
2451
- const audioNodes = (0, WABinary_1.getBinaryNodeChildren)(infoChild, 'audio')
2452
- const silenceNode = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'silence')
2453
- call.isVideo = !!videoNode
2098
+ call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(infoChild, 'video')
2454
2099
  call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid']
2455
2100
  call.groupJid = infoChild.attrs['group-jid']
2456
-
2457
- call.isLightweight = infoChild.attrs.lightweight === '1'
2458
- if (silenceNode?.attrs?.reason) {
2459
- call.silenceReason = silenceNode.attrs.reason
2460
- }
2461
-
2462
-
2463
- if (audioNodes.length) {
2464
- call.audioCodecs = audioNodes.map(n => ({ enc: n.attrs.enc, rate: n.attrs.rate ? +n.attrs.rate : undefined }))
2465
- call.audioCodec = audioNodes[0].attrs.enc
2466
- }
2467
- if (videoNode?.attrs?.dec) {
2468
- call.videoCodec = videoNode.attrs.dec
2469
- }
2470
-
2471
- const encNode = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'enc')
2472
- if (encNode?.content) {
2473
- try {
2474
- const encType = encNode.attrs.type || 'msg'
2475
- const ciphertext = Buffer.isBuffer(encNode.content) ? encNode.content : Buffer.from(encNode.content)
2476
- const decrypted = await signalRepository.decryptMessage({ jid: attrs.from, type: encType, ciphertext })
2477
- const unpadded = (0, Utils_1.unpadRandomMax16)(decrypted)
2478
- const callMsg = index_js_1.proto.Message.decode(unpadded)
2479
- if (callMsg?.call?.callKey?.length) {
2480
- call.callKey = Buffer.from(callMsg.call.callKey)
2481
- }
2482
- } catch (e) {
2483
- logger.debug({ e }, 'failed to decrypt call enc')
2484
- }
2485
- }
2486
2101
  await callOfferCache.set(call.id, call)
2487
- } else if (status === 'waiting_room_request') {
2488
- call.peerJid = infoChild.attrs.from || infoChild.attrs['peer-jid'] || infoChild.attrs['user-jid']
2489
2102
  }
2490
2103
  const existingCall = await callOfferCache.get(call.id)
2491
-
2104
+ // use existing call info to populate this event
2492
2105
  if (existingCall) {
2493
2106
  call.isVideo = existingCall.isVideo
2494
2107
  call.isGroup = existingCall.isGroup
2495
2108
  call.callerPn = call.callerPn || existingCall.callerPn
2496
2109
  }
2497
-
2498
- if (status === 'peer_state') {
2499
- const stateChild = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'peer_state')
2500
- call.state = stateChild?.attrs?.state || infoChild.attrs?.state
2501
- } else if (status === 'group_info') {
2502
- call.payload = infoChild.attrs
2503
- call.participants = parseGroupCallRoster(infoChild)
2504
- } else if (status === 'video_state') {
2505
- const vsChild = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'video_state')
2506
- const enabledRaw = vsChild?.attrs?.enabled ?? infoChild.attrs?.enabled
2507
- call.enabled = enabledRaw === 'true' || enabledRaw === '1'
2508
- } else if (status === 'enc_rekey') {
2509
- const rekeyChild =
2510
- (0, WABinary_1.getBinaryNodeChild)(infoChild, 'enc-rekey') ||
2511
- (0, WABinary_1.getBinaryNodeChild)(infoChild, 'enc_rekey')
2512
- if (rekeyChild?.content && Buffer.isBuffer(rekeyChild.content)) {
2513
- try {
2514
- call.rekeyPayload = (0, Utils_1.decodeE2eRekeyPayload)(rekeyChild.content)
2515
- } catch (e) {
2516
- logger.debug({ e }, 'failed to decode enc-rekey payload in handleCall')
2517
- }
2518
- }
2519
- }
2520
-
2521
- if (
2522
- status === 'reject' ||
2523
- status === 'accept' ||
2524
- status === 'timeout' ||
2525
- status === 'terminate' ||
2526
- status === 'reject_do_not_disturb' ||
2527
- status === 'mic_permission_denied' ||
2528
- status === 'camera_permission_denied' ||
2529
- status === 'remote_busy' ||
2530
- status === 'remote_offline'
2531
- ) {
2110
+ // delete data once call has ended
2111
+ if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
2532
2112
  await callOfferCache.del(call.id)
2533
2113
  }
2534
2114
  await normalizeCallEventJids(call, infoChild)
@@ -2539,32 +2119,15 @@ const makeMessagesRecvSocket = config => {
2539
2119
  await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack call'))
2540
2120
  }
2541
2121
  }
2542
-
2543
-
2544
-
2545
-
2546
- const CALL_STATE_TAGS = new Set([
2547
- 'offer',
2548
- 'offer_notice',
2549
- 'terminate',
2550
- 'accept',
2551
- 'reject',
2552
- 'preaccept',
2553
- 'accept_ack',
2554
- 'enc-rekey',
2555
- 'enc_rekey',
2556
- 'peer_state',
2557
- 'group_info',
2558
- 'video_state',
2559
- 'video_state_ack',
2560
- 'flow_control',
2561
- 'mute_v2',
2562
- 'waiting_room_request'
2563
- ])
2122
+ // Some accounts receive call signalling as TOP-LEVEL stanzas (<offer>, <terminate>,
2123
+ // <mute_v2>, <transport>, … each with a call-id) instead of wrapped in <call>.
2124
+ // This additively handles those: emit a 'call' event for state stanzas and ack ALL
2125
+ // of them (otherwise WhatsApp keeps redelivering). The <call> path above is untouched.
2126
+ const CALL_STATE_TAGS = new Set(['offer', 'offer_notice', 'terminate', 'accept', 'reject', 'preaccept'])
2564
2127
  const handleStandaloneCallStanza = async node => {
2565
2128
  try {
2566
2129
  if (!CALL_STATE_TAGS.has(node.tag)) {
2567
- return
2130
+ return // media/relay signalling (transport, video, duration, mute_v2, lobby, …): ack only
2568
2131
  }
2569
2132
  const { attrs } = node
2570
2133
  const status = (0, Utils_1.getCallStatusFromNode)(node)
@@ -2580,24 +2143,9 @@ const makeMessagesRecvSocket = config => {
2580
2143
  status
2581
2144
  }
2582
2145
  if (status === 'offer') {
2583
- const videoNode = (0, WABinary_1.getBinaryNodeChild)(node, 'video')
2584
- const audioNodes = (0, WABinary_1.getBinaryNodeChildren)(node, 'audio')
2585
- const silenceNode = (0, WABinary_1.getBinaryNodeChild)(node, 'silence')
2586
- call.isVideo = !!videoNode
2146
+ call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(node, 'video')
2587
2147
  call.isGroup = attrs.type === 'group' || !!attrs['group-jid']
2588
2148
  call.groupJid = attrs['group-jid']
2589
- call.isLightweight = attrs.lightweight === '1'
2590
- if (silenceNode?.attrs?.reason) {
2591
- call.silenceReason = silenceNode.attrs.reason
2592
- }
2593
-
2594
- if (audioNodes.length) {
2595
- call.audioCodecs = audioNodes.map(n => ({ enc: n.attrs.enc, rate: n.attrs.rate ? +n.attrs.rate : undefined }))
2596
- call.audioCodec = audioNodes[0].attrs.enc
2597
- }
2598
- if (videoNode?.attrs?.dec) {
2599
- call.videoCodec = videoNode.attrs.dec
2600
- }
2601
2149
  if (callId) {
2602
2150
  await callOfferCache.set(callId, call)
2603
2151
  }
@@ -2608,45 +2156,7 @@ const makeMessagesRecvSocket = config => {
2608
2156
  call.isGroup = existingCall.isGroup
2609
2157
  call.callerPn = call.callerPn || existingCall.callerPn
2610
2158
  }
2611
-
2612
- if (status === 'peer_state') {
2613
- const stateChild = (0, WABinary_1.getBinaryNodeChild)(node, 'peer_state')
2614
- call.state = stateChild?.attrs?.state || attrs?.state
2615
- } else if (status === 'group_info') {
2616
- call.payload = attrs
2617
- call.participants = parseGroupCallRoster(node)
2618
- } else if (status === 'video_state') {
2619
- const vsChild = (0, WABinary_1.getBinaryNodeChild)(node, 'video_state')
2620
- const enabledRaw = vsChild?.attrs?.enabled ?? attrs?.enabled
2621
- call.enabled = enabledRaw === 'true' || enabledRaw === '1'
2622
- } else if (status === 'enc_rekey') {
2623
- const rekeyChild =
2624
- (0, WABinary_1.getBinaryNodeChild)(node, 'enc-rekey') || (0, WABinary_1.getBinaryNodeChild)(node, 'enc_rekey')
2625
- if (rekeyChild?.content && Buffer.isBuffer(rekeyChild.content)) {
2626
- try {
2627
- call.rekeyPayload = (0, Utils_1.decodeE2eRekeyPayload)(rekeyChild.content)
2628
- } catch (e) {
2629
- logger.debug({ e }, 'failed to decode enc-rekey payload in standalone call stanza')
2630
- }
2631
- }
2632
- } else if (status === 'mute') {
2633
-
2634
-
2635
-
2636
- call.muted = node.attrs['mute-state'] === '1'
2637
- }
2638
- if (
2639
- callId &&
2640
- (status === 'reject' ||
2641
- status === 'accept' ||
2642
- status === 'timeout' ||
2643
- status === 'terminate' ||
2644
- status === 'reject_do_not_disturb' ||
2645
- status === 'mic_permission_denied' ||
2646
- status === 'camera_permission_denied' ||
2647
- status === 'remote_busy' ||
2648
- status === 'remote_offline')
2649
- ) {
2159
+ if (callId && (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate')) {
2650
2160
  await callOfferCache.del(callId)
2651
2161
  }
2652
2162
  await normalizeCallEventJids(call, node)
@@ -2659,26 +2169,26 @@ const makeMessagesRecvSocket = config => {
2659
2169
  }
2660
2170
  const handleBadAck = async ({ attrs }) => {
2661
2171
  const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id }
2662
-
2663
-
2664
-
2665
-
2666
-
2667
-
2668
-
2669
-
2670
-
2671
-
2672
-
2673
-
2674
-
2675
-
2676
-
2172
+ // WARNING: REFRAIN FROM ENABLING THIS FOR NOW. IT WILL CAUSE A LOOP
2173
+ // // current hypothesis is that if pash is sent in the ack
2174
+ // // it means -- the message hasn't reached all devices yet
2175
+ // // we'll retry sending the message here
2176
+ // if(attrs.phash) {
2177
+ // logger.info({ attrs }, 'received phash in ack, resending message...')
2178
+ // const msg = await getMessage(key)
2179
+ // if(msg) {
2180
+ // await relayMessage(key.remoteJid!, msg, { messageId: key.id!, useUserDevicesCache: false })
2181
+ // } else {
2182
+ // logger.warn({ attrs }, 'could not send message again, as it was not found')
2183
+ // }
2184
+ // }
2185
+ // error in acknowledgement,
2186
+ // device could not display the message
2677
2187
  if (attrs.error) {
2678
2188
  if (attrs.error === Utils_1.SERVER_ERROR_CODES.MissingTcToken) {
2679
-
2680
-
2681
-
2189
+ // 463 = account restricted + no tctoken for this contact.
2190
+ // WA Web prevents this client-side (disables compose bar).
2191
+ // No retry — retrying worsens the restriction by counting as another "reach out" to an unknown contact.
2682
2192
  logger.warn(
2683
2193
  { msgId: attrs.id, from: attrs.from },
2684
2194
  'error 463: account restricted or missing tctoken for contact'
@@ -2700,54 +2210,23 @@ const makeMessagesRecvSocket = config => {
2700
2210
  }
2701
2211
  }
2702
2212
  ])
2703
-
2704
-
2705
-
2706
-
2707
-
2708
-
2709
-
2710
-
2711
-
2712
-
2713
-
2714
-
2715
-
2213
+ // resend the message with device_fanout=false, use at your own risk
2214
+ // if (attrs.error === '475') {
2215
+ // const msg = await getMessage(key)
2216
+ // if (msg) {
2217
+ // await relayMessage(key.remoteJid!, msg, {
2218
+ // messageId: key.id!,
2219
+ // useUserDevicesCache: false,
2220
+ // additionalAttributes: {
2221
+ // device_fanout: 'false'
2222
+ // }
2223
+ // })
2224
+ // }
2225
+ // }
2716
2226
  }
2717
2227
  }
2718
- const handleChatstate = async node => {
2719
- const { from, state } = node.attrs
2720
- if (!from) return
2721
- const isTyping = state === 'typing'
2722
- ev.emit('presence.update', [
2723
- {
2724
- id: from,
2725
- presences: {
2726
- [from]: isTyping ? 'composing' : 'paused'
2727
- }
2728
- }
2729
- ])
2730
- }
2731
- const handlePresence = async node => {
2732
- const { from, type } = node.attrs
2733
- if (!from || !type) return
2734
- const presenceMap = {
2735
- available: 'available',
2736
- unavailable: 'unavailable'
2737
- }
2738
- const presence = presenceMap[type] || type
2739
- ev.emit('presence.update', [
2740
- {
2741
- id: from,
2742
- presences: {
2743
- [from]: presence
2744
- },
2745
- lastSeen: node.attrs.t ? Number(node.attrs.t) * 1000 : undefined
2746
- }
2747
- ])
2748
- }
2749
-
2750
-
2228
+ /// processes a node with the given function
2229
+ /// and adds the task to the existing buffer if we're buffering events
2751
2230
  const processNodeWithBuffer = async (node, identifier, exec) => {
2752
2231
  ev.buffer()
2753
2232
  await execTask()
@@ -2786,7 +2265,7 @@ const makeMessagesRecvSocket = config => {
2786
2265
  const setNodeLoggerListener = () => {
2787
2266
  return latestNodeInMemory
2788
2267
  }
2789
-
2268
+ // recv a message
2790
2269
  ws.on('CB:message', async node => {
2791
2270
  nodelogger(node)
2792
2271
  await processNode('message', node, 'processing message', handleMessage)
@@ -2795,35 +2274,10 @@ const makeMessagesRecvSocket = config => {
2795
2274
  nodelogger(node)
2796
2275
  await processNode('call', node, 'handling call', handleCall)
2797
2276
  })
2798
-
2277
+ // additive: top-level call-signalling stanzas (some accounts send these instead of <call>)
2799
2278
  for (const callTag of [
2800
- 'offer',
2801
- 'offer_notice',
2802
- 'terminate',
2803
- 'accept',
2804
- 'reject',
2805
- 'preaccept',
2806
- 'accept_ack',
2807
- 'enc-rekey',
2808
- 'enc_rekey',
2809
- 'peer_state',
2810
- 'group_info',
2811
- 'video_state',
2812
- 'video_state_ack',
2813
- 'flow_control',
2814
- 'transport',
2815
- 'video',
2816
- 'duration',
2817
- 'mute_v2',
2818
- 'lobby',
2819
- 'heartbeat',
2820
- 'relaylatency',
2821
- 'link_query',
2822
- 'waiting_room_request',
2823
-
2824
-
2825
-
2826
- 'group_update'
2279
+ 'offer', 'offer_notice', 'terminate', 'accept', 'reject', 'preaccept',
2280
+ 'transport', 'video', 'duration', 'mute_v2', 'lobby', 'heartbeat', 'relaylatency', 'link_query'
2827
2281
  ]) {
2828
2282
  ws.on('CB:' + callTag, node => {
2829
2283
  nodelogger(node)
@@ -2838,18 +2292,6 @@ const makeMessagesRecvSocket = config => {
2838
2292
  nodelogger(node)
2839
2293
  await processNode('notification', node, 'handling notification', handleNotification)
2840
2294
  })
2841
- ws.on('CB:status', async node => {
2842
- nodelogger(node)
2843
- await handleNewsletterStatus(node).catch(error => onUnexpectedError(error, 'handling newsletter status'))
2844
- })
2845
- ws.on('CB:chatstate', async node => {
2846
- nodelogger(node)
2847
- await handleChatstate(node).catch(error => onUnexpectedError(error, 'handling chatstate'))
2848
- })
2849
- ws.on('CB:presence', async node => {
2850
- nodelogger(node)
2851
- await handlePresence(node).catch(error => onUnexpectedError(error, 'handling presence'))
2852
- })
2853
2295
  ws.on('CB:ack,class:message', node => {
2854
2296
  nodelogger(node)
2855
2297
  handleBadAck(node).catch(error => onUnexpectedError(error, 'handling bad ack'))
@@ -2879,7 +2321,7 @@ const makeMessagesRecvSocket = config => {
2879
2321
  return
2880
2322
  }
2881
2323
  nodelogger(call)
2882
-
2324
+ // missed call + group call notification message generation
2883
2325
  if (call.status === 'timeout' || (call.status === 'offer' && call.isGroup)) {
2884
2326
  const msg = {
2885
2327
  key: {
@@ -2915,7 +2357,7 @@ const makeMessagesRecvSocket = config => {
2915
2357
  sendActiveReceipts = isOnline
2916
2358
  logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`)
2917
2359
  }
2918
-
2360
+ // Daily cleanup of expired tctokens (mirrors WA Web's CLEAN_TC_TOKENS task)
2919
2361
  if (isOnline) {
2920
2362
  const now = Date.now()
2921
2363
  const DAY_MS = 24 * 60 * 60 * 1000