@bellaxchuu/flowred 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/WAProto/GenerateStatics.sh +3 -0
  4. package/WAProto/WAProto.proto +6902 -0
  5. package/WAProto/fix-imports.js +85 -0
  6. package/WAProto/index.js +242946 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Defaults/index.js +190 -0
  9. package/lib/Defaults/phonenumber-mcc.json +223 -0
  10. package/lib/Signal/Group/ciphertext-message.js +15 -0
  11. package/lib/Signal/Group/group-session-builder.js +92 -0
  12. package/lib/Signal/Group/group_cipher.js +89 -0
  13. package/lib/Signal/Group/index.js +136 -0
  14. package/lib/Signal/Group/keyhelper.js +73 -0
  15. package/lib/Signal/Group/sender-chain-key.js +32 -0
  16. package/lib/Signal/Group/sender-key-distribution-message.js +66 -0
  17. package/lib/Signal/Group/sender-key-message.js +69 -0
  18. package/lib/Signal/Group/sender-key-name.js +50 -0
  19. package/lib/Signal/Group/sender-key-record.js +44 -0
  20. package/lib/Signal/Group/sender-key-state.js +97 -0
  21. package/lib/Signal/Group/sender-message-key.js +30 -0
  22. package/lib/Signal/libsignal.js +490 -0
  23. package/lib/Signal/lid-mapping.js +262 -0
  24. package/lib/Socket/Client/index.js +30 -0
  25. package/lib/Socket/Client/types.js +13 -0
  26. package/lib/Socket/Client/websocket.js +62 -0
  27. package/lib/Socket/aigroups.js +240 -0
  28. package/lib/Socket/business.js +422 -0
  29. package/lib/Socket/chats.js +1816 -0
  30. package/lib/Socket/communities.js +549 -0
  31. package/lib/Socket/graphql.js +656 -0
  32. package/lib/Socket/groups.js +764 -0
  33. package/lib/Socket/index.js +39 -0
  34. package/lib/Socket/interactive-handler.js +522 -0
  35. package/lib/Socket/interop.js +549 -0
  36. package/lib/Socket/luxu.js +387 -0
  37. package/lib/Socket/managed-account.js +183 -0
  38. package/lib/Socket/messages-recv.js +2440 -0
  39. package/lib/Socket/messages-send.js +1853 -0
  40. package/lib/Socket/mex.js +60 -0
  41. package/lib/Socket/newsletter.js +820 -0
  42. package/lib/Socket/privacy.js +449 -0
  43. package/lib/Socket/registration.js +427 -0
  44. package/lib/Socket/socket.js +1092 -0
  45. package/lib/Socket/text-router.js +83 -0
  46. package/lib/Socket/username.js +243 -0
  47. package/lib/Store/index.js +36 -0
  48. package/lib/Store/keyed-db.js +108 -0
  49. package/lib/Store/make-cache-manager-store.js +90 -0
  50. package/lib/Store/make-in-memory-store.js +604 -0
  51. package/lib/Store/make-ordered-dictionary.js +81 -0
  52. package/lib/Store/object-repository.js +29 -0
  53. package/lib/Types/Auth.js +38 -0
  54. package/lib/Types/Bussines.js +2 -0
  55. package/lib/Types/Call.js +2 -0
  56. package/lib/Types/Chat.js +4 -0
  57. package/lib/Types/Contact.js +2 -0
  58. package/lib/Types/Events.js +2 -0
  59. package/lib/Types/GroupMetadata.js +2 -0
  60. package/lib/Types/Label.js +27 -0
  61. package/lib/Types/LabelAssociation.js +9 -0
  62. package/lib/Types/Message.js +95 -0
  63. package/lib/Types/Mex.js +112 -0
  64. package/lib/Types/Newsletter.js +121 -0
  65. package/lib/Types/Product.js +2 -0
  66. package/lib/Types/Signal.js +2 -0
  67. package/lib/Types/Socket.js +2 -0
  68. package/lib/Types/State.js +70 -0
  69. package/lib/Types/USync.js +2 -0
  70. package/lib/Types/index.js +55 -0
  71. package/lib/Utils/auth-utils.js +306 -0
  72. package/lib/Utils/browser-utils.js +114 -0
  73. package/lib/Utils/business.js +247 -0
  74. package/lib/Utils/chat-utils.js +1208 -0
  75. package/lib/Utils/command-loader.js +100 -0
  76. package/lib/Utils/companion-reg-client-utils.js +42 -0
  77. package/lib/Utils/consumer-application.js +107 -0
  78. package/lib/Utils/crypto.js +155 -0
  79. package/lib/Utils/curve25519-js.js +275 -0
  80. package/lib/Utils/decode-wa-message.js +560 -0
  81. package/lib/Utils/event-buffer.js +607 -0
  82. package/lib/Utils/generics.js +563 -0
  83. package/lib/Utils/group-history.js +60 -0
  84. package/lib/Utils/history.js +244 -0
  85. package/lib/Utils/identity-change-handler.js +52 -0
  86. package/lib/Utils/index.js +57 -0
  87. package/lib/Utils/jid-display-normalization.js +218 -0
  88. package/lib/Utils/link-preview.js +143 -0
  89. package/lib/Utils/logger.js +9 -0
  90. package/lib/Utils/lt-hash.js +39 -0
  91. package/lib/Utils/make-mutex.js +36 -0
  92. package/lib/Utils/message-composer.js +479 -0
  93. package/lib/Utils/message-retry-manager.js +270 -0
  94. package/lib/Utils/messages-media.js +896 -0
  95. package/lib/Utils/messages.js +2904 -0
  96. package/lib/Utils/meta-ai-msmsg.js +262 -0
  97. package/lib/Utils/native-bridge.js +82 -0
  98. package/lib/Utils/noise-handler.js +196 -0
  99. package/lib/Utils/offline-node-processor.js +42 -0
  100. package/lib/Utils/pre-key-manager.js +107 -0
  101. package/lib/Utils/process-message.js +1048 -0
  102. package/lib/Utils/reporting-utils.js +262 -0
  103. package/lib/Utils/session-pool.js +94 -0
  104. package/lib/Utils/signal.js +224 -0
  105. package/lib/Utils/stanza-ack.js +40 -0
  106. package/lib/Utils/sticker.js +145 -0
  107. package/lib/Utils/sync-action-utils.js +54 -0
  108. package/lib/Utils/tc-token-utils.js +161 -0
  109. package/lib/Utils/use-multi-file-auth-state.js +121 -0
  110. package/lib/Utils/validate-connection.js +219 -0
  111. package/lib/Utils/view-once-cache.js +79 -0
  112. package/lib/Utils/voip-rekey.js +22 -0
  113. package/lib/WABinary/constants.js +1304 -0
  114. package/lib/WABinary/decode.js +343 -0
  115. package/lib/WABinary/encode.js +228 -0
  116. package/lib/WABinary/generic-utils.js +240 -0
  117. package/lib/WABinary/index.js +33 -0
  118. package/lib/WABinary/jid-utils.js +361 -0
  119. package/lib/WABinary/types.js +2 -0
  120. package/lib/WAM/BinaryInfo.js +13 -0
  121. package/lib/WAM/constants.js +39486 -0
  122. package/lib/WAM/encode.js +142 -0
  123. package/lib/WAM/index.js +31 -0
  124. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +55 -0
  125. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +43 -0
  126. package/lib/WAUSync/Protocols/USyncContactProtocol.js +54 -0
  127. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +57 -0
  128. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +30 -0
  129. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +59 -0
  130. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +31 -0
  131. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +32 -0
  132. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +29 -0
  133. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +39 -0
  134. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +38 -0
  135. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +27 -0
  136. package/lib/WAUSync/Protocols/index.js +39 -0
  137. package/lib/WAUSync/USyncQuery.js +133 -0
  138. package/lib/WAUSync/USyncUser.js +50 -0
  139. package/lib/WAUSync/index.js +31 -0
  140. package/lib/antiban.js +4637 -0
  141. package/lib/index.js +45 -0
  142. package/package.json +107 -0
@@ -0,0 +1,2440 @@
1
+ 'use strict'
2
+ var __importDefault =
3
+ (this && this.__importDefault) ||
4
+ function (mod) {
5
+ return mod && mod.__esModule ? mod : { default: mod }
6
+ }
7
+ Object.defineProperty(exports, '__esModule', { value: true })
8
+ exports.makeMessagesRecvSocket = void 0
9
+ const node_cache_1 = __importDefault(require('@cacheable/node-cache'))
10
+ const boom_1 = require('@hapi/boom')
11
+ const crypto_1 = require('crypto')
12
+ const index_js_1 = require('../../WAProto/index.js')
13
+ const Defaults_1 = require('../Defaults')
14
+ const Types_1 = require('../Types')
15
+ const Utils_1 = require('../Utils')
16
+ const jid_display_normalization_1 = require('../Utils/jid-display-normalization')
17
+ const make_mutex_1 = require('../Utils/make-mutex')
18
+ const offline_node_processor_1 = require('../Utils/offline-node-processor')
19
+ const stanza_ack_1 = require('../Utils/stanza-ack')
20
+ const tc_token_utils_1 = require('../Utils/tc-token-utils')
21
+ const WABinary_1 = require('../WABinary')
22
+ const groups_1 = require('./groups')
23
+ const aigroup_1 = require('./aigroups')
24
+ const messages_send_1 = require('./messages-send')
25
+
26
+ const makeMessagesRecvSocket = config => {
27
+ const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } =
28
+ config
29
+ const sock = (0, messages_send_1.makeMessagesSocket)(config)
30
+ const {
31
+ ev,
32
+ authState,
33
+ ws,
34
+ messageMutex,
35
+ notificationMutex,
36
+ receiptMutex,
37
+ signalRepository,
38
+ query,
39
+ upsertMessage,
40
+ resyncAppState,
41
+ onUnexpectedError,
42
+ assertSessions,
43
+ sendNode,
44
+ relayMessage,
45
+ sendReceipt,
46
+ uploadPreKeys,
47
+ sendPeerDataOperationMessage,
48
+ messageRetryManager,
49
+ issuePrivacyTokens
50
+ } = sock
51
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
52
+ // Track when the socket fully opens so pending pre-connect messages are treated as history
53
+ const socketCreatedAt = Math.floor(Date.now() / 1000)
54
+ let isConnected = false
55
+ /** this mutex ensures that each retryRequest will wait for the previous one to finish */
56
+ const retryMutex = (0, make_mutex_1.makeMutex)()
57
+ const msgRetryCache =
58
+ config.msgRetryCounterCache ||
59
+ new node_cache_1.default({
60
+ stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
61
+ useClones: false
62
+ })
63
+ const callOfferCache =
64
+ config.callOfferCache ||
65
+ new node_cache_1.default({
66
+ stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
67
+ useClones: false
68
+ })
69
+ const placeholderResendCache =
70
+ config.placeholderResendCache ||
71
+ new node_cache_1.default({
72
+ stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
73
+ useClones: false
74
+ })
75
+ // Debounce identity-change session refreshes per JID to avoid bursts
76
+ const identityAssertDebounce = new node_cache_1.default({ stdTTL: 5, useClones: false })
77
+ let sendActiveReceipts = false
78
+ const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
79
+ if (!authState.creds.me?.id) {
80
+ throw new boom_1.Boom('Not authenticated')
81
+ }
82
+ const pdoMessage = {
83
+ historySyncOnDemandRequest: {
84
+ chatJid: oldestMsgKey.remoteJid,
85
+ oldestMsgFromMe: oldestMsgKey.fromMe,
86
+ oldestMsgId: oldestMsgKey.id,
87
+ oldestMsgTimestampMs: oldestMsgTimestamp,
88
+ onDemandMsgCount: count
89
+ },
90
+ peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND
91
+ }
92
+ return sendPeerDataOperationMessage(pdoMessage)
93
+ }
94
+ const requestPlaceholderResend = async (messageKey, msgData) => {
95
+ if (!authState.creds.me?.id) {
96
+ throw new boom_1.Boom('Not authenticated')
97
+ }
98
+ if (await placeholderResendCache.get(messageKey?.id)) {
99
+ logger.debug({ messageKey }, 'already requested resend')
100
+ return
101
+ } else {
102
+ // Store original message data so PDO response handler can preserve
103
+ // metadata (LID details, timestamps, etc.) that the phone may omit
104
+ await placeholderResendCache.set(messageKey?.id, msgData || true)
105
+ }
106
+ await (0, Utils_1.delay)(2000)
107
+ if (!(await placeholderResendCache.get(messageKey?.id))) {
108
+ logger.debug({ messageKey }, 'message received while resend requested')
109
+ return 'RESOLVED'
110
+ }
111
+ const pdoMessage = {
112
+ placeholderMessageResendRequest: [
113
+ {
114
+ messageKey
115
+ }
116
+ ],
117
+ peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
118
+ }
119
+ setTimeout(async () => {
120
+ if (await placeholderResendCache.get(messageKey?.id)) {
121
+ logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline')
122
+ await placeholderResendCache.del(messageKey?.id)
123
+ }
124
+ }, 8000)
125
+ return sendPeerDataOperationMessage(pdoMessage)
126
+ }
127
+ /**
128
+ * Request a Waffle (Meta-account) linking nonce from the paired phone.
129
+ * The phone responds via a PeerDataOperationRequestResponseMessage containing
130
+ * a WaffleNonceFetchResponse with the nonce needed for Meta account linking.
131
+ */
132
+ const requestWaffleNonce = async () => {
133
+ if (!authState.creds.me?.id) {
134
+ throw new boom_1.Boom('Not authenticated')
135
+ }
136
+ return sendPeerDataOperationMessage({
137
+ peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.WAFFLE_LINKING_NONCE_FETCH
138
+ })
139
+ }
140
+ /**
141
+ * Request a Companion Canonical User nonce from the paired phone.
142
+ * Used during companion linking to canonicalize the user identity across devices.
143
+ * The phone responds with a CompanionCanonicalUserNonceFetchResponse (nonce + waFbid).
144
+ *
145
+ * @param {string} [registrationTraceId] - Optional trace ID for this registration attempt.
146
+ */
147
+ const requestCompanionCanonicalNonce = async (registrationTraceId) => {
148
+ if (!authState.creds.me?.id) {
149
+ throw new boom_1.Boom('Not authenticated')
150
+ }
151
+ return sendPeerDataOperationMessage({
152
+ companionCanonicalUserNonceFetchRequest: registrationTraceId ? { registrationTraceId } : {},
153
+ peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_CANONICAL_USER_NONCE_FETCH
154
+ })
155
+ }
156
+ /**
157
+ * Request a Companion Meta nonce from the paired phone.
158
+ * Used during Meta-account companion linking flow.
159
+ * The phone responds with a CompanionMetaNonceFetchResponse (nonce).
160
+ */
161
+ const requestCompanionMetaNonce = async () => {
162
+ if (!authState.creds.me?.id) {
163
+ throw new boom_1.Boom('Not authenticated')
164
+ }
165
+ return sendPeerDataOperationMessage({
166
+ peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_META_NONCE_FETCH
167
+ })
168
+ }
169
+ // Handles mex newsletter notifications
170
+ const handleMexNewsletterNotification = async node => {
171
+ const mexNode = (0, WABinary_1.getBinaryNodeChild)(node, 'mex')
172
+ if (!mexNode?.content) {
173
+ logger.warn({ node }, 'Invalid mex newsletter notification')
174
+ return
175
+ }
176
+ let data
177
+ try {
178
+ data = JSON.parse(mexNode.content.toString())
179
+ } catch (error) {
180
+ logger.error({ err: error, node }, 'Failed to parse mex newsletter notification')
181
+ return
182
+ }
183
+ const operation = data?.operation
184
+ const updates = data?.updates
185
+ if (!updates || !operation) {
186
+ logger.warn({ data }, 'Invalid mex newsletter notification content')
187
+ return
188
+ }
189
+ logger.info({ operation, updates }, 'got mex newsletter notification')
190
+ switch (operation) {
191
+ case 'NotificationNewsletterUpdate':
192
+ for (const update of updates) {
193
+ if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
194
+ ev.emit('newsletter-settings.update', {
195
+ id: update.jid,
196
+ update: update.settings
197
+ })
198
+ }
199
+ }
200
+ break
201
+ case 'NotificationNewsletterAdminPromote':
202
+ for (const update of updates) {
203
+ if (update.jid && update.user) {
204
+ ev.emit('newsletter-participants.update', {
205
+ id: update.jid,
206
+ author: node.attrs.from,
207
+ user: update.user,
208
+ new_role: 'ADMIN',
209
+ action: 'promote'
210
+ })
211
+ }
212
+ }
213
+ break
214
+ default:
215
+ logger.info({ operation, data }, 'Unhandled mex newsletter notification')
216
+ break
217
+ }
218
+ }
219
+ // Handles newsletter notifications
220
+ const handleNewsletterNotification = async node => {
221
+ const from = node.attrs.from
222
+ const child = (0, WABinary_1.getAllBinaryNodeChildren)(node)[0]
223
+ const author = node.attrs.participant
224
+ logger.info({ from, child }, 'got newsletter notification')
225
+ switch (child.tag) {
226
+ case 'reaction':
227
+ const reactionUpdate = {
228
+ id: from,
229
+ server_id: child.attrs.message_id,
230
+ reaction: {
231
+ code: (0, WABinary_1.getBinaryNodeChildString)(child, 'reaction'),
232
+ count: 1
233
+ }
234
+ }
235
+ ev.emit('newsletter.reaction', reactionUpdate)
236
+ break
237
+ case 'view':
238
+ const viewUpdate = {
239
+ id: from,
240
+ server_id: child.attrs.message_id,
241
+ count: parseInt(child.content?.toString() || '0', 10)
242
+ }
243
+ ev.emit('newsletter.view', viewUpdate)
244
+ break
245
+ case 'participant':
246
+ const participantUpdate = {
247
+ id: from,
248
+ author,
249
+ user: child.attrs.jid,
250
+ action: child.attrs.action,
251
+ new_role: child.attrs.role
252
+ }
253
+ ev.emit('newsletter-participants.update', participantUpdate)
254
+ break
255
+ case 'update':
256
+ const settingsNode = (0, WABinary_1.getBinaryNodeChild)(child, 'settings')
257
+ if (settingsNode) {
258
+ const update = {}
259
+ const nameNode = (0, WABinary_1.getBinaryNodeChild)(settingsNode, 'name')
260
+ if (nameNode?.content) update.name = nameNode.content.toString()
261
+ const descriptionNode = (0, WABinary_1.getBinaryNodeChild)(settingsNode, 'description')
262
+ if (descriptionNode?.content) update.description = descriptionNode.content.toString()
263
+ ev.emit('newsletter-settings.update', {
264
+ id: from,
265
+ update
266
+ })
267
+ }
268
+ break
269
+ case 'message':
270
+ const plaintextNode = (0, WABinary_1.getBinaryNodeChild)(child, 'plaintext')
271
+ if (plaintextNode?.content) {
272
+ try {
273
+ const contentBuf =
274
+ typeof plaintextNode.content === 'string'
275
+ ? Buffer.from(plaintextNode.content, 'binary')
276
+ : Buffer.from(plaintextNode.content)
277
+ const messageProto = index_js_1.proto.Message.decode(contentBuf).toJSON()
278
+ const fullMessage = index_js_1.proto.WebMessageInfo.fromObject({
279
+ key: {
280
+ remoteJid: from,
281
+ id: child.attrs.message_id || child.attrs.server_id,
282
+ fromMe: false // TODO: is this really true though
283
+ },
284
+ message: messageProto,
285
+ messageTimestamp: +child.attrs.t
286
+ }).toJSON()
287
+ await upsertMessage(fullMessage, 'append')
288
+ logger.info('Processed plaintext newsletter message')
289
+ } catch (error) {
290
+ logger.error({ error }, 'Failed to decode plaintext newsletter message')
291
+ }
292
+ }
293
+ break
294
+ case 'live_updates':
295
+ // Live view count / engagement update
296
+ const liveCount = parseInt(child.attrs.count || child.content?.toString() || '0', 10)
297
+ ev.emit('newsletter.live-update', {
298
+ id: from,
299
+ server_id: child.attrs.message_id || child.attrs.server_id,
300
+ liveViewers: liveCount,
301
+ timestamp: child.attrs.t ? +child.attrs.t : undefined
302
+ })
303
+ break
304
+ case 'pin':
305
+ ev.emit('newsletter.pin', {
306
+ id: from,
307
+ server_id: child.attrs.message_id || child.attrs.server_id,
308
+ pinned: child.attrs.action !== 'unpin'
309
+ })
310
+ break
311
+ case 'category':
312
+ ev.emit('newsletter-settings.update', {
313
+ id: from,
314
+ update: { category: child.attrs.value || child.content?.toString() }
315
+ })
316
+ break
317
+ case 'invite':
318
+ ev.emit('newsletter.invite', {
319
+ id: from,
320
+ inviteCode: child.attrs.code,
321
+ inviter: child.attrs.jid || author,
322
+ role: child.attrs.role || 'SUBSCRIBER'
323
+ })
324
+ break
325
+ default:
326
+ logger.warn({ node }, 'Unknown newsletter notification')
327
+ break
328
+ }
329
+ }
330
+ const sendMessageAck = async (node, errorCode) => {
331
+ const stanza = (0, stanza_ack_1.buildAckStanza)(node, errorCode, authState.creds.me.id)
332
+ logger.debug({ recv: { tag: node.tag, attrs: node.attrs }, sent: stanza.attrs }, 'sent ack')
333
+ await sendNode(stanza)
334
+ }
335
+
336
+ const offerCall = async (toJid, isVideo = false) => {
337
+ const callId = crypto_1.randomBytes(16).toString('hex').toUpperCase().substring(0, 64)
338
+ const offerContent = []
339
+ offerContent.push({
340
+ tag: 'audio',
341
+ attrs: { enc: 'opus', rate: '16000' },
342
+ content: undefined
343
+ })
344
+ offerContent.push({
345
+ tag: 'audio',
346
+ attrs: { enc: 'opus', rate: '8000' },
347
+ content: undefined
348
+ })
349
+
350
+ if (isVideo) {
351
+ offerContent.push({
352
+ tag: 'video',
353
+ attrs: {
354
+ enc: 'vp8',
355
+ dec: 'vp8',
356
+ orientation: '0',
357
+ screen_width: '1920',
358
+ screen_height: '1080',
359
+ device_orientation: '0'
360
+ },
361
+ content: undefined
362
+ })
363
+ }
364
+ offerContent.push({
365
+ tag: 'net',
366
+ attrs: { medium: '3' },
367
+ content: undefined
368
+ })
369
+ offerContent.push({
370
+ tag: 'capability',
371
+ attrs: { ver: '1' },
372
+ content: new Uint8Array([1, 4, 255, 131, 207, 4])
373
+ })
374
+ offerContent.push({
375
+ tag: 'encopt',
376
+ attrs: { keygen: '2' },
377
+ content: undefined
378
+ })
379
+
380
+ const encKey = crypto_1.randomBytes(32)
381
+ const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) =>
382
+ WABinary_1.jidEncode(user, 's.whatsapp.net', device)
383
+ )
384
+ await assertSessions(devices, true)
385
+
386
+ const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(
387
+ devices,
388
+ {
389
+ call: {
390
+ callKey: new Uint8Array(encKey)
391
+ }
392
+ },
393
+ { count: '0' }
394
+ )
395
+ offerContent.push({ tag: 'destination', attrs: {}, content: destinations })
396
+
397
+ if (shouldIncludeDeviceIdentity) {
398
+ offerContent.push({
399
+ tag: 'device-identity',
400
+ attrs: {},
401
+ content: Utils_1.encodeSignedDeviceIdentity(authState.creds.account, true)
402
+ })
403
+ }
404
+
405
+ const stanza = {
406
+ tag: 'call',
407
+ attrs: {
408
+ id: Utils_1.generateMessageID(),
409
+ to: toJid
410
+ },
411
+ content: [
412
+ {
413
+ tag: 'offer',
414
+ attrs: {
415
+ 'call-id': callId,
416
+ 'call-creator': authState.creds.me.id
417
+ },
418
+ content: offerContent
419
+ }
420
+ ]
421
+ }
422
+
423
+ await query(stanza)
424
+
425
+ return {
426
+ id: callId,
427
+ to: toJid
428
+ }
429
+ }
430
+
431
+ const rejectCall = async (callId, callFrom) => {
432
+ const stanza = {
433
+ tag: 'call',
434
+ attrs: {
435
+ from: authState.creds.me.id,
436
+ to: callFrom
437
+ },
438
+ content: [
439
+ {
440
+ tag: 'reject',
441
+ attrs: {
442
+ 'call-id': callId,
443
+ 'call-creator': callFrom,
444
+ count: '0'
445
+ },
446
+ content: undefined
447
+ }
448
+ ]
449
+ }
450
+ await query(stanza)
451
+ }
452
+
453
+ const acceptCall = async (callId, callFrom) => {
454
+ const stanza = {
455
+ tag: 'call',
456
+ attrs: {
457
+ from: authState.creds.me.id,
458
+ to: callFrom
459
+ },
460
+ content: [
461
+ {
462
+ tag: 'accept',
463
+ attrs: {
464
+ 'call-id': callId,
465
+ 'call-creator': callFrom,
466
+ count: '0'
467
+ },
468
+ content: undefined
469
+ }
470
+ ]
471
+ }
472
+ await query(stanza)
473
+ }
474
+
475
+ const terminateCall = async (callId, callFrom) => {
476
+ const stanza = {
477
+ tag: 'call',
478
+ attrs: {
479
+ from: authState.creds.me.id,
480
+ to: callFrom
481
+ },
482
+ content: [
483
+ {
484
+ tag: 'terminate',
485
+ attrs: {
486
+ 'call-id': callId,
487
+ 'call-creator': callFrom,
488
+ reason: 'user-terminated',
489
+ count: '0'
490
+ },
491
+ content: undefined
492
+ }
493
+ ]
494
+ }
495
+ await query(stanza)
496
+ }
497
+
498
+ /**
499
+ * Re-encrypt call key for a device that reconnected mid-call.
500
+ * Source: OutgoingSignalingHandler.java enc_rekey / rekeyEncryptionTask
501
+ *
502
+ * @param {string} callId - Active call ID
503
+ * @param {string} callFrom - JID of the call creator
504
+ * @param {Buffer} encryptedKeyBytes - Re-encrypted call session key bytes
505
+ * @param {number} [count=0] - Retry counter (0–4)
506
+ */
507
+ const rekeyCall = async (callId, callFrom, encryptedKeyBytes, count = 0) => {
508
+ const stanza = {
509
+ tag: 'call',
510
+ attrs: {
511
+ from: authState.creds.me.id,
512
+ to: callFrom
513
+ },
514
+ content: [
515
+ {
516
+ tag: 'enc_rekey',
517
+ attrs: {
518
+ 'call-id': callId,
519
+ 'call-creator': callFrom,
520
+ count: count.toString()
521
+ },
522
+ content: [
523
+ {
524
+ tag: 'enc',
525
+ attrs: { v: '2', type: 'msg' },
526
+ content: encryptedKeyBytes
527
+ }
528
+ ]
529
+ }
530
+ ]
531
+ }
532
+ await query(stanza)
533
+ }
534
+
535
+ /**
536
+ * Join a call via an invite link.
537
+ * Source: OutgoingSignalingHandler.java link_join tag
538
+ *
539
+ * @param {string} callId - Call ID from the link
540
+ * @param {string} callCreator - JID of the call creator
541
+ * @param {string} linkToken - Token from the call link
542
+ */
543
+ const joinCallLink = async (callId, callCreator, linkToken) => {
544
+ const stanza = {
545
+ tag: 'call',
546
+ attrs: {
547
+ from: authState.creds.me.id,
548
+ to: callCreator
549
+ },
550
+ content: [
551
+ {
552
+ tag: 'link_join',
553
+ attrs: {
554
+ 'call-id': callId,
555
+ 'call-creator': callCreator,
556
+ token: linkToken
557
+ },
558
+ content: undefined
559
+ }
560
+ ]
561
+ }
562
+ await query(stanza)
563
+ }
564
+
565
+ /**
566
+ * Query info about a call link before joining.
567
+ * Source: OutgoingSignalingHandler.java link_query tag
568
+ *
569
+ * @param {string} callLinkCode - The call link code to query
570
+ * @param {string} to - JID to send the query to
571
+ */
572
+ const queryCallLink = async (callLinkCode, to) => {
573
+ const stanza = {
574
+ tag: 'call',
575
+ attrs: {
576
+ from: authState.creds.me.id,
577
+ to
578
+ },
579
+ content: [
580
+ {
581
+ tag: 'link_query',
582
+ attrs: { code: callLinkCode },
583
+ content: undefined
584
+ }
585
+ ]
586
+ }
587
+ return query(stanza)
588
+ }
589
+ const sendRetryRequest = async (node, forceIncludeKeys = false) => {
590
+ const { fullMessage } = (0, Utils_1.decodeMessageNode)(node, authState.creds.me.id, authState.creds.me.lid || '')
591
+ const { key: msgKey } = fullMessage
592
+ const msgId = msgKey.id
593
+ if (messageRetryManager) {
594
+ // Check if we've exceeded max retries using the new system
595
+ if (messageRetryManager.hasExceededMaxRetries(msgId)) {
596
+ logger.debug({ msgId }, 'reached retry limit with new retry manager, clearing')
597
+ messageRetryManager.markRetryFailed(msgId)
598
+ return
599
+ }
600
+ // Increment retry count using new system
601
+ const retryCount = messageRetryManager.incrementRetryCount(msgId)
602
+ // Use the new retry count for the rest of the logic
603
+ const key = `${msgId}:${msgKey?.participant}`
604
+ await msgRetryCache.set(key, retryCount)
605
+ } else {
606
+ // Fallback to old system
607
+ const key = `${msgId}:${msgKey?.participant}`
608
+ let retryCount = (await msgRetryCache.get(key)) || 0
609
+ if (retryCount >= maxMsgRetryCount) {
610
+ logger.debug({ retryCount, msgId }, 'reached retry limit, clearing')
611
+ await msgRetryCache.del(key)
612
+ return
613
+ }
614
+ retryCount += 1
615
+ await msgRetryCache.set(key, retryCount)
616
+ }
617
+ const key = `${msgId}:${msgKey?.participant}`
618
+ const retryCount = (await msgRetryCache.get(key)) || 1
619
+ const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
620
+ const fromJid = node.attrs.from
621
+ // Check if we should recreate the session
622
+ let shouldRecreateSession = false
623
+ let recreateReason = ''
624
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
625
+ try {
626
+ // Check if we have a session with this JID
627
+ const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
628
+ const hasSession = await signalRepository.validateSession(fromJid)
629
+ const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists)
630
+ shouldRecreateSession = result.recreate
631
+ recreateReason = result.reason
632
+ if (shouldRecreateSession) {
633
+ logger.debug({ fromJid, retryCount, reason: recreateReason }, 'recreating session for retry')
634
+ // Delete existing session to force recreation
635
+ await authState.keys.set({ session: { [sessionId]: null } })
636
+ forceIncludeKeys = true
637
+ }
638
+ } catch (error) {
639
+ logger.warn({ error, fromJid }, 'failed to check session recreation')
640
+ }
641
+ }
642
+ if (retryCount <= 2) {
643
+ // Use new retry manager for phone requests if available
644
+ if (messageRetryManager) {
645
+ // Schedule phone request with delay (like whatsmeow)
646
+ messageRetryManager.schedulePhoneRequest(msgId, async () => {
647
+ try {
648
+ const requestId = await requestPlaceholderResend(msgKey)
649
+ logger.debug(
650
+ `sendRetryRequest: requested placeholder resend (${requestId}) for message ${msgId} (scheduled)`
651
+ )
652
+ } catch (error) {
653
+ logger.warn({ error, msgId }, 'failed to send scheduled phone request')
654
+ }
655
+ })
656
+ } else {
657
+ // Fallback to immediate request
658
+ const msgId = await requestPlaceholderResend(msgKey)
659
+ logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`)
660
+ }
661
+ }
662
+ const deviceIdentity = (0, Utils_1.encodeSignedDeviceIdentity)(account, true)
663
+ await authState.keys.transaction(async () => {
664
+ const receipt = {
665
+ tag: 'receipt',
666
+ attrs: {
667
+ id: msgId,
668
+ type: 'retry',
669
+ to: node.attrs.from
670
+ },
671
+ content: [
672
+ {
673
+ tag: 'retry',
674
+ attrs: {
675
+ count: retryCount.toString(),
676
+ id: node.attrs.id,
677
+ t: node.attrs.t,
678
+ v: '1',
679
+ // ADD ERROR FIELD
680
+ error: '0'
681
+ }
682
+ },
683
+ {
684
+ tag: 'registration',
685
+ attrs: {},
686
+ content: (0, Utils_1.encodeBigEndian)(authState.creds.registrationId)
687
+ }
688
+ ]
689
+ }
690
+ if (node.attrs.recipient) {
691
+ receipt.attrs.recipient = node.attrs.recipient
692
+ }
693
+ if (node.attrs.participant) {
694
+ receipt.attrs.participant = node.attrs.participant
695
+ }
696
+ if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
697
+ const { update, preKeys } = await (0, Utils_1.getNextPreKeys)(authState, 1)
698
+ const [keyId] = Object.keys(preKeys)
699
+ const key = preKeys[+keyId]
700
+ const content = receipt.content
701
+ content.push({
702
+ tag: 'keys',
703
+ attrs: {},
704
+ content: [
705
+ { tag: 'type', attrs: {}, content: Buffer.from(Defaults_1.KEY_BUNDLE_TYPE) },
706
+ { tag: 'identity', attrs: {}, content: identityKey.public },
707
+ (0, Utils_1.xmppPreKey)(key, +keyId),
708
+ (0, Utils_1.xmppSignedPreKey)(signedPreKey),
709
+ { tag: 'device-identity', attrs: {}, content: deviceIdentity }
710
+ ]
711
+ })
712
+ ev.emit('creds.update', update)
713
+ }
714
+ await sendNode(receipt)
715
+ logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt')
716
+ }, authState?.creds?.me?.id || 'sendRetryRequest')
717
+ }
718
+ /**
719
+ * Fire-and-forget tctoken re-issuance after a peer's device identity changed.
720
+ * Runs in parallel with the session refresh (not after it).
721
+ */
722
+ const reissueTcTokenAfterIdentityChange = from => {
723
+ void (async () => {
724
+ const normalizedJid = (0, WABinary_1.jidNormalizedUser)(from)
725
+ const tcJid = await (0, tc_token_utils_1.resolveTcTokenJid)(normalizedJid, getLIDForPN)
726
+ const tcTokenData = await authState.keys.get('tctoken', [tcJid])
727
+ const senderTs = tcTokenData?.[tcJid]?.senderTimestamp
728
+ if (senderTs === null || senderTs === undefined || (0, tc_token_utils_1.isTcTokenExpired)(senderTs)) {
729
+ return
730
+ }
731
+ logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken')
732
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
733
+ const issueJid = await (0, tc_token_utils_1.resolveIssuanceJid)(
734
+ normalizedJid,
735
+ sock.serverProps.lidTrustedTokenIssueToLid,
736
+ getLIDForPN,
737
+ getPNForLID
738
+ )
739
+ const result = await issuePrivacyTokens([issueJid], senderTs)
740
+ await (0, tc_token_utils_1.storeTcTokensFromIqResult)({
741
+ result,
742
+ fallbackJid: tcJid,
743
+ keys: authState.keys,
744
+ getLIDForPN,
745
+ onNewJidStored: trackTcTokenJid
746
+ })
747
+ })().catch(err => {
748
+ logger.debug({ jid: from, err: err?.message }, 'failed to re-issue tctoken after identity change')
749
+ })
750
+ }
751
+ const handleEncryptNotification = async node => {
752
+ const from = node.attrs.from
753
+ if (from === WABinary_1.S_WHATSAPP_NET) {
754
+ const countChild = (0, WABinary_1.getBinaryNodeChild)(node, 'count')
755
+ const count = +countChild.attrs.value
756
+ const shouldUploadMorePreKeys = count < Defaults_1.MIN_PREKEY_COUNT
757
+ logger.debug({ count, shouldUploadMorePreKeys }, 'recv pre-key count')
758
+ if (shouldUploadMorePreKeys) {
759
+ await uploadPreKeys()
760
+ }
761
+ } else {
762
+ const result = await (0, Utils_1.handleIdentityChange)(node, {
763
+ meId: authState.creds.me?.id,
764
+ meLid: authState.creds.me?.lid,
765
+ validateSession: signalRepository.validateSession,
766
+ assertSessions,
767
+ debounceCache: identityAssertDebounce,
768
+ logger,
769
+ onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange
770
+ })
771
+ if (result.action === 'no_identity_node') {
772
+ logger.info({ node }, 'unknown encrypt notification')
773
+ }
774
+ }
775
+ }
776
+ const handleGroupNotification = (fullNode, child, msg) => {
777
+ // TODO: Support PN/LID (Here is only LID now)
778
+ const actingParticipantLid = fullNode.attrs.participant
779
+ const actingParticipantPn = fullNode.attrs.participant_pn
780
+ const actingParticipantUsername = fullNode.attrs.participant_username
781
+ const affectedParticipantLid =
782
+ (0, WABinary_1.getBinaryNodeChild)(child, 'participant')?.attrs?.jid || actingParticipantLid
783
+ const affectedParticipantPn =
784
+ (0, WABinary_1.getBinaryNodeChild)(child, 'participant')?.attrs?.phone_number || actingParticipantPn
785
+ switch (child?.tag) {
786
+ case 'create':
787
+ const metadata = (0, groups_1.extractGroupMetadata)(child)
788
+ msg.messageStubType = Types_1.WAMessageStubType.GROUP_CREATE
789
+ msg.messageStubParameters = [metadata.subject]
790
+ msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn }
791
+ ev.emit('chats.upsert', [
792
+ {
793
+ id: metadata.id,
794
+ name: metadata.subject,
795
+ conversationTimestamp: metadata.creation
796
+ }
797
+ ])
798
+ ev.emit('groups.upsert', [
799
+ {
800
+ ...metadata,
801
+ author: actingParticipantLid,
802
+ authorPn: actingParticipantPn,
803
+ authorUsername: actingParticipantUsername
804
+ }
805
+ ])
806
+ break
807
+ case 'ephemeral':
808
+ case 'not_ephemeral':
809
+ msg.message = {
810
+ protocolMessage: {
811
+ type: index_js_1.proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
812
+ ephemeralExpiration: +(child.attrs.expiration || 0)
813
+ }
814
+ }
815
+ break
816
+ case 'modify':
817
+ const oldNumber = (0, WABinary_1.getBinaryNodeChildren)(child, 'participant').map(p => p.attrs.jid)
818
+ msg.messageStubParameters = oldNumber || []
819
+ msg.messageStubType = Types_1.WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER
820
+ break
821
+ case 'promote':
822
+ case 'demote':
823
+ case 'remove':
824
+ case 'add':
825
+ case 'leave':
826
+ const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`
827
+ msg.messageStubType = Types_1.WAMessageStubType[stubType]
828
+ const participants = (0, WABinary_1.getBinaryNodeChildren)(child, 'participant').map(({ attrs }) => {
829
+ // TODO: Store LID MAPPINGS
830
+ return {
831
+ id: attrs.jid,
832
+ phoneNumber:
833
+ (0, WABinary_1.isLidUser)(attrs.jid) && (0, WABinary_1.isPnUser)(attrs.phone_number)
834
+ ? attrs.phone_number
835
+ : undefined,
836
+ lid: (0, WABinary_1.isPnUser)(attrs.jid) && (0, WABinary_1.isLidUser)(attrs.lid) ? attrs.lid : undefined,
837
+ username: attrs.participant_username || attrs.username || undefined,
838
+ admin: attrs.type || null
839
+ }
840
+ })
841
+ if (
842
+ participants.length === 1 &&
843
+ // if recv. "remove" message and sender removed themselves
844
+ // mark as left
845
+ ((0, WABinary_1.areJidsSameUser)(participants[0].id, actingParticipantLid) ||
846
+ (0, WABinary_1.areJidsSameUser)(participants[0].id, actingParticipantPn)) &&
847
+ child.tag === 'remove'
848
+ ) {
849
+ msg.messageStubType = Types_1.WAMessageStubType.GROUP_PARTICIPANT_LEAVE
850
+ }
851
+ msg.messageStubParameters = participants.map(a => JSON.stringify(a))
852
+ break
853
+ case 'subject':
854
+ msg.messageStubType = Types_1.WAMessageStubType.GROUP_CHANGE_SUBJECT
855
+ msg.messageStubParameters = [child.attrs.subject]
856
+ break
857
+ case 'description':
858
+ const description = (0, WABinary_1.getBinaryNodeChild)(child, 'body')?.content?.toString()
859
+ msg.messageStubType = Types_1.WAMessageStubType.GROUP_CHANGE_DESCRIPTION
860
+ msg.messageStubParameters = description ? [description] : undefined
861
+ break
862
+ case 'announcement':
863
+ case 'not_announcement':
864
+ msg.messageStubType = Types_1.WAMessageStubType.GROUP_CHANGE_ANNOUNCE
865
+ msg.messageStubParameters = [child.tag === 'announcement' ? 'on' : 'off']
866
+ break
867
+ case 'locked':
868
+ case 'unlocked':
869
+ msg.messageStubType = Types_1.WAMessageStubType.GROUP_CHANGE_RESTRICT
870
+ msg.messageStubParameters = [child.tag === 'locked' ? 'on' : 'off']
871
+ break
872
+ case 'invite':
873
+ msg.messageStubType = Types_1.WAMessageStubType.GROUP_CHANGE_INVITE_LINK
874
+ msg.messageStubParameters = [child.attrs.code]
875
+ break
876
+ case 'member_add_mode':
877
+ const addMode = child.content
878
+ if (addMode) {
879
+ msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBER_ADD_MODE
880
+ msg.messageStubParameters = [addMode.toString()]
881
+ }
882
+ break
883
+ case 'membership_approval_mode':
884
+ const approvalMode = (0, WABinary_1.getBinaryNodeChild)(child, 'group_join')
885
+ if (approvalMode) {
886
+ msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE
887
+ msg.messageStubParameters = [approvalMode.attrs.state]
888
+ }
889
+ break
890
+ case 'created_membership_requests':
891
+ msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
892
+ msg.messageStubParameters = [
893
+ JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
894
+ 'created',
895
+ child.attrs.request_method
896
+ ]
897
+ break
898
+ case 'revoked_membership_requests':
899
+ const isDenied = (0, WABinary_1.areJidsSameUser)(affectedParticipantLid, actingParticipantLid)
900
+ // TODO: LIDMAPPING SUPPORT
901
+ msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
902
+ msg.messageStubParameters = [
903
+ JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
904
+ isDenied ? 'revoked' : 'rejected'
905
+ ]
906
+ break
907
+ }
908
+ }
909
+ const normalizeNotificationParticipant = async (jid, groupData) => {
910
+ if (!jid || typeof jid !== 'string') {
911
+ return jid
912
+ }
913
+ if (!(0, WABinary_1.isLidUser)(jid) && !(0, WABinary_1.isHostedLidUser)(jid)) {
914
+ return jid
915
+ }
916
+ const normalized = await (0, jid_display_normalization_1.normalizeMentionedJidsForSend)(
917
+ [jid],
918
+ groupData,
919
+ signalRepository,
920
+ logger
921
+ )
922
+ return normalized?.[0] || jid
923
+ }
924
+ const normalizeNotificationParticipantsArray = async (participants, groupData) => {
925
+ if (!Array.isArray(participants)) {
926
+ return participants
927
+ }
928
+ return Promise.all(participants.map(jid => normalizeNotificationParticipant(jid, groupData)))
929
+ }
930
+ const getNotificationGroupData = async node => {
931
+ const groupJid = (0, WABinary_1.jidNormalizedUser)(node?.attrs?.from)
932
+ if (!(0, WABinary_1.isJidGroup)(groupJid)) {
933
+ return undefined
934
+ }
935
+ try {
936
+ return (
937
+ (config.useCachedGroupMetadata && config.cachedGroupMetadata
938
+ ? await config.cachedGroupMetadata(groupJid)
939
+ : undefined) || (await sock.groupMetadata(groupJid))
940
+ )
941
+ } catch (error) {
942
+ logger.debug({ error, groupJid }, 'failed to fetch group metadata for notification normalization')
943
+ return undefined
944
+ }
945
+ }
946
+ const normalizeNotificationStubParameters = async (stubParameters, groupData) => {
947
+ if (!Array.isArray(stubParameters)) {
948
+ return stubParameters
949
+ }
950
+ const normalized = []
951
+ for (const entry of stubParameters) {
952
+ if (typeof entry !== 'string') {
953
+ normalized.push(entry)
954
+ continue
955
+ }
956
+ if ((0, WABinary_1.isLidUser)(entry) || (0, WABinary_1.isHostedLidUser)(entry)) {
957
+ normalized.push(await normalizeNotificationParticipant(entry, groupData))
958
+ continue
959
+ }
960
+ if (entry.startsWith('{') && entry.includes('"id"')) {
961
+ try {
962
+ const parsed = JSON.parse(entry)
963
+ const explicitPn =
964
+ typeof parsed?.phoneNumber === 'string'
965
+ ? parsed.phoneNumber
966
+ : typeof parsed?.pn === 'string'
967
+ ? parsed.pn
968
+ : undefined
969
+ if ((0, WABinary_1.isPnUser)(explicitPn) || (0, WABinary_1.isHostedPnUser)(explicitPn)) {
970
+ parsed.id = explicitPn
971
+ parsed.pn = explicitPn
972
+ normalized.push(JSON.stringify(parsed))
973
+ continue
974
+ }
975
+ if (parsed?.id) {
976
+ parsed.id = await normalizeNotificationParticipant(parsed.id, groupData)
977
+ }
978
+ if (parsed?.lid && !parsed?.pn) {
979
+ parsed.pn = await normalizeNotificationParticipant(parsed.lid, groupData)
980
+ }
981
+ normalized.push(JSON.stringify(parsed))
982
+ continue
983
+ } catch (err) {
984
+ logger.debug({ err, entry }, 'failed to normalize stub parameter JSON')
985
+ }
986
+ }
987
+ normalized.push(entry)
988
+ }
989
+ return normalized
990
+ }
991
+ const normalizeCallEventJids = async (call, infoChild) => {
992
+ if (!call) {
993
+ return call
994
+ }
995
+ const callContextGroupJid = call.groupJid || ((0, WABinary_1.isJidGroup)(call.chatId) ? call.chatId : undefined)
996
+ let groupData
997
+ if (callContextGroupJid) {
998
+ try {
999
+ groupData =
1000
+ (config.useCachedGroupMetadata && config.cachedGroupMetadata
1001
+ ? await config.cachedGroupMetadata(callContextGroupJid)
1002
+ : undefined) || (await sock.groupMetadata(callContextGroupJid))
1003
+ } catch (error) {
1004
+ logger.debug({ error, groupJid: callContextGroupJid }, 'failed to fetch group metadata for call normalization')
1005
+ }
1006
+ }
1007
+ if (call.chatId && !call.isGroup) {
1008
+ call.chatId = await normalizeNotificationParticipant(call.chatId, groupData)
1009
+ }
1010
+ if (call.from) {
1011
+ call.from = await normalizeNotificationParticipant(call.from, groupData)
1012
+ }
1013
+ if (call.groupJid) {
1014
+ call.groupJid = await normalizeNotificationParticipant(call.groupJid, groupData)
1015
+ }
1016
+ if (!call.callerPn && infoChild?.attrs?.caller_lid) {
1017
+ call.callerPn = await normalizeNotificationParticipant(infoChild.attrs.caller_lid, groupData)
1018
+ }
1019
+ if (!call.callerPn && call.from) {
1020
+ call.callerPn = call.from
1021
+ }
1022
+ return call
1023
+ }
1024
+ const normalizeNotificationResult = async (node, result, groupData) => {
1025
+ const groupJid = (0, WABinary_1.jidNormalizedUser)(node?.attrs?.from)
1026
+ if (!(0, WABinary_1.isJidGroup)(groupJid)) {
1027
+ return
1028
+ }
1029
+ if (result?.key?.participant) {
1030
+ result.key.participant = await normalizeNotificationParticipant(result.key.participant, groupData)
1031
+ }
1032
+ if (result?.participant) {
1033
+ result.participant = await normalizeNotificationParticipant(result.participant, groupData)
1034
+ }
1035
+ if (Array.isArray(result?.messageStubParameters)) {
1036
+ result.messageStubParameters = await normalizeNotificationStubParameters(result.messageStubParameters, groupData)
1037
+ }
1038
+ }
1039
+
1040
+ /**
1041
+ * Handle incoming interop notifications (type="interop").
1042
+ *
1043
+ * The APK emits these for:
1044
+ * - stella_interop_enabled / stella_ios_enabled → feature-flag toggles
1045
+ * - ig_professional / ig_handle / followers → Instagram profile data updates
1046
+ * - fbid:thread / fbid:devices → Meta thread/device association
1047
+ * - peer_device_presence → interop contact online/offline
1048
+ * - group membership changes in interop groups → add/remove/promote events
1049
+ */
1050
+ const handleInteropNotification = (node, child) => {
1051
+ const childTag = child?.tag
1052
+ const attrs = child?.attrs || {}
1053
+
1054
+ // Feature flag: server toggled stella_interop_enabled or stella_ios_enabled
1055
+ if (childTag === 'feature') {
1056
+ const feature = attrs.name
1057
+ const enabled = attrs.value === 'true' || attrs.value === '1'
1058
+ logger.info({ feature, enabled }, '[interop] feature flag update')
1059
+ ev.emit('interop.feature-update', { feature, enabled })
1060
+ return
1061
+ }
1062
+
1063
+ // Instagram profile data pushed for an interop contact
1064
+ if (childTag === 'ig_profile') {
1065
+ const contactUpdate = {
1066
+ id: (0, WABinary_1.jidNormalizedUser)(node.attrs.from),
1067
+ ...(attrs.ig_handle ? { igHandle: attrs.ig_handle } : {}),
1068
+ ...(attrs.ig_professional !== undefined ? { igProfessional: attrs.ig_professional === 'true' } : {}),
1069
+ ...(attrs.followers !== undefined ? { igFollowers: parseInt(attrs.followers, 10) } : {})
1070
+ }
1071
+ logger.debug({ contactUpdate }, '[interop] ig_profile update')
1072
+ ev.emit('contacts.update', [contactUpdate])
1073
+ return
1074
+ }
1075
+
1076
+ // peer_device_presence — interop contact came online or went offline
1077
+ if (childTag === 'peer_device_presence') {
1078
+ const jid = attrs.jid || (0, WABinary_1.jidNormalizedUser)(node.attrs.from)
1079
+ const presence = attrs.type === 'unavailable' ? 'unavailable' : 'available'
1080
+ logger.debug({ jid, presence }, '[interop] peer_device_presence')
1081
+ ev.emit('presence.update', { id: jid, presences: { [jid]: { lastKnownPresence: presence } } })
1082
+ return
1083
+ }
1084
+
1085
+ // fbid:thread / fbid:devices — Meta thread or device list association
1086
+ if (childTag === 'fbid_thread' || childTag === 'fbid_devices') {
1087
+ logger.debug({ childTag, attrs, from: node.attrs.from }, '[interop] fbid association update')
1088
+ ev.emit('interop.fbid-update', { type: childTag, jid: node.attrs.from, attrs })
1089
+ return
1090
+ }
1091
+
1092
+ // Interop group membership changes (add / remove / promote / demote)
1093
+ if (childTag === 'participants') {
1094
+ const groupJid = node.attrs.from
1095
+ const action = attrs.type // 'add' | 'remove' | 'promote' | 'demote'
1096
+ const participants = (0, WABinary_1.getBinaryNodeChildren)(child, 'participant').map(p => p.attrs.jid)
1097
+ logger.info({ groupJid, action, participants }, '[interop] group participants update')
1098
+ ev.emit('group-participants.update', { id: groupJid, participants, action })
1099
+ return
1100
+ }
1101
+
1102
+ logger.debug({ childTag, from: node.attrs.from }, '[interop] unhandled interop notification subtype')
1103
+ }
1104
+
1105
+ const processNotification = async node => {
1106
+ const result = {}
1107
+ const [child] = (0, WABinary_1.getAllBinaryNodeChildren)(node)
1108
+ const nodeType = node.attrs.type
1109
+ const from = (0, WABinary_1.jidNormalizedUser)(node.attrs.from)
1110
+ switch (nodeType) {
1111
+ case 'newsletter':
1112
+ await handleNewsletterNotification(node)
1113
+ break
1114
+ case 'mex':
1115
+ await handleMexNewsletterNotification(node)
1116
+ break
1117
+ case 'w:gp2':
1118
+ // TODO: HANDLE PARTICIPANT_PN
1119
+ const groupData = await getNotificationGroupData(node)
1120
+ handleGroupNotification(node, child, result)
1121
+
1122
+ await normalizeNotificationResult(node, result, groupData)
1123
+ break
1124
+ case 'mediaretry':
1125
+ const event = (0, Utils_1.decodeMediaRetryNode)(node)
1126
+ ev.emit('messages.media-update', [event])
1127
+ break
1128
+ case 'encrypt':
1129
+ await handleEncryptNotification(node)
1130
+ break
1131
+ case 'devices':
1132
+ const devices = (0, WABinary_1.getBinaryNodeChildren)(child, 'device')
1133
+ const deviceOwnerJid = child.attrs.jid || child.attrs.lid
1134
+ const deviceData = devices.map(d => ({
1135
+ id: d.attrs.jid,
1136
+ lid: d.attrs.lid,
1137
+ keyIndex: d.attrs.key_index ? +d.attrs.key_index : undefined,
1138
+ platform: d.attrs.platform || undefined,
1139
+ isCompanion: d.attrs.companion === 'true' || undefined
1140
+ }))
1141
+ if (
1142
+ (0, WABinary_1.areJidsSameUser)(child.attrs.jid, authState.creds.me.id) ||
1143
+ (0, WABinary_1.areJidsSameUser)(child.attrs.lid, authState.creds.me.lid)
1144
+ ) {
1145
+ logger.info({ deviceData }, 'my own devices changed')
1146
+ ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf: true })
1147
+ } else if (deviceOwnerJid) {
1148
+ ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf: false })
1149
+ }
1150
+ break
1151
+ case 'server_sync':
1152
+ const update = (0, WABinary_1.getBinaryNodeChild)(node, 'collection')
1153
+ if (update) {
1154
+ const name = update.attrs.name
1155
+ await resyncAppState([name], false)
1156
+ }
1157
+ break
1158
+ case 'picture':
1159
+ const setPicture = (0, WABinary_1.getBinaryNodeChild)(node, 'set')
1160
+ const delPicture = (0, WABinary_1.getBinaryNodeChild)(node, 'delete')
1161
+ // TODO: WAJIDHASH stuff proper support inhouse
1162
+ ev.emit('contacts.update', [
1163
+ {
1164
+ id: (0, WABinary_1.jidNormalizedUser)(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
1165
+ imgUrl: setPicture ? 'changed' : 'removed'
1166
+ }
1167
+ ])
1168
+ if ((0, WABinary_1.isJidGroup)(from)) {
1169
+ const node = setPicture || delPicture
1170
+ result.messageStubType = Types_1.WAMessageStubType.GROUP_CHANGE_ICON
1171
+ if (setPicture) {
1172
+ result.messageStubParameters = [setPicture.attrs.id]
1173
+ }
1174
+ result.participant = node?.attrs.author
1175
+ result.key = {
1176
+ ...(result.key || {}),
1177
+ participant: setPicture?.attrs.author
1178
+ }
1179
+ }
1180
+ break
1181
+ case 'account_sync':
1182
+ if (child.tag === 'disappearing_mode') {
1183
+ const newDuration = +child.attrs.duration
1184
+ const timestamp = +child.attrs.t
1185
+ logger.info({ newDuration }, 'updated account disappearing mode')
1186
+ ev.emit('creds.update', {
1187
+ accountSettings: {
1188
+ ...authState.creds.accountSettings,
1189
+ defaultDisappearingMode: {
1190
+ ephemeralExpiration: newDuration,
1191
+ ephemeralSettingTimestamp: timestamp
1192
+ }
1193
+ }
1194
+ })
1195
+ } else if (child.tag === 'blocklist') {
1196
+ const blocklists = (0, WABinary_1.getBinaryNodeChildren)(child, 'item')
1197
+ for (const { attrs } of blocklists) {
1198
+ const blocklist = [attrs.jid]
1199
+ const type = attrs.action === 'block' ? 'add' : 'remove'
1200
+ ev.emit('blocklist.update', { blocklist, type })
1201
+ }
1202
+ }
1203
+ break
1204
+ case 'business':
1205
+ // SMB privacy / data-sharing settings sync push
1206
+ // (WhatsApp Web: WASmaxInBizSettingsSyncPrivacySettingRequest)
1207
+ if (child?.tag === 'privacy') {
1208
+ ev.emit('business.privacy-settings-sync', {
1209
+ jid: from,
1210
+ categories: (0, WABinary_1.getBinaryNodeChildren)(child, 'category').map(c => ({
1211
+ name: c.attrs.name,
1212
+ value: c.attrs.value
1213
+ })),
1214
+ attrs: child.attrs
1215
+ })
1216
+ }
1217
+ break
1218
+ case 'hosted':
1219
+ // Coexistence (WhatsApp <-> Messenger/Instagram) onboarding/offboarding push
1220
+ // (WhatsApp Web: WASmaxInCoexistenceOnboarding/OffboardingNotification)
1221
+ if (child?.tag === 'onboarding_status') {
1222
+ ev.emit('coexistence.update', {
1223
+ jid: from,
1224
+ kind: 'onboarding',
1225
+ status: child.attrs.status,
1226
+ productSurface: child.attrs['product_surface']
1227
+ })
1228
+ } else if (child?.tag === 'offboarding') {
1229
+ ev.emit('coexistence.update', {
1230
+ jid: from,
1231
+ kind: 'offboarding',
1232
+ productSurface: child.attrs['product_surface']
1233
+ })
1234
+ }
1235
+ break
1236
+ case 'link_code_companion_reg':
1237
+ const linkCodeCompanionReg = (0, WABinary_1.getBinaryNodeChild)(node, 'link_code_companion_reg')
1238
+ const ref = toRequiredBuffer(
1239
+ (0, WABinary_1.getBinaryNodeChildBuffer)(linkCodeCompanionReg, 'link_code_pairing_ref')
1240
+ )
1241
+ const primaryIdentityPublicKey = toRequiredBuffer(
1242
+ (0, WABinary_1.getBinaryNodeChildBuffer)(linkCodeCompanionReg, 'primary_identity_pub')
1243
+ )
1244
+ const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(
1245
+ (0, WABinary_1.getBinaryNodeChildBuffer)(
1246
+ linkCodeCompanionReg,
1247
+ 'link_code_pairing_wrapped_primary_ephemeral_pub'
1248
+ )
1249
+ )
1250
+ const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped)
1251
+ const companionSharedKey = Utils_1.Curve.sharedKey(
1252
+ authState.creds.pairingEphemeralKeyPair.private,
1253
+ codePairingPublicKey
1254
+ )
1255
+ const random = (0, crypto_1.randomBytes)(32)
1256
+ const linkCodeSalt = (0, crypto_1.randomBytes)(32)
1257
+ const linkCodePairingExpanded = (0, Utils_1.hkdf)(companionSharedKey, 32, {
1258
+ salt: linkCodeSalt,
1259
+ info: 'link_code_pairing_key_bundle_encryption_key'
1260
+ })
1261
+ const encryptPayload = Buffer.concat([
1262
+ Buffer.from(authState.creds.signedIdentityKey.public),
1263
+ primaryIdentityPublicKey,
1264
+ random
1265
+ ])
1266
+ const encryptIv = (0, crypto_1.randomBytes)(12)
1267
+ const encrypted = (0, Utils_1.aesEncryptGCM)(
1268
+ encryptPayload,
1269
+ linkCodePairingExpanded,
1270
+ encryptIv,
1271
+ Buffer.alloc(0)
1272
+ )
1273
+ const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted])
1274
+ const identitySharedKey = Utils_1.Curve.sharedKey(
1275
+ authState.creds.signedIdentityKey.private,
1276
+ primaryIdentityPublicKey
1277
+ )
1278
+ const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random])
1279
+ authState.creds.advSecretKey = Buffer.from(
1280
+ (0, Utils_1.hkdf)(identityPayload, 32, { info: 'adv_secret' })
1281
+ ).toString('base64')
1282
+ await query({
1283
+ tag: 'iq',
1284
+ attrs: {
1285
+ to: WABinary_1.S_WHATSAPP_NET,
1286
+ type: 'set',
1287
+ id: sock.generateMessageTag(),
1288
+ xmlns: 'md'
1289
+ },
1290
+ content: [
1291
+ {
1292
+ tag: 'link_code_companion_reg',
1293
+ attrs: {
1294
+ jid: authState.creds.me.id,
1295
+ stage: 'companion_finish'
1296
+ },
1297
+ content: [
1298
+ {
1299
+ tag: 'link_code_pairing_wrapped_key_bundle',
1300
+ attrs: {},
1301
+ content: encryptedPayload
1302
+ },
1303
+ {
1304
+ tag: 'companion_identity_public',
1305
+ attrs: {},
1306
+ content: authState.creds.signedIdentityKey.public
1307
+ },
1308
+ {
1309
+ tag: 'link_code_pairing_ref',
1310
+ attrs: {},
1311
+ content: ref
1312
+ }
1313
+ ]
1314
+ }
1315
+ ]
1316
+ })
1317
+ authState.creds.registered = true
1318
+ ev.emit('creds.update', authState.creds)
1319
+ break
1320
+ case 'privacy_token':
1321
+ await handlePrivacyTokenNotification(node)
1322
+ break
1323
+ case 'security':
1324
+ // Security notifications (compromised session, location change alerts)
1325
+ const securityType = child?.tag || node.attrs.type
1326
+ const securityData = {
1327
+ type: securityType,
1328
+ jid: from,
1329
+ timestamp: node.attrs.t ? +node.attrs.t : Math.floor(Date.now() / 1000),
1330
+ details: child?.attrs || {}
1331
+ }
1332
+ logger.warn({ securityData }, 'received security notification')
1333
+ ev.emit('security.alert', securityData)
1334
+ break
1335
+ case 'identity':
1336
+ // Identity change notifications — peer changed their identity key
1337
+ const identityJid = node.attrs.from
1338
+ const identityNewKey = child?.content ? Buffer.from(child.content) : undefined
1339
+ ev.emit('identity.update', {
1340
+ jid: (0, WABinary_1.jidNormalizedUser)(identityJid),
1341
+ newIdentityKey: identityNewKey,
1342
+ timestamp: node.attrs.t ? +node.attrs.t : Math.floor(Date.now() / 1000)
1343
+ })
1344
+ break
1345
+ case 'server':
1346
+ // Server-issued notifications (config changes, client config refresh)
1347
+ const serverTag = child?.tag
1348
+ if (serverTag === 'config') {
1349
+ const configData = {}
1350
+ for (const attr of Object.keys(child?.attrs || {})) {
1351
+ configData[attr] = child.attrs[attr]
1352
+ }
1353
+ ev.emit('server.config', configData)
1354
+ } else if (serverTag === 'app_state_key') {
1355
+ // Server pushed a new app-state key — trigger resync
1356
+ logger.info('server pushed app state key update')
1357
+ await resyncAppState(['critical_block', 'regular_high', 'regular_low'], false)
1358
+ } else {
1359
+ logger.debug({ node }, 'unhandled server notification')
1360
+ }
1361
+ break
1362
+ case 'status':
1363
+ // Contact status (about) change notification
1364
+ const statusOwner = node.attrs.from
1365
+ const statusText = child?.content ? child.content.toString() : undefined
1366
+ if (statusOwner && statusText !== undefined) {
1367
+ ev.emit('contacts.update', [
1368
+ {
1369
+ id: (0, WABinary_1.jidNormalizedUser)(statusOwner),
1370
+ status: statusText
1371
+ }
1372
+ ])
1373
+ }
1374
+ break
1375
+ case 'usync':
1376
+ // USync result push from server
1377
+ const usyncResults = (0, WABinary_1.getBinaryNodeChildren)(child || node, 'user')
1378
+ if (usyncResults.length) {
1379
+ const updates = usyncResults
1380
+ .map(u => ({
1381
+ id: (0, WABinary_1.jidNormalizedUser)(u.attrs.jid),
1382
+ ...(u.attrs.lid ? { lid: u.attrs.lid } : {}),
1383
+ ...(u.attrs.username ? { username: u.attrs.username } : {}),
1384
+ ...(u.attrs.status ? { status: u.attrs.status } : {})
1385
+ }))
1386
+ .filter(u => u.id)
1387
+ if (updates.length) {
1388
+ ev.emit('contacts.update', updates)
1389
+ }
1390
+ }
1391
+ break
1392
+ case 'interop':
1393
+ // Interop-related server notifications — covers:
1394
+ // stella_interop_enabled / stella_ios_enabled feature flags
1395
+ // ig_professional / ig_handle / followers (Instagram account data)
1396
+ // fbid:thread / fbid:devices (Meta thread/device references)
1397
+ // peer_device_presence updates
1398
+ // group membership changes in interop groups
1399
+ handleInteropNotification(node, child)
1400
+ break
1401
+ }
1402
+ if (Object.keys(result).length) {
1403
+ return result
1404
+ }
1405
+ }
1406
+ /**
1407
+ * In-memory cache of storage JIDs with stored tctokens, seeded from the persisted index.
1408
+ * Used to coalesce writes during a session; pruning always re-reads the persisted index.
1409
+ */
1410
+ const tcTokenKnownJids = new Set()
1411
+ const tcTokenIndexLoaded = (async () => {
1412
+ try {
1413
+ const jids = await (0, tc_token_utils_1.readTcTokenIndex)(authState.keys)
1414
+ for (const jid of jids) tcTokenKnownJids.add(jid)
1415
+ logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index')
1416
+ } catch (err) {
1417
+ logger.warn({ err: err?.message }, 'failed to load tctoken index')
1418
+ }
1419
+ })()
1420
+ let tcTokenIndexTimer
1421
+ async function flushTcTokenIndex() {
1422
+ if (tcTokenIndexTimer) {
1423
+ clearTimeout(tcTokenIndexTimer)
1424
+ tcTokenIndexTimer = undefined
1425
+ }
1426
+ const write = await (0, tc_token_utils_1.buildMergedTcTokenIndexWrite)(authState.keys, tcTokenKnownJids)
1427
+ return authState.keys.set({ tctoken: write })
1428
+ }
1429
+ function scheduleTcTokenIndexSave() {
1430
+ if (tcTokenIndexTimer) {
1431
+ clearTimeout(tcTokenIndexTimer)
1432
+ }
1433
+ tcTokenIndexTimer = setTimeout(() => {
1434
+ tcTokenIndexTimer = undefined
1435
+ flushTcTokenIndex().catch(err => {
1436
+ logger.warn({ err: err?.message }, 'failed to save tctoken index')
1437
+ })
1438
+ }, 5000)
1439
+ }
1440
+ function trackTcTokenJid(jid) {
1441
+ if (jid && jid !== tc_token_utils_1.TC_TOKEN_INDEX_KEY && !tcTokenKnownJids.has(jid)) {
1442
+ tcTokenKnownJids.add(jid)
1443
+ scheduleTcTokenIndexSave()
1444
+ }
1445
+ }
1446
+ const handlePrivacyTokenNotification = async node => {
1447
+ const tokensNode = (0, WABinary_1.getBinaryNodeChild)(node, 'tokens')
1448
+ if (!tokensNode) return
1449
+ const from = (0, WABinary_1.jidNormalizedUser)(node.attrs.from)
1450
+ // WA Web uses: senderLid ?? toLid(from) for the storage key
1451
+ const senderLid =
1452
+ node.attrs.sender_lid && (0, WABinary_1.isLidUser)((0, WABinary_1.jidNormalizedUser)(node.attrs.sender_lid))
1453
+ ? (0, WABinary_1.jidNormalizedUser)(node.attrs.sender_lid)
1454
+ : undefined
1455
+ const fallbackJid = senderLid ?? (await (0, tc_token_utils_1.resolveTcTokenJid)(from, getLIDForPN))
1456
+ logger.debug({ from, storageJid: fallbackJid }, 'processing privacy token notification')
1457
+ await (0, tc_token_utils_1.storeTcTokensFromIqResult)({
1458
+ result: node,
1459
+ fallbackJid,
1460
+ keys: authState.keys,
1461
+ getLIDForPN,
1462
+ onNewJidStored: trackTcTokenJid
1463
+ })
1464
+ }
1465
+ async function decipherLinkPublicKey(data) {
1466
+ const buffer = toRequiredBuffer(data)
1467
+ const salt = buffer.slice(0, 32)
1468
+ const secretKey = await (0, Utils_1.derivePairingCodeKey)(authState.creds.pairingCode, salt)
1469
+ const iv = buffer.slice(32, 48)
1470
+ const payload = buffer.slice(48, 80)
1471
+ return (0, Utils_1.aesDecryptCTR)(payload, secretKey, iv)
1472
+ }
1473
+ function toRequiredBuffer(data) {
1474
+ if (data === undefined) {
1475
+ throw new boom_1.Boom('Invalid buffer', { statusCode: 400 })
1476
+ }
1477
+ return data instanceof Buffer ? data : Buffer.from(data)
1478
+ }
1479
+ const willSendMessageAgain = async (id, participant) => {
1480
+ const key = `${id}:${participant}`
1481
+ const retryCount = (await msgRetryCache.get(key)) || 0
1482
+ return retryCount < maxMsgRetryCount
1483
+ }
1484
+ const updateSendMessageAgainCount = async (id, participant) => {
1485
+ const key = `${id}:${participant}`
1486
+ const newValue = ((await msgRetryCache.get(key)) || 0) + 1
1487
+ await msgRetryCache.set(key, newValue)
1488
+ }
1489
+ const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
1490
+ const remoteJid = key.remoteJid
1491
+ const participant = key.participant || remoteJid
1492
+ const retryCount = +retryNode.attrs.count || 1
1493
+ const msgId = ids[0]
1494
+ // Try to get messages from cache first, then fallback to getMessage
1495
+ const msgs = []
1496
+ for (const id of ids) {
1497
+ let msg
1498
+ // Try to get from retry cache first if enabled
1499
+ if (messageRetryManager) {
1500
+ const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id)
1501
+ if (cachedMsg) {
1502
+ msg = cachedMsg.message
1503
+ logger.debug({ jid: remoteJid, id }, 'found message in retry cache')
1504
+ // Mark retry as successful since we found the message
1505
+ messageRetryManager.markRetrySuccess(id)
1506
+ }
1507
+ }
1508
+ // Fallback to getMessage if not found in cache
1509
+ if (!msg) {
1510
+ msg = await getMessage({ ...key, id })
1511
+ if (msg) {
1512
+ logger.debug({ jid: remoteJid, id }, 'found message via getMessage')
1513
+ // Also mark as successful if found via getMessage
1514
+ if (messageRetryManager) {
1515
+ messageRetryManager.markRetrySuccess(id)
1516
+ }
1517
+ }
1518
+ }
1519
+ msgs.push(msg)
1520
+ }
1521
+ // if it's the primary jid sending the request
1522
+ // just re-send the message to everyone
1523
+ // prevents the first message decryption failure
1524
+ const sendToAll = !(0, WABinary_1.jidDecode)(participant)?.device
1525
+ const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
1526
+ let injectedFromBundle = false
1527
+ if (typeof signalRepository.injectE2ESession === 'function' && typeof Utils_1.extractE2ESessionFromRetryReceipt === 'function') {
1528
+ const bundle = Utils_1.extractE2ESessionFromRetryReceipt(receiptNode)
1529
+ if (bundle) {
1530
+ try {
1531
+ await signalRepository.injectE2ESession({ jid: participant, session: bundle })
1532
+ injectedFromBundle = true
1533
+ logger.debug({ participant, retryCount }, 'injected session from retry receipt key bundle')
1534
+ } catch (error) {
1535
+ logger.warn({ error, participant }, 'failed to inject session from retry receipt')
1536
+ }
1537
+ }
1538
+ }
1539
+ if (!injectedFromBundle && signalRepository.getSessionInfo) {
1540
+ const receivedRegId = (0, WABinary_1.getBinaryNodeChildUInt)(receiptNode, 'registration', 4)
1541
+ if (typeof receivedRegId === 'number' && Number.isInteger(receivedRegId)) {
1542
+ const info = await signalRepository.getSessionInfo(participant)
1543
+ if (info && info.registrationId !== 0 && info.registrationId !== receivedRegId) {
1544
+ logger.info(
1545
+ { participant, stored: info.registrationId, received: receivedRegId },
1546
+ 'reg id mismatch on retry without bundle, deleting session'
1547
+ )
1548
+ await authState.keys.set({ session: { [sessionId]: null } })
1549
+ }
1550
+ }
1551
+ }
1552
+ const BASE_KEY_CHECK_RETRY = 2
1553
+ if (msgId && messageRetryManager && signalRepository.getSessionInfo) {
1554
+ const info = await signalRepository.getSessionInfo(participant)
1555
+ if (info) {
1556
+ if (retryCount === BASE_KEY_CHECK_RETRY) {
1557
+ messageRetryManager.saveBaseKey(sessionId, msgId, info.baseKey)
1558
+ } else if (retryCount > BASE_KEY_CHECK_RETRY) {
1559
+ if (messageRetryManager.hasSameBaseKey(sessionId, msgId, info.baseKey)) {
1560
+ logger.warn({ participant, retryCount }, 'base key collision on retry, forcing fresh session')
1561
+ await authState.keys.set({ session: { [sessionId]: null } })
1562
+ }
1563
+ messageRetryManager.deleteBaseKey(sessionId, msgId)
1564
+ }
1565
+ }
1566
+ }
1567
+ // Check if we should recreate session for this retry
1568
+ let shouldRecreateSession = false
1569
+ let recreateReason = ''
1570
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
1571
+ try {
1572
+ const hasSession = await signalRepository.validateSession(participant)
1573
+ const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists)
1574
+ shouldRecreateSession = result.recreate
1575
+ recreateReason = result.reason
1576
+ if (shouldRecreateSession) {
1577
+ logger.debug({ participant, retryCount, reason: recreateReason }, 'recreating session for outgoing retry')
1578
+ await authState.keys.set({ session: { [sessionId]: null } })
1579
+ }
1580
+ } catch (error) {
1581
+ logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry')
1582
+ }
1583
+ }
1584
+ if (!injectedFromBundle) {
1585
+ await assertSessions([participant], true)
1586
+ }
1587
+ if ((0, WABinary_1.isJidGroup)(remoteJid)) {
1588
+ await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } })
1589
+ }
1590
+ logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason }, 'forced new session for retry recp')
1591
+ for (const [i, msg] of msgs.entries()) {
1592
+ if (!ids[i]) continue
1593
+ if (msg && (await willSendMessageAgain(ids[i], participant))) {
1594
+ await updateSendMessageAgainCount(ids[i], participant)
1595
+ const msgRelayOpts = { messageId: ids[i] }
1596
+ if (sendToAll) {
1597
+ msgRelayOpts.useUserDevicesCache = false
1598
+ } else {
1599
+ msgRelayOpts.participant = {
1600
+ jid: participant,
1601
+ count: +retryNode.attrs.count
1602
+ }
1603
+ }
1604
+ await relayMessage(key.remoteJid, msg, msgRelayOpts)
1605
+ } else {
1606
+ logger.debug({ jid: key.remoteJid, id: ids[i] }, 'recv retry request, but message not available')
1607
+ }
1608
+ }
1609
+ }
1610
+ const handleReceipt = async node => {
1611
+ const { attrs, content } = node
1612
+ const isLid = attrs.from.includes('lid')
1613
+ const isNodeFromMe = (0, WABinary_1.areJidsSameUser)(
1614
+ attrs.participant || attrs.from,
1615
+ isLid ? authState.creds.me?.lid : authState.creds.me?.id
1616
+ )
1617
+ const remoteJid = !isNodeFromMe || (0, WABinary_1.isJidGroup)(attrs.from) ? attrs.from : attrs.recipient
1618
+ const fromMe = !attrs.recipient || ((attrs.type === 'retry' || attrs.type === 'sender') && isNodeFromMe)
1619
+ const key = {
1620
+ remoteJid,
1621
+ id: '',
1622
+ fromMe,
1623
+ participant: attrs.participant
1624
+ }
1625
+ if (shouldIgnoreJid(remoteJid) && remoteJid !== WABinary_1.S_WHATSAPP_NET) {
1626
+ logger.debug({ remoteJid }, 'ignoring receipt from jid')
1627
+ await sendMessageAck(node)
1628
+ return
1629
+ }
1630
+ const ids = [attrs.id]
1631
+ if (Array.isArray(content)) {
1632
+ const items = (0, WABinary_1.getBinaryNodeChildren)(content[0], 'item')
1633
+ ids.push(...items.map(i => i.attrs.id))
1634
+ }
1635
+ try {
1636
+ await Promise.all([
1637
+ receiptMutex.mutex(async () => {
1638
+ const status = (0, Utils_1.getStatusFromReceiptType)(attrs.type)
1639
+ if (
1640
+ typeof status !== 'undefined' &&
1641
+ // basically, we only want to know when a message from us has been delivered to/read by the other person
1642
+ // or another device of ours has read some messages
1643
+ (status >= index_js_1.proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)
1644
+ ) {
1645
+ if ((0, WABinary_1.isJidGroup)(remoteJid) || (0, WABinary_1.isJidStatusBroadcast)(remoteJid)) {
1646
+ if (attrs.participant) {
1647
+ const updateKey =
1648
+ status === index_js_1.proto.WebMessageInfo.Status.DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp'
1649
+ ev.emit(
1650
+ 'message-receipt.update',
1651
+ ids.map(id => ({
1652
+ key: { ...key, id },
1653
+ receipt: {
1654
+ userJid: (0, WABinary_1.jidNormalizedUser)(attrs.participant),
1655
+ [updateKey]: +attrs.t
1656
+ }
1657
+ }))
1658
+ )
1659
+ }
1660
+ } else {
1661
+ ev.emit(
1662
+ 'messages.update',
1663
+ ids.map(id => ({
1664
+ key: { ...key, id },
1665
+ update: { status, messageTimestamp: (0, Utils_1.toNumber)(+(attrs.t ?? 0)) }
1666
+ }))
1667
+ )
1668
+ }
1669
+ }
1670
+ if (attrs.type === 'retry') {
1671
+ // correctly set who is asking for the retry
1672
+ key.participant = key.participant || attrs.from
1673
+ const retryNode = (0, WABinary_1.getBinaryNodeChild)(node, 'retry')
1674
+ if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
1675
+ if (key.fromMe) {
1676
+ try {
1677
+ await updateSendMessageAgainCount(ids[0], key.participant)
1678
+ logger.debug({ attrs, key }, 'recv retry request')
1679
+ await sendMessagesAgain(key, ids, retryNode, node)
1680
+ } catch (error) {
1681
+ logger.error(
1682
+ { key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' },
1683
+ 'error in sending message again'
1684
+ )
1685
+ }
1686
+ } else {
1687
+ logger.info({ attrs, key }, 'recv retry for not fromMe message')
1688
+ }
1689
+ } else {
1690
+ logger.info({ attrs, key }, 'will not send message again, as sent too many times')
1691
+ }
1692
+ }
1693
+ })
1694
+ ])
1695
+ } finally {
1696
+ await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack receipt'))
1697
+ }
1698
+ }
1699
+ const handleNotification = async node => {
1700
+ const remoteJid = node.attrs.from
1701
+ if (shouldIgnoreJid(remoteJid) && remoteJid !== WABinary_1.S_WHATSAPP_NET) {
1702
+ logger.debug({ remoteJid, id: node.attrs.id }, 'ignored notification')
1703
+ await sendMessageAck(node)
1704
+ return
1705
+ }
1706
+ try {
1707
+ await Promise.all([
1708
+ notificationMutex.mutex(async () => {
1709
+ const msg = await processNotification(node)
1710
+ if (msg) {
1711
+ const fromMe = (0, WABinary_1.areJidsSameUser)(node.attrs.participant || remoteJid, authState.creds.me.id)
1712
+ const { senderAlt: participantAlt, addressingMode } = (0, Utils_1.extractAddressingContext)(node)
1713
+ msg.key = {
1714
+ remoteJid,
1715
+ fromMe,
1716
+ participant: node.attrs.participant,
1717
+ participantAlt,
1718
+ participantUsername: node.attrs.participant_username,
1719
+ addressingMode,
1720
+ id: node.attrs.id,
1721
+ ...(msg.key || {})
1722
+ }
1723
+ msg.participant ?? (msg.participant = node.attrs.participant)
1724
+ msg.messageTimestamp = +node.attrs.t
1725
+ let groupDataForNormalization
1726
+ if ((0, WABinary_1.isJidGroup)(msg?.key?.remoteJid)) {
1727
+ try {
1728
+ groupDataForNormalization =
1729
+ (config.useCachedGroupMetadata && config.cachedGroupMetadata
1730
+ ? await config.cachedGroupMetadata(msg.key.remoteJid)
1731
+ : undefined) || (await sock.groupMetadata(msg.key.remoteJid))
1732
+ } catch (error) {
1733
+ logger.debug(
1734
+ { error, jid: msg.key.remoteJid },
1735
+ 'failed to fetch group metadata for recv jid normalization'
1736
+ )
1737
+ }
1738
+ }
1739
+ await (0, jid_display_normalization_1.normalizeMessageForDisplayJids)(
1740
+ msg,
1741
+ signalRepository,
1742
+ logger,
1743
+ groupDataForNormalization
1744
+ )
1745
+ const fullMsg = index_js_1.proto.WebMessageInfo.fromObject(msg)
1746
+ await upsertMessage(fullMsg, 'append')
1747
+ }
1748
+ })
1749
+ ])
1750
+ } finally {
1751
+ await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack notification'))
1752
+ }
1753
+ }
1754
+ const handleMessage = async node => {
1755
+ const isInteropNode = (0, WABinary_1.isInteropUser)(node.attrs.from)
1756
+ if (isInteropNode) {
1757
+ logger.info(
1758
+ {
1759
+ from: node.attrs.from,
1760
+ id: node.attrs.id,
1761
+ type: node.attrs.type,
1762
+ sts: node.attrs.sts,
1763
+ display_name: node.attrs.display_name,
1764
+ encType: (0, WABinary_1.getBinaryNodeChild)(node, 'enc')?.attrs?.type
1765
+ },
1766
+ '[interop] node arrived'
1767
+ )
1768
+ }
1769
+ if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== WABinary_1.S_WHATSAPP_NET) {
1770
+ if (isInteropNode) logger.warn({ from: node.attrs.from }, '[interop] node dropped by shouldIgnoreJid')
1771
+ logger.debug({ key: node.attrs.key }, 'ignored message')
1772
+ await sendMessageAck(node, Utils_1.NACK_REASONS.UnhandledError)
1773
+ return
1774
+ }
1775
+ const groupJid = node.attrs.from
1776
+ const communityJid = linkedParentMap[groupJid]
1777
+ const encNode = (0, WABinary_1.getBinaryNodeChild)(node, 'enc')
1778
+ // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
1779
+ if (encNode?.attrs.type === 'msmsg') {
1780
+ // await sendMessageAck(node, Utils_1.NACK_REASONS.MissingMessageSecret)
1781
+ // return
1782
+ // Pre-populate botMessageSecrets from store so msmsg can be decrypted after restart
1783
+ if (getMessage) {
1784
+ const metaNode = (0, WABinary_1.getBinaryNodeChild)(node, 'meta')
1785
+ const targetId = metaNode?.attrs?.target_id
1786
+ if (targetId) {
1787
+ try {
1788
+ const targetMsg = await getMessage({ remoteJid: node.attrs.from, id: targetId, fromMe: true })
1789
+ const secret = targetMsg?.messageContextInfo?.messageSecret
1790
+ if (secret) {
1791
+ ;(0, Utils_1.setBotMessageSecret)(targetId, secret)
1792
+ }
1793
+ } catch (err) {
1794
+ logger.debug({ err, targetId }, 'failed to retrieve message secret for msmsg')
1795
+ }
1796
+ }
1797
+ }
1798
+ }
1799
+ let acked = false
1800
+ try {
1801
+ const {
1802
+ fullMessage: msg,
1803
+ category,
1804
+ author,
1805
+ decrypt
1806
+ } = (0, Utils_1.decryptMessageNode)(
1807
+ node,
1808
+ authState.creds.me.id,
1809
+ authState.creds.me.lid || '',
1810
+ signalRepository,
1811
+ logger
1812
+ )
1813
+ if (isInteropNode) {
1814
+ logger.info(
1815
+ { remoteJid: msg.key.remoteJid, id: msg.key.id, fromMe: msg.key.fromMe, pushName: msg.pushName },
1816
+ '[interop] decodeMessageNode OK'
1817
+ )
1818
+ }
1819
+ const alt = msg.key.participantAlt || msg.key.remoteJidAlt
1820
+ // store new mappings we didn't have before
1821
+ if (!!alt) {
1822
+ const altServer = (0, WABinary_1.jidDecode)(alt)?.server
1823
+ const primaryJid = msg.key.participant || msg.key.remoteJid
1824
+ if (altServer === 'lid') {
1825
+ if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
1826
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }])
1827
+ await signalRepository.migrateSession(primaryJid, alt)
1828
+ }
1829
+ } else {
1830
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }])
1831
+ await signalRepository.migrateSession(alt, primaryJid)
1832
+ }
1833
+ }
1834
+ await messageMutex.mutex(async () => {
1835
+ await decrypt()
1836
+ if (isInteropNode) {
1837
+ const stubType = msg.messageStubType
1838
+ const stubText = msg.messageStubParameters?.[0]
1839
+ logger.info(
1840
+ {
1841
+ id: msg.key.id,
1842
+ hasMessage: !!msg.message,
1843
+ messageKeys: msg.message ? Object.keys(msg.message) : [],
1844
+ stubType,
1845
+ stubText
1846
+ },
1847
+ '[interop] decrypt() done'
1848
+ )
1849
+ }
1850
+ if (msg.key?.remoteJid && msg.key?.id && msg.message && messageRetryManager) {
1851
+ messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message)
1852
+ }
1853
+ // message failed to decrypt
1854
+ if (msg.messageStubType === index_js_1.proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
1855
+ if (msg?.messageStubParameters?.[0] === Utils_1.MISSING_KEYS_ERROR_TEXT) {
1856
+ if (isInteropNode) logger.warn({ id: msg.key.id }, '[interop] decrypt failed: MISSING_KEYS')
1857
+ acked = true
1858
+ return sendMessageAck(node, Utils_1.NACK_REASONS.ParsingError)
1859
+ }
1860
+ if (msg.messageStubParameters?.[0] === Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT) {
1861
+ // Message arrived without encryption (e.g. CTWA ads messages).
1862
+ // Check if this is eligible for placeholder resend (matching WA Web filters).
1863
+ const unavailableNode = (0, WABinary_1.getBinaryNodeChild)(node, 'unavailable')
1864
+ const unavailableType = unavailableNode?.attrs?.type
1865
+ if (
1866
+ unavailableType === 'bot_unavailable_fanout' ||
1867
+ unavailableType === 'hosted_unavailable_fanout' ||
1868
+ unavailableType === 'view_once_unavailable_fanout'
1869
+ ) {
1870
+ logger.debug(
1871
+ { msgId: msg.key.id, unavailableType },
1872
+ 'skipping placeholder resend for excluded unavailable type'
1873
+ )
1874
+ acked = true
1875
+ return sendMessageAck(node)
1876
+ }
1877
+ const messageAge = (0, Utils_1.unixTimestampSeconds)() - (0, Utils_1.toNumber)(msg.messageTimestamp)
1878
+ if (messageAge > Defaults_1.PLACEHOLDER_MAX_AGE_SECONDS) {
1879
+ logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message')
1880
+ acked = true
1881
+ return sendMessageAck(node)
1882
+ }
1883
+ // Request the real content from the phone via placeholder resend PDO.
1884
+ // Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg),
1885
+ // and store the requestId in stubParameters[1] so users can correlate
1886
+ // with the incoming PDO response event.
1887
+ const cleanKey = {
1888
+ remoteJid: msg.key.remoteJid,
1889
+ fromMe: msg.key.fromMe,
1890
+ id: msg.key.id,
1891
+ participant: msg.key.participant
1892
+ }
1893
+ // Cache the original message metadata so the PDO response handler
1894
+ // can preserve key fields (LID details etc.) that the phone may omit
1895
+ const msgData = {
1896
+ key: msg.key,
1897
+ messageTimestamp: msg.messageTimestamp,
1898
+ pushName: msg.pushName,
1899
+ participant: msg.participant,
1900
+ verifiedBizName: msg.verifiedBizName
1901
+ }
1902
+ requestPlaceholderResend(cleanKey, msgData)
1903
+ .then(requestId => {
1904
+ if (requestId && requestId !== 'RESOLVED') {
1905
+ logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message')
1906
+ ev.emit('messages.update', [
1907
+ {
1908
+ key: msg.key,
1909
+ update: { messageStubParameters: [Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT, requestId] }
1910
+ }
1911
+ ])
1912
+ }
1913
+ })
1914
+ .catch(err => {
1915
+ logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message')
1916
+ })
1917
+ acked = true
1918
+ await sendMessageAck(node)
1919
+ // Don't return — fall through to upsertMessage so the stub is emitted
1920
+ } else {
1921
+ // Skip retry for expired status messages (>24h old)
1922
+ if ((0, WABinary_1.isJidStatusBroadcast)(msg.key.remoteJid)) {
1923
+ const messageAge = (0, Utils_1.unixTimestampSeconds)() - (0, Utils_1.toNumber)(msg.messageTimestamp)
1924
+ if (messageAge > Defaults_1.STATUS_EXPIRY_SECONDS) {
1925
+ logger.debug(
1926
+ { msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid },
1927
+ 'skipping retry for expired status message'
1928
+ )
1929
+ acked = true
1930
+ return sendMessageAck(node)
1931
+ }
1932
+ }
1933
+ const errorMessage = msg?.messageStubParameters?.[0] || ''
1934
+ const isPreKeyError = errorMessage.includes('PreKey')
1935
+ logger.debug(`[handleMessage] Attempting retry request for failed decryption`)
1936
+ // Handle both pre-key and normal retries in single mutex
1937
+ await retryMutex.mutex(async () => {
1938
+ try {
1939
+ if (!ws.isOpen) {
1940
+ logger.debug({ node }, 'Connection closed, skipping retry')
1941
+ return
1942
+ }
1943
+ // Handle pre-key errors with upload and delay
1944
+ if (isPreKeyError) {
1945
+ logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying')
1946
+ try {
1947
+ logger.debug('Uploading pre-keys for error recovery')
1948
+ await uploadPreKeys(5)
1949
+ logger.debug('Waiting for server to process new pre-keys')
1950
+ await (0, Utils_1.delay)(1000)
1951
+ } catch (uploadErr) {
1952
+ logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway')
1953
+ }
1954
+ }
1955
+ const encNode = (0, WABinary_1.getBinaryNodeChild)(node, 'enc')
1956
+ await sendRetryRequest(node, !encNode)
1957
+ if (retryRequestDelayMs) {
1958
+ await (0, Utils_1.delay)(retryRequestDelayMs)
1959
+ }
1960
+ } catch (err) {
1961
+ logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry')
1962
+ // Still attempt retry even if pre-key upload failed
1963
+ try {
1964
+ const encNode = (0, WABinary_1.getBinaryNodeChild)(node, 'enc')
1965
+ await sendRetryRequest(node, !encNode)
1966
+ } catch (retryErr) {
1967
+ logger.error({ retryErr }, 'Failed to send retry after error handling')
1968
+ }
1969
+ }
1970
+ acked = true
1971
+ await sendMessageAck(node, Utils_1.NACK_REASONS.UnhandledError)
1972
+ })
1973
+ }
1974
+ } else {
1975
+ if (messageRetryManager && msg.key.id) {
1976
+ messageRetryManager.cancelPendingPhoneRequest(msg.key.id)
1977
+ }
1978
+ const isNewsletter = (0, WABinary_1.isJidNewsletter)(msg.key.remoteJid)
1979
+ if (!isNewsletter) {
1980
+ // no type in the receipt => message delivered
1981
+ let type = undefined
1982
+ let participant = msg.key.participant
1983
+ if (communityJid) {
1984
+ msg.communityJid = communityJid
1985
+ }
1986
+ if (category === 'peer') {
1987
+ // special peer message
1988
+ type = 'peer_msg'
1989
+ } else if (msg.key.fromMe) {
1990
+ // message was sent by us from a different device
1991
+ type = 'sender'
1992
+ // need to specially handle this case
1993
+ if ((0, WABinary_1.isLidUser)(msg.key.remoteJid) || (0, WABinary_1.isLidUser)(msg.key.remoteJidAlt)) {
1994
+ participant = author // TODO: investigate sending receipts to LIDs and not PNs
1995
+ }
1996
+ } else if (!sendActiveReceipts) {
1997
+ type = 'inactive'
1998
+ }
1999
+ acked = true
2000
+ // Pass sts from the original stanza for interop contacts (BirdyChat/Haiket)
2001
+ const interopSts = (0, WABinary_1.isInteropUser)(msg.key.remoteJid) ? node.attrs.sts : undefined
2002
+ await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type, interopSts)
2003
+ // send ack for history message
2004
+ const isAnyHistoryMsg = (0, Utils_1.getHistoryMsg)(msg.message)
2005
+ if (isAnyHistoryMsg) {
2006
+ const jid = (0, WABinary_1.jidNormalizedUser)(msg.key.remoteJid)
2007
+ await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync') // TODO: investigate
2008
+ }
2009
+ } else {
2010
+ acked = true
2011
+ await sendMessageAck(node)
2012
+ logger.debug({ key: msg.key }, 'processed newsletter message without receipts')
2013
+ }
2014
+ }
2015
+ ;(0, Utils_1.cleanMessage)(msg, authState.creds.me.id, authState.creds.me.lid)
2016
+ const msgTs = (0, Utils_1.toNumber)(msg.messageTimestamp)
2017
+ const isPending = !isConnected || node.attrs.offline || msgTs < socketCreatedAt
2018
+ if (isInteropNode) {
2019
+ logger.info(
2020
+ {
2021
+ id: msg.key.id,
2022
+ isPending,
2023
+ isConnected,
2024
+ offline: node.attrs.offline,
2025
+ msgTs,
2026
+ socketCreatedAt
2027
+ },
2028
+ isPending ? '[interop] upsert as append (pending/offline)' : '[interop] upsert as notify'
2029
+ )
2030
+ }
2031
+ if (isPending) {
2032
+ await upsertMessage(msg, 'append')
2033
+ return
2034
+ }
2035
+ let groupDataForNormalization
2036
+ if ((0, WABinary_1.isJidGroup)(msg?.key?.remoteJid)) {
2037
+ try {
2038
+ groupDataForNormalization =
2039
+ (config.useCachedGroupMetadata && config.cachedGroupMetadata
2040
+ ? await config.cachedGroupMetadata(msg.key.remoteJid)
2041
+ : undefined) || (await sock.groupMetadata(msg.key.remoteJid))
2042
+ } catch (error) {
2043
+ logger.debug({ error, jid: msg.key.remoteJid }, 'failed to fetch group metadata for recv jid normalization')
2044
+ }
2045
+ }
2046
+ await (0, jid_display_normalization_1.normalizeMessageForDisplayJids)(
2047
+ msg,
2048
+ signalRepository,
2049
+ logger,
2050
+ groupDataForNormalization
2051
+ )
2052
+ await upsertMessage(msg, 'notify')
2053
+ })
2054
+ } catch (error) {
2055
+ if (isInteropNode) {
2056
+ logger.error(
2057
+ { error: error?.message, stack: error?.stack, from: node.attrs.from, id: node.attrs.id },
2058
+ '[interop] unhandled error in handleMessage'
2059
+ )
2060
+ }
2061
+ logger.error({ error, node: (0, WABinary_1.binaryNodeToString)(node) }, 'error in handling message')
2062
+ if (!acked) {
2063
+ await sendMessageAck(node, Utils_1.NACK_REASONS.UnhandledError).catch(ackErr =>
2064
+ logger.error({ ackErr }, 'failed to ack message after error')
2065
+ )
2066
+ }
2067
+ }
2068
+ }
2069
+ const handleCall = async node => {
2070
+ try {
2071
+ const { attrs } = node
2072
+ const [infoChild] = (0, WABinary_1.getAllBinaryNodeChildren)(node)
2073
+ if (!infoChild) {
2074
+ throw new boom_1.Boom('Missing call info in call node')
2075
+ }
2076
+ const status = (0, Utils_1.getCallStatusFromNode)(infoChild)
2077
+ const callId = infoChild.attrs['call-id']
2078
+ const from = infoChild.attrs.from || infoChild.attrs['call-creator']
2079
+ const call = {
2080
+ chatId: attrs.from,
2081
+ from,
2082
+ callerPn: infoChild.attrs['caller_pn'],
2083
+ id: callId,
2084
+ date: new Date(+attrs.t * 1000),
2085
+ offline: !!attrs.offline,
2086
+ status
2087
+ }
2088
+ if (status === 'relaylatency') {
2089
+ const latencyValue = infoChild.attrs.latency || infoChild.attrs['latency_ms'] || infoChild.attrs['latency-ms']
2090
+ const latencyMs = latencyValue ? Number(latencyValue) : undefined
2091
+ if (Number.isFinite(latencyMs)) {
2092
+ call.latencyMs = latencyMs
2093
+ }
2094
+ }
2095
+ if (status === 'offer') {
2096
+ call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(infoChild, 'video')
2097
+ call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid']
2098
+ call.groupJid = infoChild.attrs['group-jid']
2099
+ await callOfferCache.set(call.id, call)
2100
+ }
2101
+ const existingCall = await callOfferCache.get(call.id)
2102
+ // use existing call info to populate this event
2103
+ if (existingCall) {
2104
+ call.isVideo = existingCall.isVideo
2105
+ call.isGroup = existingCall.isGroup
2106
+ call.callerPn = call.callerPn || existingCall.callerPn
2107
+ }
2108
+ // delete data once call has ended
2109
+ if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
2110
+ await callOfferCache.del(call.id)
2111
+ }
2112
+ await normalizeCallEventJids(call, infoChild)
2113
+ ev.emit('call', [call])
2114
+ } catch (error) {
2115
+ logger.error({ error, node: (0, WABinary_1.binaryNodeToString)(node) }, 'error in handling call')
2116
+ } finally {
2117
+ await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack call'))
2118
+ }
2119
+ }
2120
+ // Some accounts receive call signalling as TOP-LEVEL stanzas (<offer>, <terminate>,
2121
+ // <mute_v2>, <transport>, … each with a call-id) instead of wrapped in <call>.
2122
+ // This additively handles those: emit a 'call' event for state stanzas and ack ALL
2123
+ // of them (otherwise WhatsApp keeps redelivering). The <call> path above is untouched.
2124
+ const CALL_STATE_TAGS = new Set(['offer', 'offer_notice', 'terminate', 'accept', 'reject', 'preaccept'])
2125
+ const handleStandaloneCallStanza = async node => {
2126
+ try {
2127
+ if (!CALL_STATE_TAGS.has(node.tag)) {
2128
+ return // media/relay signalling (transport, video, duration, mute_v2, lobby, …): ack only
2129
+ }
2130
+ const { attrs } = node
2131
+ const status = (0, Utils_1.getCallStatusFromNode)(node)
2132
+ const callId = attrs['call-id']
2133
+ const from = attrs.from || attrs['call-creator']
2134
+ const call = {
2135
+ chatId: attrs.from || from,
2136
+ from,
2137
+ callerPn: attrs['caller_pn'],
2138
+ id: callId,
2139
+ date: attrs.t ? new Date(+attrs.t * 1000) : new Date(),
2140
+ offline: !!attrs.offline,
2141
+ status
2142
+ }
2143
+ if (status === 'offer') {
2144
+ call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(node, 'video')
2145
+ call.isGroup = attrs.type === 'group' || !!attrs['group-jid']
2146
+ call.groupJid = attrs['group-jid']
2147
+ if (callId) {
2148
+ await callOfferCache.set(callId, call)
2149
+ }
2150
+ }
2151
+ const existingCall = callId ? await callOfferCache.get(callId) : undefined
2152
+ if (existingCall) {
2153
+ call.isVideo = existingCall.isVideo
2154
+ call.isGroup = existingCall.isGroup
2155
+ call.callerPn = call.callerPn || existingCall.callerPn
2156
+ }
2157
+ if (callId && (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate')) {
2158
+ await callOfferCache.del(callId)
2159
+ }
2160
+ await normalizeCallEventJids(call, node)
2161
+ ev.emit('call', [call])
2162
+ } catch (error) {
2163
+ logger.error({ error, node: (0, WABinary_1.binaryNodeToString)(node) }, 'error handling standalone call stanza')
2164
+ } finally {
2165
+ await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack standalone call'))
2166
+ }
2167
+ }
2168
+ const handleBadAck = async ({ attrs }) => {
2169
+ const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id }
2170
+ // WARNING: REFRAIN FROM ENABLING THIS FOR NOW. IT WILL CAUSE A LOOP
2171
+ // // current hypothesis is that if pash is sent in the ack
2172
+ // // it means -- the message hasn't reached all devices yet
2173
+ // // we'll retry sending the message here
2174
+ // if(attrs.phash) {
2175
+ // logger.info({ attrs }, 'received phash in ack, resending message...')
2176
+ // const msg = await getMessage(key)
2177
+ // if(msg) {
2178
+ // await relayMessage(key.remoteJid!, msg, { messageId: key.id!, useUserDevicesCache: false })
2179
+ // } else {
2180
+ // logger.warn({ attrs }, 'could not send message again, as it was not found')
2181
+ // }
2182
+ // }
2183
+ // error in acknowledgement,
2184
+ // device could not display the message
2185
+ if (attrs.error) {
2186
+ if (attrs.error === Utils_1.SERVER_ERROR_CODES.MissingTcToken) {
2187
+ // 463 = account restricted + no tctoken for this contact.
2188
+ // WA Web prevents this client-side (disables compose bar).
2189
+ // No retry — retrying worsens the restriction by counting as another "reach out" to an unknown contact.
2190
+ logger.warn(
2191
+ { msgId: attrs.id, from: attrs.from },
2192
+ 'error 463: account restricted or missing tctoken for contact'
2193
+ )
2194
+ } else if (attrs.error === Utils_1.SERVER_ERROR_CODES.SmaxInvalid) {
2195
+ logger.warn(
2196
+ { msgId: attrs.id, from: attrs.from },
2197
+ 'smax-invalid (479): stanza rejected by server — likely stale device session or malformed addressing'
2198
+ )
2199
+ } else {
2200
+ logger.warn({ attrs }, 'received error in ack')
2201
+ }
2202
+ ev.emit('messages.update', [
2203
+ {
2204
+ key,
2205
+ update: {
2206
+ status: Types_1.WAMessageStatus.ERROR,
2207
+ messageStubParameters: [attrs.error]
2208
+ }
2209
+ }
2210
+ ])
2211
+ // resend the message with device_fanout=false, use at your own risk
2212
+ // if (attrs.error === '475') {
2213
+ // const msg = await getMessage(key)
2214
+ // if (msg) {
2215
+ // await relayMessage(key.remoteJid!, msg, {
2216
+ // messageId: key.id!,
2217
+ // useUserDevicesCache: false,
2218
+ // additionalAttributes: {
2219
+ // device_fanout: 'false'
2220
+ // }
2221
+ // })
2222
+ // }
2223
+ // }
2224
+ }
2225
+ }
2226
+ /// processes a node with the given function
2227
+ /// and adds the task to the existing buffer if we're buffering events
2228
+ const processNodeWithBuffer = async (node, identifier, exec) => {
2229
+ ev.buffer()
2230
+ await execTask()
2231
+ ev.flush()
2232
+ function execTask() {
2233
+ return exec(node, false).catch(err => onUnexpectedError(err, identifier))
2234
+ }
2235
+ }
2236
+ const offlineNodeProcessor = (0, offline_node_processor_1.makeOfflineNodeProcessor)(
2237
+ new Map([
2238
+ ['message', handleMessage],
2239
+ ['call', handleCall],
2240
+ ['receipt', handleReceipt],
2241
+ ['notification', handleNotification]
2242
+ ]),
2243
+ {
2244
+ isWsOpen: () => ws.isOpen,
2245
+ onUnexpectedError,
2246
+ yieldToEventLoop: () => new Promise(resolve => setImmediate(resolve))
2247
+ }
2248
+ )
2249
+ const processNode = async (type, node, identifier, exec) => {
2250
+ const isOffline = !!node.attrs.offline
2251
+ if (isOffline) {
2252
+ offlineNodeProcessor.enqueue(type, node)
2253
+ } else {
2254
+ await processNodeWithBuffer(node, identifier, exec)
2255
+ }
2256
+ }
2257
+ let latestNodeInMemory = null
2258
+ const nodelogger = node => {
2259
+ if (!node) return null
2260
+ latestNodeInMemory = node
2261
+ return latestNodeInMemory
2262
+ }
2263
+ const setNodeLoggerListener = () => {
2264
+ return latestNodeInMemory
2265
+ }
2266
+ // recv a message
2267
+ ws.on('CB:message', async node => {
2268
+ nodelogger(node)
2269
+ await processNode('message', node, 'processing message', handleMessage)
2270
+ })
2271
+ ws.on('CB:call', async node => {
2272
+ nodelogger(node)
2273
+ await processNode('call', node, 'handling call', handleCall)
2274
+ })
2275
+ // additive: top-level call-signalling stanzas (some accounts send these instead of <call>)
2276
+ for (const callTag of [
2277
+ 'offer', 'offer_notice', 'terminate', 'accept', 'reject', 'preaccept',
2278
+ 'transport', 'video', 'duration', 'mute_v2', 'lobby', 'heartbeat', 'relaylatency', 'link_query'
2279
+ ]) {
2280
+ ws.on('CB:' + callTag, node => {
2281
+ nodelogger(node)
2282
+ handleStandaloneCallStanza(node).catch(error => onUnexpectedError(error, 'handling standalone call stanza'))
2283
+ })
2284
+ }
2285
+ ws.on('CB:receipt', async node => {
2286
+ nodelogger(node)
2287
+ await processNode('receipt', node, 'handling receipt', handleReceipt)
2288
+ })
2289
+ ws.on('CB:notification', async node => {
2290
+ nodelogger(node)
2291
+ await processNode('notification', node, 'handling notification', handleNotification)
2292
+ })
2293
+ ws.on('CB:ack,class:message', node => {
2294
+ nodelogger(node)
2295
+ handleBadAck(node).catch(error => onUnexpectedError(error, 'handling bad ack'))
2296
+ })
2297
+ const linkedParentMap = {}
2298
+ ws.on('CB:iq', node => {
2299
+ if (node && node.tag === 'iq' && node.attrs.type === 'result') {
2300
+ const groups = node.content
2301
+
2302
+ if (Array.isArray(groups)) {
2303
+ for (const group of groups) {
2304
+ const groupId = group.attrs.id + '@g.us'
2305
+
2306
+ if (group && Array.isArray(group.content)) {
2307
+ for (const item of group.content) {
2308
+ if (item.tag === 'linked_parent' && item.attrs && item.attrs.jid) {
2309
+ linkedParentMap[groupId] = item.attrs.jid
2310
+ }
2311
+ }
2312
+ }
2313
+ }
2314
+ }
2315
+ }
2316
+ })
2317
+ ev.on('call', async ([call]) => {
2318
+ if (!call) {
2319
+ return
2320
+ }
2321
+ nodelogger(call)
2322
+ // missed call + group call notification message generation
2323
+ if (call.status === 'timeout' || (call.status === 'offer' && call.isGroup)) {
2324
+ const msg = {
2325
+ key: {
2326
+ remoteJid: call.chatId,
2327
+ id: call.id,
2328
+ fromMe: false
2329
+ },
2330
+ messageTimestamp: (0, Utils_1.unixTimestampSeconds)(call.date)
2331
+ }
2332
+ if (call.status === 'timeout') {
2333
+ if (call.isGroup) {
2334
+ msg.messageStubType = call.isVideo
2335
+ ? Types_1.WAMessageStubType.CALL_MISSED_GROUP_VIDEO
2336
+ : Types_1.WAMessageStubType.CALL_MISSED_GROUP_VOICE
2337
+ } else {
2338
+ msg.messageStubType = call.isVideo
2339
+ ? Types_1.WAMessageStubType.CALL_MISSED_VIDEO
2340
+ : Types_1.WAMessageStubType.CALL_MISSED_VOICE
2341
+ }
2342
+ } else {
2343
+ msg.message = { call: { callKey: Buffer.from(call.id) } }
2344
+ }
2345
+ const protoMsg = index_js_1.proto.WebMessageInfo.fromObject(msg)
2346
+ await upsertMessage(protoMsg, call.offline ? 'append' : 'notify')
2347
+ }
2348
+ })
2349
+ let lastTcTokenPruneTs = 0
2350
+ ev.on('connection.update', ({ isOnline, connection }) => {
2351
+ if (connection === 'open') {
2352
+ isConnected = true
2353
+ }
2354
+ if (typeof isOnline !== 'undefined') {
2355
+ sendActiveReceipts = isOnline
2356
+ logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`)
2357
+ }
2358
+ // Daily cleanup of expired tctokens (mirrors WA Web's CLEAN_TC_TOKENS task)
2359
+ if (isOnline) {
2360
+ const now = Date.now()
2361
+ const DAY_MS = 24 * 60 * 60 * 1000
2362
+ if (now - lastTcTokenPruneTs >= DAY_MS) {
2363
+ lastTcTokenPruneTs = now
2364
+ void pruneExpiredTcTokens()
2365
+ }
2366
+ }
2367
+ })
2368
+ async function pruneExpiredTcTokens() {
2369
+ try {
2370
+ await tcTokenIndexLoaded
2371
+ const persisted = await (0, tc_token_utils_1.readTcTokenIndex)(authState.keys)
2372
+ const allJids = new Set(tcTokenKnownJids)
2373
+ for (const jid of persisted) allJids.add(jid)
2374
+ if (!allJids.size) return
2375
+ const jids = [...allJids]
2376
+ const allTokens = await authState.keys.get('tctoken', jids)
2377
+ const writes = {}
2378
+ const survivors = new Set()
2379
+ let mutated = 0
2380
+ for (const jid of jids) {
2381
+ const entry = allTokens[jid]
2382
+ if (!entry) {
2383
+ mutated++
2384
+ continue
2385
+ }
2386
+ const hasPeerToken = !!entry.token?.length
2387
+ const peerTokenExpired = hasPeerToken && (0, tc_token_utils_1.isTcTokenExpired)(entry.timestamp)
2388
+ const hasSenderTs = entry.senderTimestamp !== undefined
2389
+ const senderTsExpired = hasSenderTs && (0, tc_token_utils_1.isTcTokenExpired)(entry.senderTimestamp)
2390
+ const keepPeerToken = hasPeerToken && !peerTokenExpired
2391
+ const keepSenderTs = hasSenderTs && !senderTsExpired
2392
+ if (!keepPeerToken && !keepSenderTs) {
2393
+ writes[jid] = null
2394
+ mutated++
2395
+ } else if (peerTokenExpired && keepSenderTs) {
2396
+ writes[jid] = { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp }
2397
+ survivors.add(jid)
2398
+ mutated++
2399
+ } else {
2400
+ survivors.add(jid)
2401
+ }
2402
+ }
2403
+ if (mutated === 0) return
2404
+ await authState.keys.set({
2405
+ tctoken: {
2406
+ ...writes,
2407
+ [tc_token_utils_1.TC_TOKEN_INDEX_KEY]: {
2408
+ token: Buffer.from(JSON.stringify([...survivors]))
2409
+ }
2410
+ }
2411
+ })
2412
+ tcTokenKnownJids.clear()
2413
+ for (const jid of survivors) tcTokenKnownJids.add(jid)
2414
+ logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens')
2415
+ } catch (err) {
2416
+ logger.warn({ err: err?.message }, 'failed to prune expired tctokens')
2417
+ }
2418
+ }
2419
+ return {
2420
+ ...sock,
2421
+ sendMessageAck,
2422
+ sendRetryRequest,
2423
+ offerCall,
2424
+ rejectCall,
2425
+ acceptCall,
2426
+ terminateCall,
2427
+ rekeyCall,
2428
+ joinCallLink,
2429
+ queryCallLink,
2430
+ nodelogger,
2431
+ setNodeLoggerListener,
2432
+ fetchMessageHistory,
2433
+ requestPlaceholderResend,
2434
+ requestWaffleNonce,
2435
+ requestCompanionCanonicalNonce,
2436
+ requestCompanionMetaNonce,
2437
+ messageRetryManager
2438
+ }
2439
+ }
2440
+ exports.makeMessagesRecvSocket = makeMessagesRecvSocket