@badzz88/baileys 8.5.2 → 8.5.4

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 (110) hide show
  1. package/README.md +1 -1
  2. package/WAProto/WAProto.proto +1256 -75
  3. package/WAProto/index.d.ts +36042 -19649
  4. package/WAProto/index.js +181990 -135645
  5. package/engine-requirements.js +7 -15
  6. package/lib/Defaults/index.js +22 -17
  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 +39 -59
  15. package/lib/Signal/lid-mapping.js +28 -10
  16. package/lib/Socket/aigroups.js +5 -5
  17. package/lib/Socket/business.js +3 -11
  18. package/lib/Socket/chats.js +668 -319
  19. package/lib/Socket/communities.js +85 -89
  20. package/lib/Socket/graphql.js +435 -228
  21. package/lib/Socket/groups.js +132 -211
  22. package/lib/Socket/index.js +9 -7
  23. package/lib/Socket/interactive-handler.js +117 -60
  24. package/lib/Socket/interop.js +56 -175
  25. package/lib/Socket/managed-account.js +79 -48
  26. package/lib/Socket/messages-recv.js +898 -340
  27. package/lib/Socket/messages-send.js +462 -160
  28. package/lib/Socket/mex.js +40 -53
  29. package/lib/Socket/newsletter.js +291 -291
  30. package/lib/Socket/privacy.js +77 -265
  31. package/lib/Socket/registration.js +164 -157
  32. package/lib/Socket/socket.js +181 -199
  33. package/lib/Socket/username.js +144 -227
  34. package/lib/Store/make-cache-manager-store.js +2 -2
  35. package/lib/Store/make-in-memory-store.js +25 -141
  36. package/lib/Types/Auth.js +3 -3
  37. package/lib/Types/Label.js +1 -1
  38. package/lib/Types/LabelAssociation.js +1 -1
  39. package/lib/Types/Newsletter.js +96 -65
  40. package/lib/Types/State.js +4 -4
  41. package/lib/Types/index.js +1 -1
  42. package/lib/Utils/auth-utils.js +52 -57
  43. package/lib/Utils/browser-utils.js +1 -1
  44. package/lib/Utils/business.js +2 -6
  45. package/lib/Utils/chat-utils.js +102 -38
  46. package/lib/Utils/crypto.js +54 -84
  47. package/lib/Utils/decode-wa-message.js +316 -83
  48. package/lib/Utils/event-buffer.js +37 -61
  49. package/lib/Utils/generics.js +157 -103
  50. package/lib/Utils/group-history.js +3 -12
  51. package/lib/Utils/history.js +15 -15
  52. package/lib/Utils/index.js +1 -5
  53. package/lib/Utils/link-preview.js +3 -8
  54. package/lib/Utils/lt-hash.js +2 -35
  55. package/lib/Utils/make-mutex.js +1 -1
  56. package/lib/Utils/message-inspect.js +393 -0
  57. package/lib/Utils/message-retry-manager.js +27 -92
  58. package/lib/Utils/messages-media.js +159 -130
  59. package/lib/Utils/messages.js +144 -566
  60. package/lib/Utils/meta-ai-msmsg.js +62 -202
  61. package/lib/Utils/noise-handler.js +19 -21
  62. package/lib/Utils/offline-node-processor.js +3 -8
  63. package/lib/Utils/pre-key-manager.js +13 -23
  64. package/lib/Utils/process-message.js +197 -106
  65. package/lib/Utils/reporting-utils.js +2 -2
  66. package/lib/Utils/signal.js +28 -63
  67. package/lib/Utils/stanza-ack.js +39 -12
  68. package/lib/Utils/sync-action-utils.js +4 -7
  69. package/lib/Utils/tc-token-utils.js +20 -25
  70. package/lib/Utils/use-multi-file-auth-state.js +30 -14
  71. package/lib/Utils/validate-connection.js +59 -19
  72. package/lib/WABinary/decode.js +50 -16
  73. package/lib/WABinary/encode.js +54 -224
  74. package/lib/WABinary/generic-utils.js +7 -97
  75. package/lib/WABinary/jid-utils.js +27 -14
  76. package/lib/WAM/encode.js +7 -7
  77. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +58 -1
  78. package/lib/WAUSync/Protocols/USyncContactProtocol.js +7 -1
  79. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +13 -5
  80. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +1 -4
  81. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +20 -7
  82. package/lib/WAUSync/Protocols/USyncNewsletterProtocol.js +865 -0
  83. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +1 -1
  84. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +2 -2
  85. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +7 -2
  86. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +1 -1
  87. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +2 -1
  88. package/lib/WAUSync/Protocols/index.js +1 -0
  89. package/lib/WAUSync/USyncBackoff.js +26 -0
  90. package/lib/WAUSync/USyncQuery.js +77 -6
  91. package/lib/WAUSync/USyncUser.js +16 -8
  92. package/lib/WAUSync/index.js +1 -0
  93. package/lib/antiban.js +608 -854
  94. package/lib/index.js +3 -3
  95. package/package.json +16 -19
  96. package/lib/Socket/luxu.js +0 -387
  97. package/lib/Socket/text-router.js +0 -83
  98. package/lib/Store/keyed-db.js +0 -108
  99. package/lib/Types/Mex.js +0 -112
  100. package/lib/Utils/command-loader.d.ts +0 -31
  101. package/lib/Utils/command-loader.js +0 -100
  102. package/lib/Utils/companion-reg-client-utils.js +0 -42
  103. package/lib/Utils/curve25519-js.js +0 -275
  104. package/lib/Utils/native-bridge.js +0 -82
  105. package/lib/Utils/session-pool.d.ts +0 -16
  106. package/lib/Utils/session-pool.js +0 -94
  107. package/lib/Utils/sticker.d.ts +0 -17
  108. package/lib/Utils/sticker.js +0 -145
  109. package/lib/Utils/view-once-cache.d.ts +0 -9
  110. package/lib/Utils/view-once-cache.js +0 -79
@@ -37,8 +37,6 @@ const makeMessagesRecvSocket = config => {
37
37
  signalRepository,
38
38
  query,
39
39
  upsertMessage,
40
- getUSyncDevices,
41
- createParticipantNodes,
42
40
  resyncAppState,
43
41
  onUnexpectedError,
44
42
  assertSessions,
@@ -48,33 +46,36 @@ const makeMessagesRecvSocket = config => {
48
46
  uploadPreKeys,
49
47
  sendPeerDataOperationMessage,
50
48
  messageRetryManager,
51
- issuePrivacyTokens
49
+ issuePrivacyTokens,
50
+ getUSyncDevices,
51
+ createParticipantNodes,
52
+ newsletterServerIdCache
52
53
  } = sock
53
54
  const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
54
- // Track when the socket fully opens so pending pre-connect messages are treated as history
55
+
55
56
  const socketCreatedAt = Math.floor(Date.now() / 1000)
56
57
  let isConnected = false
57
- /** this mutex ensures that each retryRequest will wait for the previous one to finish */
58
+
58
59
  const retryMutex = (0, make_mutex_1.makeMutex)()
59
60
  const msgRetryCache =
60
61
  config.msgRetryCounterCache ||
61
62
  new node_cache_1.default({
62
- stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
63
+ stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY,
63
64
  useClones: false
64
65
  })
65
66
  const callOfferCache =
66
67
  config.callOfferCache ||
67
68
  new node_cache_1.default({
68
- stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
69
+ stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.CALL_OFFER,
69
70
  useClones: false
70
71
  })
71
72
  const placeholderResendCache =
72
73
  config.placeholderResendCache ||
73
74
  new node_cache_1.default({
74
- stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
75
+ stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY,
75
76
  useClones: false
76
77
  })
77
- // Debounce identity-change session refreshes per JID to avoid bursts
78
+
78
79
  const identityAssertDebounce = new node_cache_1.default({ stdTTL: 5, useClones: false })
79
80
  let sendActiveReceipts = false
80
81
  const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
@@ -101,8 +102,8 @@ const makeMessagesRecvSocket = config => {
101
102
  logger.debug({ messageKey }, 'already requested resend')
102
103
  return
103
104
  } else {
104
- // Store original message data so PDO response handler can preserve
105
- // metadata (LID details, timestamps, etc.) that the phone may omit
105
+
106
+
106
107
  await placeholderResendCache.set(messageKey?.id, msgData || true)
107
108
  }
108
109
  await (0, Utils_1.delay)(2000)
@@ -126,11 +127,7 @@ const makeMessagesRecvSocket = config => {
126
127
  }, 8000)
127
128
  return sendPeerDataOperationMessage(pdoMessage)
128
129
  }
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
- */
130
+
134
131
  const requestWaffleNonce = async () => {
135
132
  if (!authState.creds.me?.id) {
136
133
  throw new boom_1.Boom('Not authenticated')
@@ -139,27 +136,18 @@ const makeMessagesRecvSocket = config => {
139
136
  peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.WAFFLE_LINKING_NONCE_FETCH
140
137
  })
141
138
  }
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) => {
139
+
140
+ const requestCompanionCanonicalNonce = async registrationTraceId => {
150
141
  if (!authState.creds.me?.id) {
151
142
  throw new boom_1.Boom('Not authenticated')
152
143
  }
153
144
  return sendPeerDataOperationMessage({
154
145
  companionCanonicalUserNonceFetchRequest: registrationTraceId ? { registrationTraceId } : {},
155
- peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_CANONICAL_USER_NONCE_FETCH
146
+ peerDataOperationRequestType:
147
+ index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_CANONICAL_USER_NONCE_FETCH
156
148
  })
157
149
  }
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
- */
150
+
163
151
  const requestCompanionMetaNonce = async () => {
164
152
  if (!authState.creds.me?.id) {
165
153
  throw new boom_1.Boom('Not authenticated')
@@ -168,57 +156,138 @@ const makeMessagesRecvSocket = config => {
168
156
  peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_META_NONCE_FETCH
169
157
  })
170
158
  }
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')
176
- return
177
- }
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')
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')
189
165
  return
190
166
  }
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) {
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) {
196
188
  ev.emit('newsletter-settings.update', {
197
- id: update.jid,
198
- update: update.settings
189
+ id: upd.id,
190
+ update: upd.thread_metadata?.settings ?? {}
199
191
  })
200
192
  }
193
+ break
201
194
  }
202
- break
203
- case 'NotificationNewsletterAdminPromote':
204
- for (const update of updates) {
205
- if (update.jid && update.user) {
195
+ case 'NotificationNewsletterJoin': {
196
+
197
+ const upd = d.xwa2_notify_newsletter_on_join
198
+ if (upd?.id) {
206
199
  ev.emit('newsletter-participants.update', {
207
- id: update.jid,
200
+ id: upd.id,
208
201
  author: node.attrs.from,
209
- user: update.user,
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) {
225
+ ev.emit('newsletter-settings.update', {
226
+ id: upd.id,
227
+ update: { userSetting: upd.setting }
228
+ })
229
+ }
230
+ break
231
+ }
232
+ case 'NotificationNewsletterAdminPromote': {
233
+
234
+ const upd = d.xwa2_notify_newsletter_on_admin_promote
235
+ if (upd?.id) {
236
+ ev.emit('newsletter-participants.update', {
237
+ id: upd.id,
238
+ author: node.attrs.from,
239
+ user: upd.user,
210
240
  new_role: 'ADMIN',
211
241
  action: 'promote'
212
242
  })
213
243
  }
244
+ break
214
245
  }
215
- break
216
- default:
217
- logger.info({ operation, data }, 'Unhandled mex newsletter notification')
218
- break
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
+ }
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
+ }
219
288
  }
220
289
  }
221
- // Handles newsletter notifications
290
+
222
291
  const handleNewsletterNotification = async node => {
223
292
  const from = node.attrs.from
224
293
  const child = (0, WABinary_1.getAllBinaryNodeChildren)(node)[0]
@@ -268,7 +337,9 @@ const makeMessagesRecvSocket = config => {
268
337
  })
269
338
  }
270
339
  break
271
- case 'message':
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
272
343
  const plaintextNode = (0, WABinary_1.getBinaryNodeChild)(child, 'plaintext')
273
344
  if (plaintextNode?.content) {
274
345
  try {
@@ -281,11 +352,16 @@ const makeMessagesRecvSocket = config => {
281
352
  key: {
282
353
  remoteJid: from,
283
354
  id: child.attrs.message_id || child.attrs.server_id,
284
- fromMe: false // TODO: is this really true though
355
+
356
+
357
+ fromMe: false
285
358
  },
286
359
  message: messageProto,
287
360
  messageTimestamp: +child.attrs.t
288
361
  }).toJSON()
362
+
363
+ if (viewCount !== undefined) fullMessage.views = viewCount
364
+ if (impressionCount !== undefined) fullMessage.impressions = impressionCount
289
365
  await upsertMessage(fullMessage, 'append')
290
366
  logger.info('Processed plaintext newsletter message')
291
367
  } catch (error) {
@@ -293,23 +369,44 @@ const makeMessagesRecvSocket = config => {
293
369
  }
294
370
  }
295
371
  break
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
- })
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
+ }
305
396
  break
306
- case 'pin':
397
+ }
398
+ case 'pin': {
399
+ const pinnedServerId = child.attrs.message_id || child.attrs.server_id
400
+ const isPinned = child.attrs.action !== 'unpin'
307
401
  ev.emit('newsletter.pin', {
308
402
  id: from,
309
- server_id: child.attrs.message_id || child.attrs.server_id,
310
- pinned: child.attrs.action !== 'unpin'
403
+ server_id: pinnedServerId,
404
+ pinned: isPinned
311
405
  })
406
+
407
+ ev.emit('newsletters.update', [{ id: from, pinnedMessage: isPinned ? pinnedServerId : null }])
312
408
  break
409
+ }
313
410
  case 'category':
314
411
  ev.emit('newsletter-settings.update', {
315
412
  id: from,
@@ -329,8 +426,92 @@ const makeMessagesRecvSocket = config => {
329
426
  break
330
427
  }
331
428
  }
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
+ }
332
511
  const sendMessageAck = async (node, errorCode) => {
333
- const stanza = (0, stanza_ack_1.buildAckStanza)(node, errorCode, authState.creds.me.id)
512
+
513
+
514
+ const stanza = (0, stanza_ack_1.buildAckStanza)(node, errorCode, authState.creds.me?.id)
334
515
  logger.debug({ recv: { tag: node.tag, attrs: node.attrs }, sent: stanza.attrs }, 'sent ack')
335
516
  await sendNode(stanza)
336
517
  }
@@ -497,15 +678,7 @@ const makeMessagesRecvSocket = config => {
497
678
  await query(stanza)
498
679
  }
499
680
 
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
- */
681
+
509
682
  const rekeyCall = async (callId, callFrom, encryptedKeyBytes, count = 0) => {
510
683
  const stanza = {
511
684
  tag: 'call',
@@ -534,14 +707,7 @@ const makeMessagesRecvSocket = config => {
534
707
  await query(stanza)
535
708
  }
536
709
 
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
- */
710
+
545
711
  const joinCallLink = async (callId, callCreator, linkToken) => {
546
712
  const stanza = {
547
713
  tag: 'call',
@@ -564,13 +730,7 @@ const makeMessagesRecvSocket = config => {
564
730
  await query(stanza)
565
731
  }
566
732
 
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
- */
733
+
574
734
  const queryCallLink = async (callLinkCode, to) => {
575
735
  const stanza = {
576
736
  tag: 'call',
@@ -593,19 +753,19 @@ const makeMessagesRecvSocket = config => {
593
753
  const { key: msgKey } = fullMessage
594
754
  const msgId = msgKey.id
595
755
  if (messageRetryManager) {
596
- // Check if we've exceeded max retries using the new system
756
+
597
757
  if (messageRetryManager.hasExceededMaxRetries(msgId)) {
598
758
  logger.debug({ msgId }, 'reached retry limit with new retry manager, clearing')
599
759
  messageRetryManager.markRetryFailed(msgId)
600
760
  return
601
761
  }
602
- // Increment retry count using new system
762
+
603
763
  const retryCount = messageRetryManager.incrementRetryCount(msgId)
604
- // Use the new retry count for the rest of the logic
764
+
605
765
  const key = `${msgId}:${msgKey?.participant}`
606
766
  await msgRetryCache.set(key, retryCount)
607
767
  } else {
608
- // Fallback to old system
768
+
609
769
  const key = `${msgId}:${msgKey?.participant}`
610
770
  let retryCount = (await msgRetryCache.get(key)) || 0
611
771
  if (retryCount >= maxMsgRetryCount) {
@@ -620,12 +780,12 @@ const makeMessagesRecvSocket = config => {
620
780
  const retryCount = (await msgRetryCache.get(key)) || 1
621
781
  const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
622
782
  const fromJid = node.attrs.from
623
- // Check if we should recreate the session
783
+
624
784
  let shouldRecreateSession = false
625
785
  let recreateReason = ''
626
786
  if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
627
787
  try {
628
- // Check if we have a session with this JID
788
+
629
789
  const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
630
790
  const hasSession = await signalRepository.validateSession(fromJid)
631
791
  const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists)
@@ -633,7 +793,7 @@ const makeMessagesRecvSocket = config => {
633
793
  recreateReason = result.reason
634
794
  if (shouldRecreateSession) {
635
795
  logger.debug({ fromJid, retryCount, reason: recreateReason }, 'recreating session for retry')
636
- // Delete existing session to force recreation
796
+
637
797
  await authState.keys.set({ session: { [sessionId]: null } })
638
798
  forceIncludeKeys = true
639
799
  }
@@ -642,9 +802,9 @@ const makeMessagesRecvSocket = config => {
642
802
  }
643
803
  }
644
804
  if (retryCount <= 2) {
645
- // Use new retry manager for phone requests if available
805
+
646
806
  if (messageRetryManager) {
647
- // Schedule phone request with delay (like whatsmeow)
807
+
648
808
  messageRetryManager.schedulePhoneRequest(msgId, async () => {
649
809
  try {
650
810
  const requestId = await requestPlaceholderResend(msgKey)
@@ -656,7 +816,7 @@ const makeMessagesRecvSocket = config => {
656
816
  }
657
817
  })
658
818
  } else {
659
- // Fallback to immediate request
819
+
660
820
  const msgId = await requestPlaceholderResend(msgKey)
661
821
  logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`)
662
822
  }
@@ -678,8 +838,9 @@ const makeMessagesRecvSocket = config => {
678
838
  id: node.attrs.id,
679
839
  t: node.attrs.t,
680
840
  v: '1',
681
- // ADD ERROR FIELD
682
- error: '0'
841
+
842
+
843
+ error: '1'
683
844
  }
684
845
  },
685
846
  {
@@ -717,17 +878,14 @@ const makeMessagesRecvSocket = config => {
717
878
  logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt')
718
879
  }, authState?.creds?.me?.id || 'sendRetryRequest')
719
880
  }
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
- */
881
+
724
882
  const reissueTcTokenAfterIdentityChange = from => {
725
883
  void (async () => {
726
884
  const normalizedJid = (0, WABinary_1.jidNormalizedUser)(from)
727
885
  const tcJid = await (0, tc_token_utils_1.resolveTcTokenJid)(normalizedJid, getLIDForPN)
728
886
  const tcTokenData = await authState.keys.get('tctoken', [tcJid])
729
887
  const senderTs = tcTokenData?.[tcJid]?.senderTimestamp
730
- if (senderTs === null || senderTs === undefined || (0, tc_token_utils_1.isTcTokenExpired)(senderTs)) {
888
+ if (senderTs == null || (0, tc_token_utils_1.isTcTokenExpired)(senderTs)) {
731
889
  return
732
890
  }
733
891
  logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken')
@@ -775,8 +933,21 @@ const makeMessagesRecvSocket = config => {
775
933
  }
776
934
  }
777
935
  }
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
+ }
778
949
  const handleGroupNotification = (fullNode, child, msg) => {
779
- // TODO: Support PN/LID (Here is only LID now)
950
+
780
951
  const actingParticipantLid = fullNode.attrs.participant
781
952
  const actingParticipantPn = fullNode.attrs.participant_pn
782
953
  const actingParticipantUsername = fullNode.attrs.participant_username
@@ -790,6 +961,7 @@ const makeMessagesRecvSocket = config => {
790
961
  msg.messageStubType = Types_1.WAMessageStubType.GROUP_CREATE
791
962
  msg.messageStubParameters = [metadata.subject]
792
963
  msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn }
964
+ storeParticipantLidPairs(metadata.participants)
793
965
  ev.emit('chats.upsert', [
794
966
  {
795
967
  id: metadata.id,
@@ -828,7 +1000,6 @@ const makeMessagesRecvSocket = config => {
828
1000
  const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`
829
1001
  msg.messageStubType = Types_1.WAMessageStubType[stubType]
830
1002
  const participants = (0, WABinary_1.getBinaryNodeChildren)(child, 'participant').map(({ attrs }) => {
831
- // TODO: Store LID MAPPINGS
832
1003
  return {
833
1004
  id: attrs.jid,
834
1005
  phoneNumber:
@@ -837,19 +1008,21 @@ const makeMessagesRecvSocket = config => {
837
1008
  : undefined,
838
1009
  lid: (0, WABinary_1.isPnUser)(attrs.jid) && (0, WABinary_1.isLidUser)(attrs.lid) ? attrs.lid : undefined,
839
1010
  username: attrs.participant_username || attrs.username || undefined,
840
- admin: attrs.type || null
1011
+ admin: attrs.type || null,
1012
+ uuid: attrs.uuid || attrs.participant_uuid || undefined
841
1013
  }
842
1014
  })
843
1015
  if (
844
1016
  participants.length === 1 &&
845
- // if recv. "remove" message and sender removed themselves
846
- // mark as left
1017
+
1018
+
847
1019
  ((0, WABinary_1.areJidsSameUser)(participants[0].id, actingParticipantLid) ||
848
1020
  (0, WABinary_1.areJidsSameUser)(participants[0].id, actingParticipantPn)) &&
849
1021
  child.tag === 'remove'
850
1022
  ) {
851
1023
  msg.messageStubType = Types_1.WAMessageStubType.GROUP_PARTICIPANT_LEAVE
852
1024
  }
1025
+ storeParticipantLidPairs(participants)
853
1026
  msg.messageStubParameters = participants.map(a => JSON.stringify(a))
854
1027
  break
855
1028
  case 'subject':
@@ -899,13 +1072,51 @@ const makeMessagesRecvSocket = config => {
899
1072
  break
900
1073
  case 'revoked_membership_requests':
901
1074
  const isDenied = (0, WABinary_1.areJidsSameUser)(affectedParticipantLid, actingParticipantLid)
902
- // TODO: LIDMAPPING SUPPORT
1075
+ if ((0, WABinary_1.isLidUser)(affectedParticipantLid) && (0, WABinary_1.isPnUser)(affectedParticipantPn)) {
1076
+ storeParticipantLidPairs([{ id: affectedParticipantLid, phoneNumber: affectedParticipantPn }])
1077
+ }
903
1078
  msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
904
1079
  msg.messageStubParameters = [
905
1080
  JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
906
1081
  isDenied ? 'revoked' : 'rejected'
907
1082
  ]
908
1083
  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
+ }
909
1120
  }
910
1121
  }
911
1122
  const normalizeNotificationParticipant = async (jid, groupData) => {
@@ -1039,21 +1250,12 @@ const makeMessagesRecvSocket = config => {
1039
1250
  }
1040
1251
  }
1041
1252
 
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
- */
1253
+
1052
1254
  const handleInteropNotification = (node, child) => {
1053
1255
  const childTag = child?.tag
1054
1256
  const attrs = child?.attrs || {}
1055
1257
 
1056
- // Feature flag: server toggled stella_interop_enabled or stella_ios_enabled
1258
+
1057
1259
  if (childTag === 'feature') {
1058
1260
  const feature = attrs.name
1059
1261
  const enabled = attrs.value === 'true' || attrs.value === '1'
@@ -1062,7 +1264,7 @@ const makeMessagesRecvSocket = config => {
1062
1264
  return
1063
1265
  }
1064
1266
 
1065
- // Instagram profile data pushed for an interop contact
1267
+
1066
1268
  if (childTag === 'ig_profile') {
1067
1269
  const contactUpdate = {
1068
1270
  id: (0, WABinary_1.jidNormalizedUser)(node.attrs.from),
@@ -1075,26 +1277,28 @@ const makeMessagesRecvSocket = config => {
1075
1277
  return
1076
1278
  }
1077
1279
 
1078
- // peer_device_presence — interop contact came online or went offline
1280
+
1079
1281
  if (childTag === 'peer_device_presence') {
1080
1282
  const jid = attrs.jid || (0, WABinary_1.jidNormalizedUser)(node.attrs.from)
1081
1283
  const presence = attrs.type === 'unavailable' ? 'unavailable' : 'available'
1082
- logger.debug({ jid, presence }, '[interop] peer_device_presence')
1083
- ev.emit('presence.update', { id: jid, presences: { [jid]: { lastKnownPresence: presence } } })
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 })
1084
1287
  return
1085
1288
  }
1086
1289
 
1087
- // fbid:thread / fbid:devices — Meta thread or device list association
1290
+
1088
1291
  if (childTag === 'fbid_thread' || childTag === 'fbid_devices') {
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 })
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 })
1091
1295
  return
1092
1296
  }
1093
1297
 
1094
- // Interop group membership changes (add / remove / promote / demote)
1298
+
1095
1299
  if (childTag === 'participants') {
1096
1300
  const groupJid = node.attrs.from
1097
- const action = attrs.type // 'add' | 'remove' | 'promote' | 'demote'
1301
+ const action = attrs.type
1098
1302
  const participants = (0, WABinary_1.getBinaryNodeChildren)(child, 'participant').map(p => p.attrs.jid)
1099
1303
  logger.info({ groupJid, action, participants }, '[interop] group participants update')
1100
1304
  ev.emit('group-participants.update', { id: groupJid, participants, action })
@@ -1114,10 +1318,24 @@ const makeMessagesRecvSocket = config => {
1114
1318
  await handleNewsletterNotification(node)
1115
1319
  break
1116
1320
  case 'mex':
1117
- await handleMexNewsletterNotification(node)
1321
+ await handleMexNotification(node)
1118
1322
  break
1119
1323
  case 'w:gp2':
1120
- // TODO: HANDLE PARTICIPANT_PN
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
+
1121
1339
  const groupData = await getNotificationGroupData(node)
1122
1340
  handleGroupNotification(node, child, result)
1123
1341
 
@@ -1130,26 +1348,39 @@ const makeMessagesRecvSocket = config => {
1130
1348
  case 'encrypt':
1131
1349
  await handleEncryptNotification(node)
1132
1350
  break
1133
- case 'devices':
1134
- const devices = (0, WABinary_1.getBinaryNodeChildren)(child, 'device')
1135
- const deviceOwnerJid = child.attrs.jid || child.attrs.lid
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
1136
1365
  const deviceData = devices.map(d => ({
1137
1366
  id: d.attrs.jid,
1138
1367
  lid: d.attrs.lid,
1139
- keyIndex: d.attrs.key_index ? +d.attrs.key_index : undefined,
1368
+
1369
+ keyIndex: d.attrs['key-index'] ? +d.attrs['key-index'] : undefined,
1140
1370
  platform: d.attrs.platform || undefined,
1141
1371
  isCompanion: d.attrs.companion === 'true' || undefined
1142
1372
  }))
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 })
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 })
1151
1381
  }
1152
1382
  break
1383
+ }
1153
1384
  case 'server_sync':
1154
1385
  const update = (0, WABinary_1.getBinaryNodeChild)(node, 'collection')
1155
1386
  if (update) {
@@ -1157,13 +1388,18 @@ const makeMessagesRecvSocket = config => {
1157
1388
  await resyncAppState([name], false)
1158
1389
  }
1159
1390
  break
1160
- case 'picture':
1391
+ case 'picture': {
1161
1392
  const setPicture = (0, WABinary_1.getBinaryNodeChild)(node, 'set')
1162
1393
  const delPicture = (0, WABinary_1.getBinaryNodeChild)(node, 'delete')
1163
- // TODO: WAJIDHASH stuff proper support inhouse
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
+ }
1164
1400
  ev.emit('contacts.update', [
1165
1401
  {
1166
- id: (0, WABinary_1.jidNormalizedUser)(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
1402
+ id: pictureOwnerJid,
1167
1403
  imgUrl: setPicture ? 'changed' : 'removed'
1168
1404
  }
1169
1405
  ])
@@ -1180,6 +1416,7 @@ const makeMessagesRecvSocket = config => {
1180
1416
  }
1181
1417
  }
1182
1418
  break
1419
+ }
1183
1420
  case 'account_sync':
1184
1421
  if (child.tag === 'disappearing_mode') {
1185
1422
  const newDuration = +child.attrs.duration
@@ -1195,18 +1432,60 @@ const makeMessagesRecvSocket = config => {
1195
1432
  }
1196
1433
  })
1197
1434
  } else if (child.tag === 'blocklist') {
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 })
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
+ }
1203
1471
  }
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')
1204
1483
  }
1205
1484
  break
1485
+ }
1206
1486
  case 'business':
1207
- // SMB privacy / data-sharing settings sync push
1208
- // (WhatsApp Web: WASmaxInBizSettingsSyncPrivacySettingRequest)
1209
1487
  if (child?.tag === 'privacy') {
1488
+
1210
1489
  ev.emit('business.privacy-settings-sync', {
1211
1490
  jid: from,
1212
1491
  categories: (0, WABinary_1.getBinaryNodeChildren)(child, 'category').map(c => ({
@@ -1215,11 +1494,37 @@ const makeMessagesRecvSocket = config => {
1215
1494
  })),
1216
1495
  attrs: child.attrs
1217
1496
  })
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
+ }
1218
1523
  }
1219
1524
  break
1220
1525
  case 'hosted':
1221
- // Coexistence (WhatsApp <-> Messenger/Instagram) onboarding/offboarding push
1222
- // (WhatsApp Web: WASmaxInCoexistenceOnboarding/OffboardingNotification)
1526
+
1527
+
1223
1528
  if (child?.tag === 'onboarding_status') {
1224
1529
  ev.emit('coexistence.update', {
1225
1530
  jid: from,
@@ -1237,18 +1542,28 @@ const makeMessagesRecvSocket = config => {
1237
1542
  break
1238
1543
  case 'link_code_companion_reg':
1239
1544
  const linkCodeCompanionReg = (0, WABinary_1.getBinaryNodeChild)(node, 'link_code_companion_reg')
1240
- const ref = toRequiredBuffer(
1241
- (0, WABinary_1.getBinaryNodeChildBuffer)(linkCodeCompanionReg, 'link_code_pairing_ref')
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'
1242
1549
  )
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
- )
1550
+ const primaryEphemeralPublicKeyWrappedBuffer = (0, WABinary_1.getBinaryNodeChildBuffer)(
1551
+ linkCodeCompanionReg,
1552
+ 'link_code_pairing_wrapped_primary_ephemeral_pub'
1251
1553
  )
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)
1252
1567
  const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped)
1253
1568
  const companionSharedKey = Utils_1.Curve.sharedKey(
1254
1569
  authState.creds.pairingEphemeralKeyPair.private,
@@ -1323,7 +1638,7 @@ const makeMessagesRecvSocket = config => {
1323
1638
  await handlePrivacyTokenNotification(node)
1324
1639
  break
1325
1640
  case 'security':
1326
- // Security notifications (compromised session, location change alerts)
1641
+
1327
1642
  const securityType = child?.tag || node.attrs.type
1328
1643
  const securityData = {
1329
1644
  type: securityType,
@@ -1335,7 +1650,7 @@ const makeMessagesRecvSocket = config => {
1335
1650
  ev.emit('security.alert', securityData)
1336
1651
  break
1337
1652
  case 'identity':
1338
- // Identity change notifications — peer changed their identity key
1653
+
1339
1654
  const identityJid = node.attrs.from
1340
1655
  const identityNewKey = child?.content ? Buffer.from(child.content) : undefined
1341
1656
  ev.emit('identity.update', {
@@ -1345,7 +1660,7 @@ const makeMessagesRecvSocket = config => {
1345
1660
  })
1346
1661
  break
1347
1662
  case 'server':
1348
- // Server-issued notifications (config changes, client config refresh)
1663
+
1349
1664
  const serverTag = child?.tag
1350
1665
  if (serverTag === 'config') {
1351
1666
  const configData = {}
@@ -1354,7 +1669,7 @@ const makeMessagesRecvSocket = config => {
1354
1669
  }
1355
1670
  ev.emit('server.config', configData)
1356
1671
  } else if (serverTag === 'app_state_key') {
1357
- // Server pushed a new app-state key — trigger resync
1672
+
1358
1673
  logger.info('server pushed app state key update')
1359
1674
  await resyncAppState(['critical_block', 'regular_high', 'regular_low'], false)
1360
1675
  } else {
@@ -1362,7 +1677,7 @@ const makeMessagesRecvSocket = config => {
1362
1677
  }
1363
1678
  break
1364
1679
  case 'status':
1365
- // Contact status (about) change notification
1680
+
1366
1681
  const statusOwner = node.attrs.from
1367
1682
  const statusText = child?.content ? child.content.toString() : undefined
1368
1683
  if (statusOwner && statusText !== undefined) {
@@ -1375,7 +1690,7 @@ const makeMessagesRecvSocket = config => {
1375
1690
  }
1376
1691
  break
1377
1692
  case 'usync':
1378
- // USync result push from server
1693
+
1379
1694
  const usyncResults = (0, WABinary_1.getBinaryNodeChildren)(child || node, 'user')
1380
1695
  if (usyncResults.length) {
1381
1696
  const updates = usyncResults
@@ -1392,12 +1707,12 @@ const makeMessagesRecvSocket = config => {
1392
1707
  }
1393
1708
  break
1394
1709
  case 'interop':
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
1710
+
1711
+
1712
+
1713
+
1714
+
1715
+
1401
1716
  handleInteropNotification(node, child)
1402
1717
  break
1403
1718
  }
@@ -1405,10 +1720,7 @@ const makeMessagesRecvSocket = config => {
1405
1720
  return result
1406
1721
  }
1407
1722
  }
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
- */
1723
+
1412
1724
  const tcTokenKnownJids = new Set()
1413
1725
  const tcTokenIndexLoaded = (async () => {
1414
1726
  try {
@@ -1449,7 +1761,7 @@ const makeMessagesRecvSocket = config => {
1449
1761
  const tokensNode = (0, WABinary_1.getBinaryNodeChild)(node, 'tokens')
1450
1762
  if (!tokensNode) return
1451
1763
  const from = (0, WABinary_1.jidNormalizedUser)(node.attrs.from)
1452
- // WA Web uses: senderLid ?? toLid(from) for the storage key
1764
+
1453
1765
  const senderLid =
1454
1766
  node.attrs.sender_lid && (0, WABinary_1.isLidUser)((0, WABinary_1.jidNormalizedUser)(node.attrs.sender_lid))
1455
1767
  ? (0, WABinary_1.jidNormalizedUser)(node.attrs.sender_lid)
@@ -1488,31 +1800,30 @@ const makeMessagesRecvSocket = config => {
1488
1800
  const newValue = ((await msgRetryCache.get(key)) || 0) + 1
1489
1801
  await msgRetryCache.set(key, newValue)
1490
1802
  }
1491
- const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
1803
+ const sendMessagesAgain = async (key, ids, retryNode) => {
1492
1804
  const remoteJid = key.remoteJid
1493
1805
  const participant = key.participant || remoteJid
1494
1806
  const retryCount = +retryNode.attrs.count || 1
1495
- const msgId = ids[0]
1496
- // Try to get messages from cache first, then fallback to getMessage
1807
+
1497
1808
  const msgs = []
1498
1809
  for (const id of ids) {
1499
1810
  let msg
1500
- // Try to get from retry cache first if enabled
1811
+
1501
1812
  if (messageRetryManager) {
1502
1813
  const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id)
1503
1814
  if (cachedMsg) {
1504
1815
  msg = cachedMsg.message
1505
1816
  logger.debug({ jid: remoteJid, id }, 'found message in retry cache')
1506
- // Mark retry as successful since we found the message
1817
+
1507
1818
  messageRetryManager.markRetrySuccess(id)
1508
1819
  }
1509
1820
  }
1510
- // Fallback to getMessage if not found in cache
1821
+
1511
1822
  if (!msg) {
1512
1823
  msg = await getMessage({ ...key, id })
1513
1824
  if (msg) {
1514
1825
  logger.debug({ jid: remoteJid, id }, 'found message via getMessage')
1515
- // Also mark as successful if found via getMessage
1826
+
1516
1827
  if (messageRetryManager) {
1517
1828
  messageRetryManager.markRetrySuccess(id)
1518
1829
  }
@@ -1520,57 +1831,16 @@ const makeMessagesRecvSocket = config => {
1520
1831
  }
1521
1832
  msgs.push(msg)
1522
1833
  }
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
1834
+
1835
+
1836
+
1526
1837
  const sendToAll = !(0, WABinary_1.jidDecode)(participant)?.device
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
1838
+
1570
1839
  let shouldRecreateSession = false
1571
1840
  let recreateReason = ''
1572
- if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
1841
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
1573
1842
  try {
1843
+ const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
1574
1844
  const hasSession = await signalRepository.validateSession(participant)
1575
1845
  const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists)
1576
1846
  shouldRecreateSession = result.recreate
@@ -1583,9 +1853,7 @@ const makeMessagesRecvSocket = config => {
1583
1853
  logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry')
1584
1854
  }
1585
1855
  }
1586
- if (!injectedFromBundle) {
1587
- await assertSessions([participant], true)
1588
- }
1856
+ await assertSessions([participant], true)
1589
1857
  if ((0, WABinary_1.isJidGroup)(remoteJid)) {
1590
1858
  await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } })
1591
1859
  }
@@ -1634,14 +1902,59 @@ const makeMessagesRecvSocket = config => {
1634
1902
  const items = (0, WABinary_1.getBinaryNodeChildren)(content[0], 'item')
1635
1903
  ids.push(...items.map(i => i.attrs.id))
1636
1904
  }
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
+ }
1637
1950
  try {
1638
1951
  await Promise.all([
1639
1952
  receiptMutex.mutex(async () => {
1640
1953
  const status = (0, Utils_1.getStatusFromReceiptType)(attrs.type)
1641
1954
  if (
1642
1955
  typeof status !== 'undefined' &&
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
1956
+
1957
+
1645
1958
  (status >= index_js_1.proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)
1646
1959
  ) {
1647
1960
  if ((0, WABinary_1.isJidGroup)(remoteJid) || (0, WABinary_1.isJidStatusBroadcast)(remoteJid)) {
@@ -1670,7 +1983,7 @@ const makeMessagesRecvSocket = config => {
1670
1983
  }
1671
1984
  }
1672
1985
  if (attrs.type === 'retry') {
1673
- // correctly set who is asking for the retry
1986
+
1674
1987
  key.participant = key.participant || attrs.from
1675
1988
  const retryNode = (0, WABinary_1.getBinaryNodeChild)(node, 'retry')
1676
1989
  if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
@@ -1678,7 +1991,7 @@ const makeMessagesRecvSocket = config => {
1678
1991
  try {
1679
1992
  await updateSendMessageAgainCount(ids[0], key.participant)
1680
1993
  logger.debug({ attrs, key }, 'recv retry request')
1681
- await sendMessagesAgain(key, ids, retryNode, node)
1994
+ await sendMessagesAgain(key, ids, retryNode)
1682
1995
  } catch (error) {
1683
1996
  logger.error(
1684
1997
  { key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' },
@@ -1777,11 +2090,11 @@ const makeMessagesRecvSocket = config => {
1777
2090
  const groupJid = node.attrs.from
1778
2091
  const communityJid = linkedParentMap[groupJid]
1779
2092
  const encNode = (0, WABinary_1.getBinaryNodeChild)(node, 'enc')
1780
- // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
2093
+
1781
2094
  if (encNode?.attrs.type === 'msmsg') {
1782
- // await sendMessageAck(node, Utils_1.NACK_REASONS.MissingMessageSecret)
1783
- // return
1784
- // Pre-populate botMessageSecrets from store so msmsg can be decrypted after restart
2095
+
2096
+
2097
+
1785
2098
  if (getMessage) {
1786
2099
  const metaNode = (0, WABinary_1.getBinaryNodeChild)(node, 'meta')
1787
2100
  const targetId = metaNode?.attrs?.target_id
@@ -1819,7 +2132,7 @@ const makeMessagesRecvSocket = config => {
1819
2132
  )
1820
2133
  }
1821
2134
  const alt = msg.key.participantAlt || msg.key.remoteJidAlt
1822
- // store new mappings we didn't have before
2135
+
1823
2136
  if (!!alt) {
1824
2137
  const altServer = (0, WABinary_1.jidDecode)(alt)?.server
1825
2138
  const primaryJid = msg.key.participant || msg.key.remoteJid
@@ -1832,6 +2145,32 @@ const makeMessagesRecvSocket = config => {
1832
2145
  await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }])
1833
2146
  await signalRepository.migrateSession(alt, primaryJid)
1834
2147
  }
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
+ }
1835
2174
  }
1836
2175
  await messageMutex.mutex(async () => {
1837
2176
  await decrypt()
@@ -1852,7 +2191,7 @@ const makeMessagesRecvSocket = config => {
1852
2191
  if (msg.key?.remoteJid && msg.key?.id && msg.message && messageRetryManager) {
1853
2192
  messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message)
1854
2193
  }
1855
- // message failed to decrypt
2194
+
1856
2195
  if (msg.messageStubType === index_js_1.proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
1857
2196
  if (msg?.messageStubParameters?.[0] === Utils_1.MISSING_KEYS_ERROR_TEXT) {
1858
2197
  if (isInteropNode) logger.warn({ id: msg.key.id }, '[interop] decrypt failed: MISSING_KEYS')
@@ -1860,8 +2199,8 @@ const makeMessagesRecvSocket = config => {
1860
2199
  return sendMessageAck(node, Utils_1.NACK_REASONS.ParsingError)
1861
2200
  }
1862
2201
  if (msg.messageStubParameters?.[0] === Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT) {
1863
- // Message arrived without encryption (e.g. CTWA ads messages).
1864
- // Check if this is eligible for placeholder resend (matching WA Web filters).
2202
+
2203
+
1865
2204
  const unavailableNode = (0, WABinary_1.getBinaryNodeChild)(node, 'unavailable')
1866
2205
  const unavailableType = unavailableNode?.attrs?.type
1867
2206
  if (
@@ -1882,18 +2221,18 @@ const makeMessagesRecvSocket = config => {
1882
2221
  acked = true
1883
2222
  return sendMessageAck(node)
1884
2223
  }
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.
2224
+
2225
+
2226
+
2227
+
1889
2228
  const cleanKey = {
1890
2229
  remoteJid: msg.key.remoteJid,
1891
2230
  fromMe: msg.key.fromMe,
1892
2231
  id: msg.key.id,
1893
2232
  participant: msg.key.participant
1894
2233
  }
1895
- // Cache the original message metadata so the PDO response handler
1896
- // can preserve key fields (LID details etc.) that the phone may omit
2234
+
2235
+
1897
2236
  const msgData = {
1898
2237
  key: msg.key,
1899
2238
  messageTimestamp: msg.messageTimestamp,
@@ -1918,9 +2257,9 @@ const makeMessagesRecvSocket = config => {
1918
2257
  })
1919
2258
  acked = true
1920
2259
  await sendMessageAck(node)
1921
- // Don't return — fall through to upsertMessage so the stub is emitted
2260
+
1922
2261
  } else {
1923
- // Skip retry for expired status messages (>24h old)
2262
+
1924
2263
  if ((0, WABinary_1.isJidStatusBroadcast)(msg.key.remoteJid)) {
1925
2264
  const messageAge = (0, Utils_1.unixTimestampSeconds)() - (0, Utils_1.toNumber)(msg.messageTimestamp)
1926
2265
  if (messageAge > Defaults_1.STATUS_EXPIRY_SECONDS) {
@@ -1935,14 +2274,14 @@ const makeMessagesRecvSocket = config => {
1935
2274
  const errorMessage = msg?.messageStubParameters?.[0] || ''
1936
2275
  const isPreKeyError = errorMessage.includes('PreKey')
1937
2276
  logger.debug(`[handleMessage] Attempting retry request for failed decryption`)
1938
- // Handle both pre-key and normal retries in single mutex
2277
+
1939
2278
  await retryMutex.mutex(async () => {
1940
2279
  try {
1941
2280
  if (!ws.isOpen) {
1942
2281
  logger.debug({ node }, 'Connection closed, skipping retry')
1943
2282
  return
1944
2283
  }
1945
- // Handle pre-key errors with upload and delay
2284
+
1946
2285
  if (isPreKeyError) {
1947
2286
  logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying')
1948
2287
  try {
@@ -1961,7 +2300,7 @@ const makeMessagesRecvSocket = config => {
1961
2300
  }
1962
2301
  } catch (err) {
1963
2302
  logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry')
1964
- // Still attempt retry even if pre-key upload failed
2303
+
1965
2304
  try {
1966
2305
  const encNode = (0, WABinary_1.getBinaryNodeChild)(node, 'enc')
1967
2306
  await sendRetryRequest(node, !encNode)
@@ -1979,34 +2318,34 @@ const makeMessagesRecvSocket = config => {
1979
2318
  }
1980
2319
  const isNewsletter = (0, WABinary_1.isJidNewsletter)(msg.key.remoteJid)
1981
2320
  if (!isNewsletter) {
1982
- // no type in the receipt => message delivered
2321
+
1983
2322
  let type = undefined
1984
2323
  let participant = msg.key.participant
1985
2324
  if (communityJid) {
1986
2325
  msg.communityJid = communityJid
1987
2326
  }
1988
2327
  if (category === 'peer') {
1989
- // special peer message
2328
+
1990
2329
  type = 'peer_msg'
1991
2330
  } else if (msg.key.fromMe) {
1992
- // message was sent by us from a different device
2331
+
1993
2332
  type = 'sender'
1994
- // need to specially handle this case
2333
+
1995
2334
  if ((0, WABinary_1.isLidUser)(msg.key.remoteJid) || (0, WABinary_1.isLidUser)(msg.key.remoteJidAlt)) {
1996
- participant = author // TODO: investigate sending receipts to LIDs and not PNs
2335
+ participant = author
1997
2336
  }
1998
2337
  } else if (!sendActiveReceipts) {
1999
2338
  type = 'inactive'
2000
2339
  }
2001
2340
  acked = true
2002
- // Pass sts from the original stanza for interop contacts (BirdyChat/Haiket)
2341
+
2003
2342
  const interopSts = (0, WABinary_1.isInteropUser)(msg.key.remoteJid) ? node.attrs.sts : undefined
2004
2343
  await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type, interopSts)
2005
- // send ack for history message
2344
+
2006
2345
  const isAnyHistoryMsg = (0, Utils_1.getHistoryMsg)(msg.message)
2007
2346
  if (isAnyHistoryMsg) {
2008
2347
  const jid = (0, WABinary_1.jidNormalizedUser)(msg.key.remoteJid)
2009
- await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync') // TODO: investigate
2348
+ await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync')
2010
2349
  }
2011
2350
  } else {
2012
2351
  acked = true
@@ -2068,6 +2407,19 @@ const makeMessagesRecvSocket = config => {
2068
2407
  }
2069
2408
  }
2070
2409
  }
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
+ }))
2071
2423
  const handleCall = async node => {
2072
2424
  try {
2073
2425
  const { attrs } = node
@@ -2095,20 +2447,88 @@ const makeMessagesRecvSocket = config => {
2095
2447
  }
2096
2448
  }
2097
2449
  if (status === 'offer') {
2098
- call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(infoChild, 'video')
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
2099
2454
  call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid']
2100
2455
  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
+ }
2101
2486
  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']
2102
2489
  }
2103
2490
  const existingCall = await callOfferCache.get(call.id)
2104
- // use existing call info to populate this event
2491
+
2105
2492
  if (existingCall) {
2106
2493
  call.isVideo = existingCall.isVideo
2107
2494
  call.isGroup = existingCall.isGroup
2108
2495
  call.callerPn = call.callerPn || existingCall.callerPn
2109
2496
  }
2110
- // delete data once call has ended
2111
- if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
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
+ ) {
2112
2532
  await callOfferCache.del(call.id)
2113
2533
  }
2114
2534
  await normalizeCallEventJids(call, infoChild)
@@ -2119,15 +2539,32 @@ const makeMessagesRecvSocket = config => {
2119
2539
  await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack call'))
2120
2540
  }
2121
2541
  }
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'])
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
+ ])
2127
2564
  const handleStandaloneCallStanza = async node => {
2128
2565
  try {
2129
2566
  if (!CALL_STATE_TAGS.has(node.tag)) {
2130
- return // media/relay signalling (transport, video, duration, mute_v2, lobby, …): ack only
2567
+ return
2131
2568
  }
2132
2569
  const { attrs } = node
2133
2570
  const status = (0, Utils_1.getCallStatusFromNode)(node)
@@ -2143,9 +2580,24 @@ const makeMessagesRecvSocket = config => {
2143
2580
  status
2144
2581
  }
2145
2582
  if (status === 'offer') {
2146
- call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(node, 'video')
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
2147
2587
  call.isGroup = attrs.type === 'group' || !!attrs['group-jid']
2148
2588
  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
+ }
2149
2601
  if (callId) {
2150
2602
  await callOfferCache.set(callId, call)
2151
2603
  }
@@ -2156,7 +2608,45 @@ const makeMessagesRecvSocket = config => {
2156
2608
  call.isGroup = existingCall.isGroup
2157
2609
  call.callerPn = call.callerPn || existingCall.callerPn
2158
2610
  }
2159
- if (callId && (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate')) {
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
+ ) {
2160
2650
  await callOfferCache.del(callId)
2161
2651
  }
2162
2652
  await normalizeCallEventJids(call, node)
@@ -2169,26 +2659,26 @@ const makeMessagesRecvSocket = config => {
2169
2659
  }
2170
2660
  const handleBadAck = async ({ attrs }) => {
2171
2661
  const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id }
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
2662
+
2663
+
2664
+
2665
+
2666
+
2667
+
2668
+
2669
+
2670
+
2671
+
2672
+
2673
+
2674
+
2675
+
2676
+
2187
2677
  if (attrs.error) {
2188
2678
  if (attrs.error === Utils_1.SERVER_ERROR_CODES.MissingTcToken) {
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.
2679
+
2680
+
2681
+
2192
2682
  logger.warn(
2193
2683
  { msgId: attrs.id, from: attrs.from },
2194
2684
  'error 463: account restricted or missing tctoken for contact'
@@ -2210,23 +2700,54 @@ const makeMessagesRecvSocket = config => {
2210
2700
  }
2211
2701
  }
2212
2702
  ])
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
- // }
2703
+
2704
+
2705
+
2706
+
2707
+
2708
+
2709
+
2710
+
2711
+
2712
+
2713
+
2714
+
2715
+
2226
2716
  }
2227
2717
  }
2228
- /// processes a node with the given function
2229
- /// and adds the task to the existing buffer if we're buffering events
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
+
2230
2751
  const processNodeWithBuffer = async (node, identifier, exec) => {
2231
2752
  ev.buffer()
2232
2753
  await execTask()
@@ -2265,7 +2786,7 @@ const makeMessagesRecvSocket = config => {
2265
2786
  const setNodeLoggerListener = () => {
2266
2787
  return latestNodeInMemory
2267
2788
  }
2268
- // recv a message
2789
+
2269
2790
  ws.on('CB:message', async node => {
2270
2791
  nodelogger(node)
2271
2792
  await processNode('message', node, 'processing message', handleMessage)
@@ -2274,10 +2795,35 @@ const makeMessagesRecvSocket = config => {
2274
2795
  nodelogger(node)
2275
2796
  await processNode('call', node, 'handling call', handleCall)
2276
2797
  })
2277
- // additive: top-level call-signalling stanzas (some accounts send these instead of <call>)
2798
+
2278
2799
  for (const callTag of [
2279
- 'offer', 'offer_notice', 'terminate', 'accept', 'reject', 'preaccept',
2280
- 'transport', 'video', 'duration', 'mute_v2', 'lobby', 'heartbeat', 'relaylatency', 'link_query'
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'
2281
2827
  ]) {
2282
2828
  ws.on('CB:' + callTag, node => {
2283
2829
  nodelogger(node)
@@ -2292,6 +2838,18 @@ const makeMessagesRecvSocket = config => {
2292
2838
  nodelogger(node)
2293
2839
  await processNode('notification', node, 'handling notification', handleNotification)
2294
2840
  })
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
+ })
2295
2853
  ws.on('CB:ack,class:message', node => {
2296
2854
  nodelogger(node)
2297
2855
  handleBadAck(node).catch(error => onUnexpectedError(error, 'handling bad ack'))
@@ -2321,7 +2879,7 @@ const makeMessagesRecvSocket = config => {
2321
2879
  return
2322
2880
  }
2323
2881
  nodelogger(call)
2324
- // missed call + group call notification message generation
2882
+
2325
2883
  if (call.status === 'timeout' || (call.status === 'offer' && call.isGroup)) {
2326
2884
  const msg = {
2327
2885
  key: {
@@ -2357,7 +2915,7 @@ const makeMessagesRecvSocket = config => {
2357
2915
  sendActiveReceipts = isOnline
2358
2916
  logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`)
2359
2917
  }
2360
- // Daily cleanup of expired tctokens (mirrors WA Web's CLEAN_TC_TOKENS task)
2918
+
2361
2919
  if (isOnline) {
2362
2920
  const now = Date.now()
2363
2921
  const DAY_MS = 24 * 60 * 60 * 1000