@nexustechpro/baileys 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,193 +2,70 @@ import NodeCache from "@cacheable/node-cache"
2
2
  import { Boom } from "@hapi/boom"
3
3
  import { randomBytes } from "crypto"
4
4
  import { proto } from "../../WAProto/index.js"
5
- import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from "../Defaults/index.js"
5
+ import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from "../Defaults/index.js"
6
6
  import { WAMessageStatus, WAMessageStubType } from "../Types/index.js"
7
- import {
8
- aesDecryptCTR,
9
- aesEncryptGCM,
10
- cleanMessage,
11
- Curve,
12
- decodeMediaRetryNode,
13
- decodeMessageNode,
14
- decryptMessageNode,
15
- delay,
16
- derivePairingCodeKey,
17
- encodeBigEndian,
18
- encodeSignedDeviceIdentity,
19
- extractAddressingContext,
20
- getCallStatusFromNode,
21
- getHistoryMsg,
22
- getNextPreKeys,
23
- getStatusFromReceiptType,
24
- hkdf,
25
- MISSING_KEYS_ERROR_TEXT,
26
- NACK_REASONS,
27
- unixTimestampSeconds,
28
- xmppPreKey,
29
- xmppSignedPreKey,
30
- } from "../Utils/index.js"
7
+ import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, handleIdentityChange, getStatusFromReceiptType, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from "../Utils/index.js"
31
8
  import { makeMutex } from "../Utils/make-mutex.js"
32
- import {
33
- areJidsSameUser,
34
- binaryNodeToString,
35
- getAllBinaryNodeChildren,
36
- getBinaryNodeChild,
37
- getBinaryNodeChildBuffer,
38
- getBinaryNodeChildren,
39
- getBinaryNodeChildString,
40
- isJidGroup,
41
- isJidStatusBroadcast,
42
- isLidUser,
43
- isPnUser,
44
- jidDecode,
45
- jidNormalizedUser,
46
- S_WHATSAPP_NET,
47
- } from "../WABinary/index.js"
9
+ import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from "../WABinary/index.js"
48
10
  import { extractGroupMetadata } from "./groups.js"
49
11
  import { makeMessagesSocket } from "./messages-send.js"
12
+
50
13
  export const makeMessagesRecvSocket = (config) => {
51
- const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } =
52
- config
14
+ const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config
53
15
  const sock = makeMessagesSocket(config)
54
- const {
55
- ev,
56
- authState,
57
- ws,
58
- processingMutex,
59
- signalRepository,
60
- query,
61
- upsertMessage,
62
- resyncAppState,
63
- onUnexpectedError,
64
- assertSessions,
65
- sendNode,
66
- relayMessage,
67
- sendReceipt,
68
- uploadPreKeys,
69
- sendPeerDataOperationMessage,
70
- messageRetryManager,
71
- triggerPreKeyCheck,
72
- } = sock
73
- /** this mutex ensures that each retryRequest will wait for the previous one to finish */
16
+ const { ev, authState, ws, processingMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager, triggerPreKeyCheck } = sock
17
+
18
+ const messageMutex = makeMutex()
19
+ const notificationMutex = makeMutex()
20
+ const receiptMutex = makeMutex()
74
21
  const retryMutex = makeMutex()
75
- const msgRetryCache =
76
- config.msgRetryCounterCache ||
77
- new NodeCache({
78
- stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
79
- useClones: false,
80
- })
81
- const callOfferCache =
82
- config.callOfferCache ||
83
- new NodeCache({
84
- stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
85
- useClones: false,
86
- })
87
- const placeholderResendCache =
88
- config.placeholderResendCache ||
89
- new NodeCache({
90
- stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
91
- useClones: false,
92
- })
22
+
23
+ const msgRetryCache = config.msgRetryCounterCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, useClones: false })
24
+ const callOfferCache = config.callOfferCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, useClones: false })
25
+ const placeholderResendCache = config.placeholderResendCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, useClones: false })
26
+ const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false })
27
+
93
28
  let sendActiveReceipts = false
29
+
94
30
  const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
95
- if (!authState.creds.me?.id) {
96
- throw new Boom("Not authenticated")
97
- }
98
- const pdoMessage = {
99
- historySyncOnDemandRequest: {
100
- chatJid: oldestMsgKey.remoteJid,
101
- oldestMsgFromMe: oldestMsgKey.fromMe,
102
- oldestMsgId: oldestMsgKey.id,
103
- oldestMsgTimestampMs: oldestMsgTimestamp,
104
- onDemandMsgCount: count,
105
- },
106
- peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND,
107
- }
31
+ if (!authState.creds.me?.id) throw new Boom("Not authenticated")
32
+ const pdoMessage = { historySyncOnDemandRequest: { chatJid: oldestMsgKey.remoteJid, oldestMsgFromMe: oldestMsgKey.fromMe, oldestMsgId: oldestMsgKey.id, oldestMsgTimestampMs: oldestMsgTimestamp, onDemandMsgCount: count }, peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND }
108
33
  return sendPeerDataOperationMessage(pdoMessage)
109
34
  }
110
- const requestPlaceholderResend = async (messageKey) => {
111
- if (!authState.creds.me?.id) {
112
- throw new Boom("Not authenticated")
113
- }
114
- if (placeholderResendCache.get(messageKey?.id)) {
115
- logger.debug({ messageKey }, "already requested resend")
116
- return
117
- } else {
118
- placeholderResendCache.set(messageKey?.id, true)
119
- }
120
- await delay(5000)
121
- if (!placeholderResendCache.get(messageKey?.id)) {
122
- logger.debug({ messageKey }, "message received while resend requested")
123
- return "RESOLVED"
124
- }
125
- const pdoMessage = {
126
- placeholderMessageResendRequest: [
127
- {
128
- messageKey,
129
- },
130
- ],
131
- peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND,
132
- }
133
- setTimeout(() => {
134
- if (placeholderResendCache.get(messageKey?.id)) {
135
- logger.debug({ messageKey }, "PDO message without response after 15 seconds. Phone possibly offline")
136
- placeholderResendCache.del(messageKey?.id)
137
- }
138
- }, 15000)
35
+
36
+ const requestPlaceholderResend = async (messageKey, msgData) => {
37
+ if (!authState.creds.me?.id) throw new Boom("Not authenticated")
38
+ if (await placeholderResendCache.get(messageKey?.id)) { logger.debug({ messageKey }, "already requested resend"); return }
39
+ else await placeholderResendCache.set(messageKey?.id, msgData || true)
40
+ await delay(2000)
41
+ if (!(await placeholderResendCache.get(messageKey?.id))) { logger.debug({ messageKey }, "message received while resend requested"); return "RESOLVED" }
42
+ const pdoMessage = { placeholderMessageResendRequest: [{ messageKey }], peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND }
43
+ setTimeout(async () => { if (await placeholderResendCache.get(messageKey?.id)) { logger.debug({ messageKey }, "PDO message without response after 8 seconds. Phone possibly offline"); await placeholderResendCache.del(messageKey?.id) } }, 8000)
139
44
  return sendPeerDataOperationMessage(pdoMessage)
140
45
  }
141
- // Handles mex newsletter notifications
46
+
142
47
  const handleMexNewsletterNotification = async (node) => {
143
48
  const mexNode = getBinaryNodeChild(node, "mex")
144
- if (!mexNode?.content) {
145
- //logger.warn({ node }, "Invalid mex newsletter notification")
146
- return
147
- }
49
+ if (!mexNode?.content) { logger.warn({ node }, "Invalid mex newsletter notification"); return }
148
50
  let data
149
- try {
150
- data = JSON.parse(mexNode.content.toString())
151
- } catch (error) {
152
- logger.error({ err: error, node }, "Failed to parse mex newsletter notification")
153
- return
154
- }
51
+ try { data = JSON.parse(mexNode.content.toString()) } catch (error) { logger.error({ err: error, node }, "Failed to parse mex newsletter notification"); return }
155
52
  const operation = data?.operation
156
53
  const updates = data?.updates
157
- if (!updates || !operation) {
158
- logger.warn({ data }, "Invalid mex newsletter notification content")
159
- return
160
- }
54
+ if (!updates || !operation) { logger.warn({ data }, "Invalid mex newsletter notification content"); return }
161
55
  logger.info({ operation, updates }, "got mex newsletter notification")
162
56
  switch (operation) {
163
57
  case "NotificationNewsletterUpdate":
164
- for (const update of updates) {
165
- if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
166
- ev.emit("newsletter-settings.update", {
167
- id: update.jid,
168
- update: update.settings,
169
- })
170
- }
171
- }
58
+ for (const update of updates) if (update.jid && update.settings && Object.keys(update.settings).length > 0) ev.emit("newsletter-settings.update", { id: update.jid, update: update.settings })
172
59
  break
173
60
  case "NotificationNewsletterAdminPromote":
174
- for (const update of updates) {
175
- if (update.jid && update.user) {
176
- ev.emit("newsletter-participants.update", {
177
- id: update.jid,
178
- author: node.attrs.from,
179
- user: update.user,
180
- new_role: "ADMIN",
181
- action: "promote",
182
- })
183
- }
184
- }
61
+ for (const update of updates) if (update.jid && update.user) ev.emit("newsletter-participants.update", { id: update.jid, author: node.attrs.from, user: update.user, new_role: "ADMIN", action: "promote" })
185
62
  break
186
63
  default:
187
64
  logger.info({ operation, data }, "Unhandled mex newsletter notification")
188
65
  break
189
66
  }
190
67
  }
191
- // Handles newsletter notifications
68
+
192
69
  const handleNewsletterNotification = async (node) => {
193
70
  const from = node.attrs.from
194
71
  const child = getAllBinaryNodeChildren(node)[0]
@@ -196,33 +73,13 @@ export const makeMessagesRecvSocket = (config) => {
196
73
  logger.info({ from, child }, "got newsletter notification")
197
74
  switch (child.tag) {
198
75
  case "reaction":
199
- const reactionUpdate = {
200
- id: from,
201
- server_id: child.attrs.message_id,
202
- reaction: {
203
- code: getBinaryNodeChildString(child, "reaction"),
204
- count: 1,
205
- },
206
- }
207
- ev.emit("newsletter.reaction", reactionUpdate)
76
+ ev.emit("newsletter.reaction", { id: from, server_id: child.attrs.message_id, reaction: { code: getBinaryNodeChildString(child, "reaction"), count: 1 } })
208
77
  break
209
78
  case "view":
210
- const viewUpdate = {
211
- id: from,
212
- server_id: child.attrs.message_id,
213
- count: Number.parseInt(child.content?.toString() || "0", 10),
214
- }
215
- ev.emit("newsletter.view", viewUpdate)
79
+ ev.emit("newsletter.view", { id: from, server_id: child.attrs.message_id, count: parseInt(child.content?.toString() || "0", 10) })
216
80
  break
217
81
  case "participant":
218
- const participantUpdate = {
219
- id: from,
220
- author,
221
- user: child.attrs.jid,
222
- action: child.attrs.action,
223
- new_role: child.attrs.role,
224
- }
225
- ev.emit("newsletter-participants.update", participantUpdate)
82
+ ev.emit("newsletter-participants.update", { id: from, author, user: child.attrs.jid, action: child.attrs.action, new_role: child.attrs.role })
226
83
  break
227
84
  case "update":
228
85
  const settingsNode = getBinaryNodeChild(child, "settings")
@@ -232,35 +89,19 @@ export const makeMessagesRecvSocket = (config) => {
232
89
  if (nameNode?.content) update.name = nameNode.content.toString()
233
90
  const descriptionNode = getBinaryNodeChild(settingsNode, "description")
234
91
  if (descriptionNode?.content) update.description = descriptionNode.content.toString()
235
- ev.emit("newsletter-settings.update", {
236
- id: from,
237
- update,
238
- })
92
+ ev.emit("newsletter-settings.update", { id: from, update })
239
93
  }
240
94
  break
241
95
  case "message":
242
96
  const plaintextNode = getBinaryNodeChild(child, "plaintext")
243
97
  if (plaintextNode?.content) {
244
98
  try {
245
- const contentBuf =
246
- typeof plaintextNode.content === "string"
247
- ? Buffer.from(plaintextNode.content, "binary")
248
- : Buffer.from(plaintextNode.content)
99
+ const contentBuf = typeof plaintextNode.content === "string" ? Buffer.from(plaintextNode.content, "binary") : Buffer.from(plaintextNode.content)
249
100
  const messageProto = proto.Message.decode(contentBuf).toJSON()
250
- const fullMessage = proto.WebMessageInfo.fromObject({
251
- key: {
252
- remoteJid: from,
253
- id: child.attrs.message_id || child.attrs.server_id,
254
- fromMe: false, // TODO: is this really true though
255
- },
256
- message: messageProto,
257
- messageTimestamp: +child.attrs.t,
258
- }).toJSON()
101
+ const fullMessage = proto.WebMessageInfo.fromObject({ key: { remoteJid: from, id: child.attrs.message_id || child.attrs.server_id, fromMe: false }, message: messageProto, messageTimestamp: +child.attrs.t }).toJSON()
259
102
  await upsertMessage(fullMessage, "append")
260
103
  logger.info("Processed plaintext newsletter message")
261
- } catch (error) {
262
- logger.error({ error }, "Failed to decode plaintext newsletter message")
263
- }
104
+ } catch (error) { logger.error({ error }, "Failed to decode plaintext newsletter message") }
264
105
  }
265
106
  break
266
107
  default:
@@ -268,243 +109,116 @@ export const makeMessagesRecvSocket = (config) => {
268
109
  break
269
110
  }
270
111
  }
112
+
271
113
  const sendMessageAck = async ({ tag, attrs, content }, errorCode) => {
272
- const stanza = {
273
- tag: "ack",
274
- attrs: {
275
- id: attrs.id,
276
- to: attrs.from,
277
- class: tag,
278
- },
279
- }
280
- if (!!errorCode) {
281
- stanza.attrs.error = errorCode.toString()
282
- }
283
- if (!!attrs.participant) {
284
- stanza.attrs.participant = attrs.participant
285
- }
286
- if (!!attrs.recipient) {
287
- stanza.attrs.recipient = attrs.recipient
288
- }
289
- if (
290
- !!attrs.type &&
291
- (tag !== "message" || getBinaryNodeChild({ tag, attrs, content }, "unavailable") || errorCode !== 0)
292
- ) {
293
- stanza.attrs.type = attrs.type
294
- }
295
- if (tag === "message" && getBinaryNodeChild({ tag, attrs, content }, "unavailable")) {
296
- stanza.attrs.from = authState.creds.me.id
297
- }
114
+ const stanza = { tag: "ack", attrs: { id: attrs.id, to: attrs.from, class: tag } }
115
+ if (!!errorCode) stanza.attrs.error = errorCode.toString()
116
+ if (!!attrs.participant) stanza.attrs.participant = attrs.participant
117
+ if (!!attrs.recipient) stanza.attrs.recipient = attrs.recipient
118
+ if (!!attrs.type && (tag !== "message" || getBinaryNodeChild({ tag, attrs, content }, "unavailable") || errorCode !== 0)) stanza.attrs.type = attrs.type
119
+ if (tag === "message" && getBinaryNodeChild({ tag, attrs, content }, "unavailable")) stanza.attrs.from = authState.creds.me.id
298
120
  logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, "sent ack")
299
121
  try {
300
122
  await sendNode(stanza)
301
123
  } catch (error) {
302
- // Handle connection closed errors gracefully
303
- // Don't crash if ACK fails - the message was already received
304
- if (error?.output?.statusCode === 428 || error?.message?.includes("Connection")) {
305
- logger.warn(
306
- { id: attrs.id, error: error?.message },
307
- "Failed to send ACK (connection closed) - message already received",
308
- )
309
- // Silently continue instead of throwing
310
- } else {
311
- // Re-throw other errors
312
- throw error
313
- }
124
+ if (error?.output?.statusCode === 428 || error?.message?.includes("Connection")) logger.warn({ id: attrs.id, error: error?.message }, "Failed to send ACK (connection closed) - message already received")
125
+ else throw error
314
126
  }
315
127
  }
128
+
316
129
  const rejectCall = async (callId, callFrom) => {
317
- const stanza = {
318
- tag: "call",
319
- attrs: {
320
- from: authState.creds.me.id,
321
- to: callFrom,
322
- },
323
- content: [
324
- {
325
- tag: "reject",
326
- attrs: {
327
- "call-id": callId,
328
- "call-creator": callFrom,
329
- count: "0",
330
- },
331
- content: undefined,
332
- },
333
- ],
334
- }
130
+ const stanza = { tag: "call", attrs: { from: authState.creds.me.id, to: callFrom }, content: [{ tag: "reject", attrs: { "call-id": callId, "call-creator": callFrom, count: "0" }, content: undefined }] }
335
131
  await query(stanza)
336
132
  }
133
+
337
134
  const sendRetryRequest = async (node, forceIncludeKeys = false) => {
338
- const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || "")
339
- const { key: msgKey } = fullMessage
340
- const msgId = msgKey.id
341
-
342
- if (messageRetryManager) {
343
- if (messageRetryManager.hasExceededMaxRetries(msgId)) {
344
- logger.debug({ msgId }, "reached retry limit with new retry manager, clearing")
345
- messageRetryManager.markRetryFailed(msgId)
346
- return
135
+ const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || "")
136
+ const { key: msgKey } = fullMessage
137
+ const msgId = msgKey.id
138
+ if (messageRetryManager) {
139
+ if (messageRetryManager.hasExceededMaxRetries(msgId)) { logger.debug({ msgId }, "reached retry limit with new retry manager, clearing"); messageRetryManager.markRetryFailed(msgId); return }
140
+ const retryCount = messageRetryManager.incrementRetryCount(msgId)
141
+ const key = `${msgId}:${msgKey?.participant}`
142
+ await msgRetryCache.set(key, retryCount)
143
+ } else {
144
+ const key = `${msgId}:${msgKey?.participant}`
145
+ let retryCount = (await msgRetryCache.get(key)) || 0
146
+ if (retryCount >= maxMsgRetryCount) { logger.debug({ retryCount, msgId }, "reached retry limit, clearing"); await msgRetryCache.del(key); return }
147
+ retryCount += 1
148
+ await msgRetryCache.set(key, retryCount)
347
149
  }
348
- const retryCount = messageRetryManager.incrementRetryCount(msgId)
349
- const key = `${msgId}:${msgKey?.participant}`
350
- msgRetryCache.set(key, retryCount)
351
- } else {
352
150
  const key = `${msgId}:${msgKey?.participant}`
353
- let retryCount = (await msgRetryCache.get(key)) || 0
354
- if (retryCount >= maxMsgRetryCount) {
355
- logger.debug({ retryCount, msgId }, "reached retry limit, clearing")
356
- msgRetryCache.del(key)
357
- return
358
- }
359
- retryCount += 1
360
- await msgRetryCache.set(key, retryCount)
361
- }
362
-
363
- const key = `${msgId}:${msgKey?.participant}`
364
- const retryCount = (await msgRetryCache.get(key)) || 1
365
- const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
366
- const fromJid = node.attrs.from
367
-
368
- // ENHANCED: Check both PN and potential LID for session
369
- let shouldRecreateSession = false
370
- let recreateReason = ""
371
-
372
- if (enableAutoSessionRecreation && messageRetryManager) {
373
- try {
374
- const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
375
- const hasSession = await signalRepository.validateSession(fromJid)
376
-
377
- // Also check if LID mapping exists and session should be under LID
378
- let lidJid = null
379
- if (isPnUser(fromJid)) {
380
- lidJid = await signalRepository.lidMapping.getLIDForPN(fromJid)
381
- }
382
-
383
- const result = messageRetryManager.shouldRecreateSession(fromJid, retryCount, hasSession.exists)
384
- shouldRecreateSession = result.recreate
385
- recreateReason = result.reason
386
-
387
- if (shouldRecreateSession) {
388
- logger.debug({ fromJid, lidJid, retryCount, reason: recreateReason }, "recreating session for retry")
389
-
390
- // Delete both PN and LID sessions to force clean rebuild
391
- await authState.keys.set({ session: { [sessionId]: null } })
392
- if (lidJid) {
393
- const lidSessionId = signalRepository.jidToSignalProtocolAddress(lidJid)
394
- await authState.keys.set({ session: { [lidSessionId]: null } })
151
+ const retryCount = (await msgRetryCache.get(key)) || 1
152
+ const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
153
+ const fromJid = node.attrs.from
154
+ let shouldRecreateSession = false
155
+ let recreateReason = ""
156
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
157
+ try {
158
+ const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
159
+ const hasSession = await signalRepository.validateSession(fromJid)
160
+ let lidJid = null
161
+ if (isPnUser(fromJid)) lidJid = await signalRepository.lidMapping.getLIDForPN(fromJid)
162
+ const result = messageRetryManager.shouldRecreateSession(fromJid, retryCount, hasSession.exists)
163
+ shouldRecreateSession = result.recreate
164
+ recreateReason = result.reason
165
+ if (shouldRecreateSession) {
166
+ logger.debug({ fromJid, lidJid, retryCount, reason: recreateReason }, "recreating session for retry")
167
+ await authState.keys.set({ session: { [sessionId]: null } })
168
+ if (lidJid) {
169
+ const lidSessionId = signalRepository.jidToSignalProtocolAddress(lidJid)
170
+ await authState.keys.set({ session: { [lidSessionId]: null } })
171
+ }
172
+ forceIncludeKeys = true
395
173
  }
396
-
397
- forceIncludeKeys = true
398
- }
399
- } catch (error) {
400
- logger.warn({ error, fromJid }, "failed to check session recreation")
174
+ } catch (error) { logger.warn({ error, fromJid }, "failed to check session recreation") }
401
175
  }
402
- }
403
-
404
- // Rest of the function remains the same...
405
- if (retryCount <= 2) {
406
- if (messageRetryManager) {
407
- messageRetryManager.schedulePhoneRequest(msgId, async () => {
408
- try {
409
- const requestId = await requestPlaceholderResend(msgKey)
410
- logger.debug(
411
- `sendRetryRequest: requested placeholder resend (${requestId}) for message ${msgId} (scheduled)`,
412
- )
413
- } catch (error) {
414
- logger.warn({ error, msgId }, "failed to send scheduled phone request")
415
- }
416
- })
417
- } else {
418
- const msgId = await requestPlaceholderResend(msgKey)
419
- logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`)
176
+ if (retryCount <= 2) {
177
+ if (messageRetryManager) {
178
+ messageRetryManager.schedulePhoneRequest(msgId, async () => {
179
+ try {
180
+ const requestId = await requestPlaceholderResend(msgKey)
181
+ logger.debug(`sendRetryRequest: requested placeholder resend (${requestId}) for message ${msgId} (scheduled)`)
182
+ } catch (error) { logger.warn({ error, msgId }, "failed to send scheduled phone request") }
183
+ })
184
+ } else {
185
+ const msgId = await requestPlaceholderResend(msgKey)
186
+ logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`)
187
+ }
420
188
  }
189
+ const deviceIdentity = encodeSignedDeviceIdentity(account, true)
190
+ await authState.keys.transaction(async () => {
191
+ const receipt = { tag: "receipt", attrs: { id: msgId, type: "retry", to: node.attrs.from }, content: [{ tag: "retry", attrs: { count: retryCount.toString(), id: node.attrs.id, t: node.attrs.t, v: "1", error: "0" } }, { tag: "registration", attrs: {}, content: encodeBigEndian(authState.creds.registrationId) }] }
192
+ if (node.attrs.recipient) receipt.attrs.recipient = node.attrs.recipient
193
+ if (node.attrs.participant) receipt.attrs.participant = node.attrs.participant
194
+ if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
195
+ const { update, preKeys } = await getNextPreKeys(authState, 1)
196
+ const [keyId] = Object.keys(preKeys)
197
+ const key = preKeys[+keyId]
198
+ const content = receipt.content
199
+ content.push({ tag: "keys", attrs: {}, content: [{ tag: "type", attrs: {}, content: Buffer.from(KEY_BUNDLE_TYPE) }, { tag: "identity", attrs: {}, content: identityKey.public }, xmppPreKey(key, +keyId), xmppSignedPreKey(signedPreKey), { tag: "device-identity", attrs: {}, content: deviceIdentity }] })
200
+ ev.emit("creds.update", update)
201
+ }
202
+ await sendNode(receipt)
203
+ logger.info({ msgAttrs: node.attrs, retryCount }, "sent retry receipt")
204
+ }, authState?.creds?.me?.id || "sendRetryRequest")
421
205
  }
422
-
423
- const deviceIdentity = encodeSignedDeviceIdentity(account, true)
424
- await authState.keys.transaction(async () => {
425
- const receipt = {
426
- tag: "receipt",
427
- attrs: {
428
- id: msgId,
429
- type: "retry",
430
- to: node.attrs.from,
431
- },
432
- content: [
433
- {
434
- tag: "retry",
435
- attrs: {
436
- count: retryCount.toString(),
437
- id: node.attrs.id,
438
- t: node.attrs.t,
439
- v: "1",
440
- error: "0",
441
- },
442
- },
443
- {
444
- tag: "registration",
445
- attrs: {},
446
- content: encodeBigEndian(authState.creds.registrationId),
447
- },
448
- ],
449
- }
450
-
451
- if (node.attrs.recipient) {
452
- receipt.attrs.recipient = node.attrs.recipient
453
- }
454
- if (node.attrs.participant) {
455
- receipt.attrs.participant = node.attrs.participant
456
- }
457
-
458
- if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
459
- const { update, preKeys } = await getNextPreKeys(authState, 1)
460
- const [keyId] = Object.keys(preKeys)
461
- const key = preKeys[+keyId]
462
- const content = receipt.content
463
- content.push({
464
- tag: "keys",
465
- attrs: {},
466
- content: [
467
- { tag: "type", attrs: {}, content: Buffer.from(KEY_BUNDLE_TYPE) },
468
- { tag: "identity", attrs: {}, content: identityKey.public },
469
- xmppPreKey(key, +keyId),
470
- xmppSignedPreKey(signedPreKey),
471
- { tag: "device-identity", attrs: {}, content: deviceIdentity },
472
- ],
473
- })
474
- ev.emit("creds.update", update)
475
- }
476
-
477
- await sendNode(receipt)
478
- logger.info({ msgAttrs: node.attrs, retryCount }, "sent retry receipt")
479
- }, authState?.creds?.me?.id || "sendRetryRequest")
480
- }
481
206
 
482
207
  const handleEncryptNotification = async (node) => {
483
- const from = node.attrs.from
484
- if (from === S_WHATSAPP_NET) {
485
- const countChild = getBinaryNodeChild(node, "count")
486
- const count = +countChild.attrs.value
487
- const shouldUploadMorePreKeys = count < MIN_PREKEY_COUNT
488
- logger.debug({ count, shouldUploadMorePreKeys }, "recv pre-key count")
489
- if (shouldUploadMorePreKeys) {
490
- // Use debounced trigger if available, otherwise fallback to direct upload
491
- if (typeof triggerPreKeyCheck === 'function') {
492
- triggerPreKeyCheck("server-notification", "normal")
493
- } else {
494
- await uploadPreKeys(MIN_PREKEY_COUNT)
495
- }
496
- }
497
- } else {
498
- const identityNode = getBinaryNodeChild(node, "identity")
499
- if (identityNode) {
500
- logger.info({ jid: from }, "identity changed")
208
+ const from = node.attrs.from
209
+ if (from === S_WHATSAPP_NET) {
210
+ const countChild = getBinaryNodeChild(node, "count")
211
+ const count = +countChild.attrs.value
212
+ const shouldUploadMorePreKeys = count < MIN_PREKEY_COUNT
213
+ logger.debug({ count, shouldUploadMorePreKeys }, "recv pre-key count")
214
+ if (shouldUploadMorePreKeys) await uploadPreKeys()
501
215
  } else {
502
- logger.info({ node }, "unknown encrypt notification")
216
+ const result = await handleIdentityChange(node, { meId: authState.creds.me?.id, meLid: authState.creds.me?.lid, validateSession: signalRepository.validateSession, assertSessions, debounceCache: identityAssertDebounce, logger })
217
+ if (result.action === "no_identity_node") logger.info({ node }, "unknown encrypt notification")
503
218
  }
504
219
  }
505
- }
220
+
506
221
  const handleGroupNotification = (fullNode, child, msg) => {
507
- // TODO: Support PN/LID (Here is only LID now)
508
222
  const actingParticipantLid = fullNode.attrs.participant
509
223
  const actingParticipantPn = fullNode.attrs.participant_pn
510
224
  const affectedParticipantLid = getBinaryNodeChild(child, "participant")?.attrs?.jid || actingParticipantLid
@@ -515,29 +229,12 @@ export const makeMessagesRecvSocket = (config) => {
515
229
  msg.messageStubType = WAMessageStubType.GROUP_CREATE
516
230
  msg.messageStubParameters = [metadata.subject]
517
231
  msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn }
518
- ev.emit("chats.upsert", [
519
- {
520
- id: metadata.id,
521
- name: metadata.subject,
522
- conversationTimestamp: metadata.creation,
523
- },
524
- ])
525
- ev.emit("groups.upsert", [
526
- {
527
- ...metadata,
528
- author: actingParticipantLid,
529
- authorPn: actingParticipantPn,
530
- },
531
- ])
232
+ ev.emit("chats.upsert", [{ id: metadata.id, name: metadata.subject, conversationTimestamp: metadata.creation }])
233
+ ev.emit("groups.upsert", [{ ...metadata, author: actingParticipantLid, authorPn: actingParticipantPn }])
532
234
  break
533
235
  case "ephemeral":
534
236
  case "not_ephemeral":
535
- msg.message = {
536
- protocolMessage: {
537
- type: proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
538
- ephemeralExpiration: +(child.attrs.expiration || 0),
539
- },
540
- }
237
+ msg.message = { protocolMessage: { type: proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: +(child.attrs.expiration || 0) } }
541
238
  break
542
239
  case "modify":
543
240
  const oldNumber = getBinaryNodeChildren(child, "participant").map((p) => p.attrs.jid)
@@ -551,25 +248,8 @@ export const makeMessagesRecvSocket = (config) => {
551
248
  case "leave":
552
249
  const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`
553
250
  msg.messageStubType = WAMessageStubType[stubType]
554
- const participants = getBinaryNodeChildren(child, "participant").map(({ attrs }) => {
555
- // TODO: Store LID MAPPINGS
556
- return {
557
- id: attrs.jid,
558
- phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
559
- lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
560
- admin: attrs.type || null,
561
- }
562
- })
563
- if (
564
- participants.length === 1 &&
565
- // if recv. "remove" message and sender removed themselves
566
- // mark as left
567
- (areJidsSameUser(participants[0].id, actingParticipantLid) ||
568
- areJidsSameUser(participants[0].id, actingParticipantPn)) &&
569
- child.tag === "remove"
570
- ) {
571
- msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE
572
- }
251
+ const participants = getBinaryNodeChildren(child, "participant").map(({ attrs }) => ({ id: attrs.jid, phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined, lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined, admin: attrs.type || null }))
252
+ if (participants.length === 1 && (areJidsSameUser(participants[0].id, actingParticipantLid) || areJidsSameUser(participants[0].id, actingParticipantPn)) && child.tag === "remove") msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE
573
253
  msg.messageStubParameters = participants.map((a) => JSON.stringify(a))
574
254
  break
575
255
  case "subject":
@@ -597,37 +277,41 @@ export const makeMessagesRecvSocket = (config) => {
597
277
  break
598
278
  case "member_add_mode":
599
279
  const addMode = child.content
600
- if (addMode) {
601
- msg.messageStubType = WAMessageStubType.GROUP_MEMBER_ADD_MODE
602
- msg.messageStubParameters = [addMode.toString()]
603
- }
280
+ if (addMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBER_ADD_MODE; msg.messageStubParameters = [addMode.toString()] }
604
281
  break
605
282
  case "membership_approval_mode":
606
283
  const approvalMode = getBinaryNodeChild(child, "group_join")
607
- if (approvalMode) {
608
- msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE
609
- msg.messageStubParameters = [approvalMode.attrs.state]
610
- }
284
+ if (approvalMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE; msg.messageStubParameters = [approvalMode.attrs.state] }
611
285
  break
612
286
  case "created_membership_requests":
613
287
  msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
614
- msg.messageStubParameters = [
615
- JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
616
- "created",
617
- child.attrs.request_method,
618
- ]
288
+ msg.messageStubParameters = [JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }), "created", child.attrs.request_method]
619
289
  break
620
290
  case "revoked_membership_requests":
621
291
  const isDenied = areJidsSameUser(affectedParticipantLid, actingParticipantLid)
622
- // TODO: LIDMAPPING SUPPORT
623
292
  msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
624
- msg.messageStubParameters = [
625
- JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
626
- isDenied ? "revoked" : "rejected",
627
- ]
293
+ msg.messageStubParameters = [JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }), isDenied ? "revoked" : "rejected"]
628
294
  break
629
295
  }
630
296
  }
297
+
298
+ const handlePrivacyTokenNotification = async (node) => {
299
+ const tokensNode = getBinaryNodeChild(node, "tokens")
300
+ const from = jidNormalizedUser(node.attrs.from)
301
+ if (!tokensNode) return
302
+ const tokenNodes = getBinaryNodeChildren(tokensNode, "token")
303
+ for (const tokenNode of tokenNodes) {
304
+ const { attrs, content } = tokenNode
305
+ const type = attrs.type
306
+ const timestamp = attrs.t
307
+ if (type === "trusted_contact" && content instanceof Buffer) {
308
+ logger.debug({ from, timestamp, tcToken: content }, "received trusted contact token")
309
+ await authState.keys.set({ tctoken: { [from]: { token: content, timestamp } } })
310
+ ev.emit("chats.update", [{ id: from, tcToken: content }])
311
+ }
312
+ }
313
+ }
314
+
631
315
  const processNotification = async (node) => {
632
316
  const result = {}
633
317
  const [child] = getAllBinaryNodeChildren(node)
@@ -635,17 +319,7 @@ export const makeMessagesRecvSocket = (config) => {
635
319
  const from = jidNormalizedUser(node.attrs.from)
636
320
  switch (nodeType) {
637
321
  case "privacy_token":
638
- const tokenList = getBinaryNodeChildren(child, "token")
639
- for (const { attrs, content } of tokenList) {
640
- const jid = attrs.jid
641
- ev.emit("chats.update", [
642
- {
643
- id: jid,
644
- tcToken: content,
645
- },
646
- ])
647
- logger.debug({ jid }, "got privacy token update")
648
- }
322
+ await handlePrivacyTokenNotification(node)
649
323
  break
650
324
  case "newsletter":
651
325
  await handleNewsletterNotification(node)
@@ -654,7 +328,6 @@ export const makeMessagesRecvSocket = (config) => {
654
328
  await handleMexNewsletterNotification(node)
655
329
  break
656
330
  case "w:gp2":
657
- // TODO: HANDLE PARTICIPANT_PN
658
331
  handleGroupNotification(node, child, result)
659
332
  break
660
333
  case "mediaretry":
@@ -666,14 +339,10 @@ export const makeMessagesRecvSocket = (config) => {
666
339
  break
667
340
  case "devices":
668
341
  const devices = getBinaryNodeChildren(child, "device")
669
- if (
670
- areJidsSameUser(child.attrs.jid, authState.creds.me.id) ||
671
- areJidsSameUser(child.attrs.lid, authState.creds.me.lid)
672
- ) {
342
+ if (areJidsSameUser(child.attrs.jid, authState.creds.me.id) || areJidsSameUser(child.attrs.lid, authState.creds.me.lid)) {
673
343
  const deviceData = devices.map((d) => ({ id: d.attrs.jid, lid: d.attrs.lid }))
674
344
  logger.info({ deviceData }, "my own devices changed")
675
345
  }
676
- //TODO: drop a new event, add hashes
677
346
  break
678
347
  case "server_sync":
679
348
  const update = getBinaryNodeChild(node, "collection")
@@ -685,23 +354,13 @@ export const makeMessagesRecvSocket = (config) => {
685
354
  case "picture":
686
355
  const setPicture = getBinaryNodeChild(node, "set")
687
356
  const delPicture = getBinaryNodeChild(node, "delete")
688
- ev.emit("contacts.update", [
689
- {
690
- id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || "",
691
- imgUrl: setPicture ? "changed" : "removed",
692
- },
693
- ])
357
+ ev.emit("contacts.update", [{ id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || "", imgUrl: setPicture ? "changed" : "removed" }])
694
358
  if (isJidGroup(from)) {
695
359
  const node = setPicture || delPicture
696
360
  result.messageStubType = WAMessageStubType.GROUP_CHANGE_ICON
697
- if (setPicture) {
698
- result.messageStubParameters = [setPicture.attrs.id]
699
- }
361
+ if (setPicture) result.messageStubParameters = [setPicture.attrs.id]
700
362
  result.participant = node?.attrs.author
701
- result.key = {
702
- ...(result.key || {}),
703
- participant: setPicture?.attrs.author,
704
- }
363
+ result.key = { ...(result.key || {}), participant: setPicture?.attrs.author }
705
364
  }
706
365
  break
707
366
  case "account_sync":
@@ -709,15 +368,7 @@ export const makeMessagesRecvSocket = (config) => {
709
368
  const newDuration = +child.attrs.duration
710
369
  const timestamp = +child.attrs.t
711
370
  logger.info({ newDuration }, "updated account disappearing mode")
712
- ev.emit("creds.update", {
713
- accountSettings: {
714
- ...authState.creds.accountSettings,
715
- defaultDisappearingMode: {
716
- ephemeralExpiration: newDuration,
717
- ephemeralSettingTimestamp: timestamp,
718
- },
719
- },
720
- })
371
+ ev.emit("creds.update", { accountSettings: { ...authState.creds.accountSettings, defaultDisappearingMode: { ephemeralExpiration: newDuration, ephemeralSettingTimestamp: timestamp } } })
721
372
  } else if (child.tag === "blocklist") {
722
373
  const blocklists = getBinaryNodeChildren(child, "item")
723
374
  for (const { attrs } of blocklists) {
@@ -730,76 +381,28 @@ export const makeMessagesRecvSocket = (config) => {
730
381
  case "link_code_companion_reg":
731
382
  const linkCodeCompanionReg = getBinaryNodeChild(node, "link_code_companion_reg")
732
383
  const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, "link_code_pairing_ref"))
733
- const primaryIdentityPublicKey = toRequiredBuffer(
734
- getBinaryNodeChildBuffer(linkCodeCompanionReg, "primary_identity_pub"),
735
- )
736
- const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(
737
- getBinaryNodeChildBuffer(linkCodeCompanionReg, "link_code_pairing_wrapped_primary_ephemeral_pub"),
738
- )
384
+ const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, "primary_identity_pub"))
385
+ const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, "link_code_pairing_wrapped_primary_ephemeral_pub"))
739
386
  const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped)
740
- const companionSharedKey = Curve.sharedKey(
741
- authState.creds.pairingEphemeralKeyPair.private,
742
- codePairingPublicKey,
743
- )
387
+ const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey)
744
388
  const random = randomBytes(32)
745
389
  const linkCodeSalt = randomBytes(32)
746
- const linkCodePairingExpanded = await hkdf(companionSharedKey, 32, {
747
- salt: linkCodeSalt,
748
- info: "link_code_pairing_key_bundle_encryption_key",
749
- })
750
- const encryptPayload = Buffer.concat([
751
- Buffer.from(authState.creds.signedIdentityKey.public),
752
- primaryIdentityPublicKey,
753
- random,
754
- ])
390
+ const linkCodePairingExpanded = hkdf(companionSharedKey, 32, { salt: linkCodeSalt, info: "link_code_pairing_key_bundle_encryption_key" })
391
+ const encryptPayload = Buffer.concat([Buffer.from(authState.creds.signedIdentityKey.public), primaryIdentityPublicKey, random])
755
392
  const encryptIv = randomBytes(12)
756
393
  const encrypted = aesEncryptGCM(encryptPayload, linkCodePairingExpanded, encryptIv, Buffer.alloc(0))
757
394
  const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted])
758
395
  const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey)
759
396
  const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random])
760
- authState.creds.advSecretKey = (await hkdf(identityPayload, 32, { info: "adv_secret" })).toString("base64")
761
- await query({
762
- tag: "iq",
763
- attrs: {
764
- to: S_WHATSAPP_NET,
765
- type: "set",
766
- id: sock.generateMessageTag(),
767
- xmlns: "md",
768
- },
769
- content: [
770
- {
771
- tag: "link_code_companion_reg",
772
- attrs: {
773
- jid: authState.creds.me.id,
774
- stage: "companion_finish",
775
- },
776
- content: [
777
- {
778
- tag: "link_code_pairing_wrapped_key_bundle",
779
- attrs: {},
780
- content: encryptedPayload,
781
- },
782
- {
783
- tag: "companion_identity_public",
784
- attrs: {},
785
- content: authState.creds.signedIdentityKey.public,
786
- },
787
- {
788
- tag: "link_code_pairing_ref",
789
- attrs: {},
790
- content: ref,
791
- },
792
- ],
793
- },
794
- ],
795
- })
397
+ authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: "adv_secret" })).toString("base64")
398
+ await query({ tag: "iq", attrs: { to: S_WHATSAPP_NET, type: "set", id: sock.generateMessageTag(), xmlns: "md" }, content: [{ tag: "link_code_companion_reg", attrs: { jid: authState.creds.me.id, stage: "companion_finish" }, content: [{ tag: "link_code_pairing_wrapped_key_bundle", attrs: {}, content: encryptedPayload }, { tag: "companion_identity_public", attrs: {}, content: authState.creds.signedIdentityKey.public }, { tag: "link_code_pairing_ref", attrs: {}, content: ref }] }] })
796
399
  authState.creds.registered = true
797
400
  ev.emit("creds.update", authState.creds)
401
+ break
798
402
  }
799
- if (Object.keys(result).length) {
800
- return result
801
- }
403
+ if (Object.keys(result).length) return result
802
404
  }
405
+
803
406
  async function decipherLinkPublicKey(data) {
804
407
  const buffer = toRequiredBuffer(data)
805
408
  const salt = buffer.slice(0, 32)
@@ -808,490 +411,285 @@ export const makeMessagesRecvSocket = (config) => {
808
411
  const payload = buffer.slice(48, 80)
809
412
  return aesDecryptCTR(payload, secretKey, iv)
810
413
  }
414
+
811
415
  function toRequiredBuffer(data) {
812
- if (data === undefined) {
813
- throw new Boom("Invalid buffer", { statusCode: 400 })
814
- }
416
+ if (data === undefined) throw new Boom("Invalid buffer", { statusCode: 400 })
815
417
  return data instanceof Buffer ? data : Buffer.from(data)
816
418
  }
419
+
817
420
  const willSendMessageAgain = async (id, participant) => {
818
421
  const key = `${id}:${participant}`
819
422
  const retryCount = (await msgRetryCache.get(key)) || 0
820
423
  return retryCount < maxMsgRetryCount
821
424
  }
425
+
822
426
  const updateSendMessageAgainCount = async (id, participant) => {
823
427
  const key = `${id}:${participant}`
824
428
  const newValue = ((await msgRetryCache.get(key)) || 0) + 1
825
429
  await msgRetryCache.set(key, newValue)
826
430
  }
431
+
827
432
  const sendMessagesAgain = async (key, ids, retryNode) => {
828
433
  const remoteJid = key.remoteJid
829
434
  const participant = key.participant || remoteJid
830
435
  const retryCount = +retryNode.attrs.count || 1
831
- // Try to get messages from cache first, then fallback to getMessage
832
436
  const msgs = []
833
437
  for (const id of ids) {
834
438
  let msg
835
- // Try to get from retry cache first if enabled
836
439
  if (messageRetryManager) {
837
440
  const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id)
838
- if (cachedMsg) {
839
- msg = cachedMsg.message
840
- logger.debug({ jid: remoteJid, id }, "found message in retry cache")
841
- // Mark retry as successful since we found the message
842
- messageRetryManager.markRetrySuccess(id)
843
- }
441
+ if (cachedMsg) { msg = cachedMsg.message; logger.debug({ jid: remoteJid, id }, "found message in retry cache"); messageRetryManager.markRetrySuccess(id) }
844
442
  }
845
- // Fallback to getMessage if not found in cache
846
443
  if (!msg) {
847
444
  msg = await getMessage({ ...key, id })
848
- if (msg) {
849
- logger.debug({ jid: remoteJid, id }, "found message via getMessage")
850
- // Also mark as successful if found via getMessage
851
- if (messageRetryManager) {
852
- messageRetryManager.markRetrySuccess(id)
853
- }
854
- }
445
+ if (msg) { logger.debug({ jid: remoteJid, id }, "found message via getMessage"); if (messageRetryManager) messageRetryManager.markRetrySuccess(id) }
855
446
  }
856
447
  msgs.push(msg)
857
448
  }
858
- // if it's the primary jid sending the request
859
- // just re-send the message to everyone
860
- // prevents the first message decryption failure
861
449
  const sendToAll = !jidDecode(participant)?.device
862
- // Check if we should recreate session for this retry
863
450
  let shouldRecreateSession = false
864
451
  let recreateReason = ""
865
- if (enableAutoSessionRecreation && messageRetryManager) {
452
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
866
453
  try {
867
454
  const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
868
455
  const hasSession = await signalRepository.validateSession(participant)
869
456
  const result = messageRetryManager.shouldRecreateSession(participant, retryCount, hasSession.exists)
870
457
  shouldRecreateSession = result.recreate
871
458
  recreateReason = result.reason
872
- if (shouldRecreateSession) {
873
- logger.debug({ participant, retryCount, reason: recreateReason }, "recreating session for outgoing retry")
874
- await authState.keys.set({ session: { [sessionId]: null } })
875
- }
876
- } catch (error) {
877
- logger.warn({ error, participant }, "failed to check session recreation for outgoing retry")
878
- }
879
- }
880
- await assertSessions([participant])
881
- if (isJidGroup(remoteJid)) {
882
- await authState.keys.set({ "sender-key-memory": { [remoteJid]: null } })
459
+ if (shouldRecreateSession) { logger.debug({ participant, retryCount, reason: recreateReason }, "recreating session for outgoing retry"); await authState.keys.set({ session: { [sessionId]: null } }) }
460
+ } catch (error) { logger.warn({ error, participant }, "failed to check session recreation for outgoing retry") }
883
461
  }
462
+ await assertSessions([participant], true)
463
+ if (isJidGroup(remoteJid)) await authState.keys.set({ "sender-key-memory": { [remoteJid]: null } })
884
464
  logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason }, "forced new session for retry recp")
885
465
  for (const [i, msg] of msgs.entries()) {
886
466
  if (!ids[i]) continue
887
467
  if (msg && (await willSendMessageAgain(ids[i], participant))) {
888
- updateSendMessageAgainCount(ids[i], participant)
468
+ await updateSendMessageAgainCount(ids[i], participant)
889
469
  const msgRelayOpts = { messageId: ids[i] }
890
- if (sendToAll) {
891
- msgRelayOpts.useUserDevicesCache = false
892
- } else {
893
- msgRelayOpts.participant = {
894
- jid: participant,
895
- count: +retryNode.attrs.count,
896
- }
897
- }
470
+ if (sendToAll) msgRelayOpts.useUserDevicesCache = false
471
+ else msgRelayOpts.participant = { jid: participant, count: +retryNode.attrs.count }
898
472
  await relayMessage(key.remoteJid, msg, msgRelayOpts)
899
- } else {
900
- logger.debug({ jid: key.remoteJid, id: ids[i] }, "recv retry request, but message not available")
901
- }
473
+ } else logger.debug({ jid: key.remoteJid, id: ids[i] }, "recv retry request, but message not available")
902
474
  }
903
475
  }
476
+
904
477
  const handleReceipt = async (node) => {
905
478
  const { attrs, content } = node
906
479
  const isLid = attrs.from.includes("lid")
907
- const isNodeFromMe = areJidsSameUser(
908
- attrs.participant || attrs.from,
909
- isLid ? authState.creds.me?.lid : authState.creds.me?.id,
910
- )
480
+ const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id)
911
481
  const remoteJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient
912
482
  const fromMe = !attrs.recipient || ((attrs.type === "retry" || attrs.type === "sender") && isNodeFromMe)
913
- const key = {
914
- remoteJid,
915
- id: "",
916
- fromMe,
917
- participant: attrs.participant,
918
- }
919
- if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
920
- logger.debug({ remoteJid }, "ignoring receipt from jid")
921
- await sendMessageAck(node)
922
- return
923
- }
483
+ const key = { remoteJid, id: "", fromMe, participant: attrs.participant }
484
+ if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) { logger.debug({ remoteJid }, "ignoring receipt from jid"); await sendMessageAck(node); return }
924
485
  const ids = [attrs.id]
925
486
  if (Array.isArray(content)) {
926
487
  const items = getBinaryNodeChildren(content[0], "item")
927
488
  ids.push(...items.map((i) => i.attrs.id))
928
489
  }
929
490
  try {
930
- await Promise.all([
931
- processingMutex.mutex(async () => {
932
- const status = getStatusFromReceiptType(attrs.type)
933
- if (
934
- typeof status !== "undefined" &&
935
- // basically, we only want to know when a message from us has been delivered to/read by the other person
936
- // or another device of ours has read some messages
937
- (status >= proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)
938
- ) {
939
- if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
940
- if (attrs.participant) {
941
- const updateKey =
942
- status === proto.WebMessageInfo.Status.DELIVERY_ACK ? "receiptTimestamp" : "readTimestamp"
943
- ev.emit(
944
- "message-receipt.update",
945
- ids.map((id) => ({
946
- key: { ...key, id },
947
- receipt: {
948
- userJid: jidNormalizedUser(attrs.participant),
949
- [updateKey]: +attrs.t,
950
- },
951
- })),
952
- )
953
- }
954
- } else {
955
- ev.emit(
956
- "messages.update",
957
- ids.map((id) => ({
958
- key: { ...key, id },
959
- update: { status },
960
- })),
961
- )
962
- }
963
- }
964
- if (attrs.type === "retry") {
965
- // correctly set who is asking for the retry
966
- key.participant = key.participant || attrs.from
967
- const retryNode = getBinaryNodeChild(node, "retry")
968
- if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
969
- if (key.fromMe) {
970
- try {
971
- updateSendMessageAgainCount(ids[0], key.participant)
972
- logger.debug({ attrs, key }, "recv retry request")
973
- await sendMessagesAgain(key, ids, retryNode)
974
- } catch (error) {
975
- logger.error(
976
- { key, ids, trace: error instanceof Error ? error.stack : "Unknown error" },
977
- "error in sending message again",
978
- )
979
- }
980
- } else {
981
- logger.info({ attrs, key }, "recv retry for not fromMe message")
982
- }
983
- } else {
984
- logger.info({ attrs, key }, "will not send message again, as sent too many times")
491
+ await Promise.all([receiptMutex.mutex(async () => {
492
+ const status = getStatusFromReceiptType(attrs.type)
493
+ if (typeof status !== "undefined" && (status >= proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)) {
494
+ if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
495
+ if (attrs.participant) {
496
+ const updateKey = status === proto.WebMessageInfo.Status.DELIVERY_ACK ? "receiptTimestamp" : "readTimestamp"
497
+ ev.emit("message-receipt.update", ids.map((id) => ({ key: { ...key, id }, receipt: { userJid: jidNormalizedUser(attrs.participant), [updateKey]: +attrs.t } })))
985
498
  }
986
- }
987
- }),
988
- ])
499
+ } else ev.emit("messages.update", ids.map((id) => ({ key: { ...key, id }, update: { status, messageTimestamp: toNumber(+(attrs.t ?? 0)) } })))
500
+ }
501
+ if (attrs.type === "retry") {
502
+ key.participant = key.participant || attrs.from
503
+ const retryNode = getBinaryNodeChild(node, "retry")
504
+ if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
505
+ if (key.fromMe) {
506
+ try {
507
+ await updateSendMessageAgainCount(ids[0], key.participant)
508
+ logger.debug({ attrs, key }, "recv retry request")
509
+ await sendMessagesAgain(key, ids, retryNode)
510
+ } catch (error) { logger.error({ key, ids, trace: error instanceof Error ? error.stack : "Unknown error" }, "error in sending message again") }
511
+ } else logger.info({ attrs, key }, "recv retry for not fromMe message")
512
+ } else logger.info({ attrs, key }, "will not send message again, as sent too many times")
513
+ }
514
+ })])
989
515
  } finally {
990
516
  await sendMessageAck(node)
991
517
  }
992
518
  }
519
+
993
520
  const handleNotification = async (node) => {
994
521
  const remoteJid = node.attrs.from
995
- if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
996
- logger.debug({ remoteJid, id: node.attrs.id }, "ignored notification")
997
- await sendMessageAck(node)
998
- return
999
- }
522
+ if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) { logger.debug({ remoteJid, id: node.attrs.id }, "ignored notification"); await sendMessageAck(node); return }
1000
523
  try {
1001
- await Promise.all([
1002
- processingMutex.mutex(async () => {
1003
- const msg = await processNotification(node)
1004
- if (msg) {
1005
- const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id)
1006
- const { senderAlt: participantAlt, addressingMode } = extractAddressingContext(node)
1007
- msg.key = {
1008
- remoteJid,
1009
- fromMe,
1010
- participant: node.attrs.participant,
1011
- participantAlt,
1012
- addressingMode,
1013
- id: node.attrs.id,
1014
- ...(msg.key || {}),
1015
- }
1016
- msg.participant ?? (msg.participant = node.attrs.participant)
1017
- msg.messageTimestamp = +node.attrs.t
1018
- const fullMsg = proto.WebMessageInfo.fromObject(msg)
1019
- await upsertMessage(fullMsg, "append")
1020
- }
1021
- }),
1022
- ])
524
+ await Promise.all([notificationMutex.mutex(async () => {
525
+ const msg = await processNotification(node)
526
+ if (msg) {
527
+ const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id)
528
+ const { senderAlt: participantAlt, addressingMode } = extractAddressingContext(node)
529
+ msg.key = { remoteJid, fromMe, participant: node.attrs.participant, participantAlt, addressingMode, id: node.attrs.id, ...(msg.key || {}) }
530
+ msg.participant ?? (msg.participant = node.attrs.participant)
531
+ msg.messageTimestamp = +node.attrs.t
532
+ const fullMsg = proto.WebMessageInfo.fromObject(msg)
533
+ await upsertMessage(fullMsg, "append")
534
+ }
535
+ })])
1023
536
  } finally {
1024
537
  await sendMessageAck(node)
1025
538
  }
1026
539
  }
1027
540
 
1028
541
  const handleMessage = async (node) => {
1029
- if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== S_WHATSAPP_NET) {
1030
- logger.debug({ key: node.attrs.key }, "ignored message")
1031
- await sendMessageAck(node, NACK_REASONS.UnhandledError)
1032
- return
1033
- }
1034
-
1035
- const encNode = getBinaryNodeChild(node, "enc")
1036
- // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
1037
- if (encNode && encNode.attrs.type === "msmsg") {
1038
- logger.debug({ key: node.attrs.key }, "ignored msmsg")
1039
- await sendMessageAck(node, NACK_REASONS.MissingMessageSecret)
1040
- return
1041
- }
1042
-
1043
- const {
1044
- fullMessage: msg,
1045
- category,
1046
- author,
1047
- decrypt,
1048
- } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || "", signalRepository, logger)
1049
-
1050
- const alt = msg.key.participantAlt || msg.key.remoteJidAlt
1051
- // store new mappings we didn't have before
1052
- if (!!alt) {
1053
- const altServer = jidDecode(alt)?.server
1054
- const primaryJid = msg.key.participant || msg.key.remoteJid
1055
- if (altServer === "lid") {
1056
- if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
1057
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }])
1058
- await signalRepository.migrateSession(primaryJid, alt)
1059
- }
1060
- } else {
1061
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }])
1062
- await signalRepository.migrateSession(alt, primaryJid)
1063
- }
1064
- }
1065
-
1066
- if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
1067
- messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message)
1068
- logger.debug(
1069
- {
1070
- jid: msg.key.remoteJid,
1071
- id: msg.key.id,
1072
- },
1073
- "Added message to recent cache for retry receipts",
1074
- )
1075
- }
1076
-
1077
- try {
1078
- await processingMutex.mutex(async () => {
1079
- await decrypt()
1080
-
1081
- // message failed to decrypt
1082
- if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
1083
- if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
1084
- return sendMessageAck(node, NACK_REASONS.ParsingError)
1085
- }
1086
-
1087
- const errorMessage = msg?.messageStubParameters?.[0] || ""
1088
- const isPreKeyError = errorMessage.includes("PreKey")
1089
- const isBadMacError = errorMessage.includes("Bad MAC")
1090
- const isMessageCounterError = errorMessage.includes("Key used already or never filled")
1091
-
1092
- logger.debug(`[handleMessage] Failed decryption - PreKey: ${isPreKeyError}, BadMAC: ${isBadMacError}`)
1093
-
1094
- // IMMEDIATE SESSION RESET FOR BAD MAC - Don't wait for retry
1095
- if (isBadMacError || isMessageCounterError) {
1096
- const jidToReset = msg.key.participant || msg.key.remoteJid
1097
- logger.error({ jid: jidToReset, error: errorMessage },
1098
- "BAD MAC ERROR - Corrupted session detected, initiating emergency recovery")
1099
-
1100
- // Execute recovery immediately (not in retry mutex)
1101
- Promise.resolve().then(async () => {
1102
- try {
1103
- // 1. Delete corrupted session NOW
1104
- await signalRepository.deleteSession([jidToReset])
1105
- logger.info({ jid: jidToReset }, "✓ Deleted corrupted session")
1106
-
1107
- // 2. Force upload fresh pre-keys NOW
1108
- await uploadPreKeys(MIN_PREKEY_COUNT)
1109
- logger.info("✓ Uploaded fresh pre-keys")
1110
-
1111
- // 3. Small delay for server sync
1112
- await delay(500)
1113
-
1114
- logger.info({ jid: jidToReset }, "✓ Emergency recovery complete - ready for retry")
1115
- } catch (recoveryErr) {
1116
- logger.error({ err: recoveryErr, jid: jidToReset }, "✗ Emergency recovery failed")
542
+ if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== S_WHATSAPP_NET) { logger.debug({ key: node.attrs.key }, "ignored message"); await sendMessageAck(node, NACK_REASONS.UnhandledError); return }
543
+ const encNode = getBinaryNodeChild(node, "enc")
544
+ if (encNode && encNode.attrs.type === "msmsg") { logger.debug({ key: node.attrs.key }, "ignored msmsg"); await sendMessageAck(node, NACK_REASONS.MissingMessageSecret); return }
545
+ const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || "", signalRepository, logger)
546
+ const alt = msg.key.participantAlt || msg.key.remoteJidAlt
547
+ if (!!alt) {
548
+ const altServer = jidDecode(alt)?.server
549
+ const primaryJid = msg.key.participant || msg.key.remoteJid
550
+ if (altServer === "lid") { if (!(await signalRepository.lidMapping.getPNForLID(alt))) { await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]); await signalRepository.migrateSession(primaryJid, alt) } }
551
+ else { await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]); await signalRepository.migrateSession(alt, primaryJid) }
552
+ }
553
+ if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) { messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message); logger.debug({ jid: msg.key.remoteJid, id: msg.key.id }, "Added message to recent cache for retry receipts") }
554
+ try {
555
+ await messageMutex.mutex(async () => {
556
+ await decrypt()
557
+ if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== "peer") {
558
+ if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) return sendMessageAck(node, NACK_REASONS.ParsingError)
559
+ if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
560
+ const unavailableNode = getBinaryNodeChild(node, "unavailable")
561
+ const unavailableType = unavailableNode?.attrs?.type
562
+ if (unavailableType === "bot_unavailable_fanout" || unavailableType === "hosted_unavailable_fanout" || unavailableType === "view_once_unavailable_fanout") { logger.debug({ msgId: msg.key.id, unavailableType }, "skipping placeholder resend for excluded unavailable type"); return sendMessageAck(node) }
563
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp)
564
+ if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) { logger.debug({ msgId: msg.key.id, messageAge }, "skipping placeholder resend for old message"); return sendMessageAck(node) }
565
+ const cleanKey = { remoteJid: msg.key.remoteJid, fromMe: msg.key.fromMe, id: msg.key.id, participant: msg.key.participant }
566
+ const msgData = { key: msg.key, messageTimestamp: msg.messageTimestamp, pushName: msg.pushName, participant: msg.participant, verifiedBizName: msg.verifiedBizName }
567
+ requestPlaceholderResend(cleanKey, msgData).then(requestId => { if (requestId && requestId !== "RESOLVED") { logger.debug({ msgId: msg.key.id, requestId }, "requested placeholder resend for unavailable message"); ev.emit("messages.update", [{ key: msg.key, update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] } }]) } }).catch(err => { logger.warn({ err, msgId: msg.key.id }, "failed to request placeholder resend for unavailable message") })
568
+ await sendMessageAck(node)
569
+ } else {
570
+ if (isJidStatusBroadcast(msg.key.remoteJid)) {
571
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp)
572
+ if (messageAge > STATUS_EXPIRY_SECONDS) { logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, "skipping retry for expired status message"); return sendMessageAck(node) }
1117
573
  }
1118
- }).catch(err => logger.error({ err }, "Recovery promise failed"))
1119
- }
1120
-
1121
- // Then proceed with normal retry mechanism
1122
- retryMutex.mutex(async () => {
1123
- try {
1124
- if (!ws.isOpen) {
1125
- logger.debug({ node }, "Connection closed, skipping retry")
1126
- return
574
+ const errorMessage = msg?.messageStubParameters?.[0] || ""
575
+ const isPreKeyError = errorMessage.includes("PreKey")
576
+ const isBadMacError = errorMessage.includes("Bad MAC")
577
+ const isMessageCounterError = errorMessage.includes("Key used already or never filled")
578
+ logger.debug(`[handleMessage] Failed decryption - PreKey: ${isPreKeyError}, BadMAC: ${isBadMacError}`)
579
+ if (isBadMacError || isMessageCounterError) {
580
+ const jidToReset = msg.key.participant || msg.key.remoteJid
581
+ logger.error({ jid: jidToReset, error: errorMessage }, "BAD MAC ERROR - Corrupted session detected, initiating emergency recovery")
582
+ Promise.resolve().then(async () => {
583
+ try {
584
+ await signalRepository.deleteSession([jidToReset])
585
+ logger.info({ jid: jidToReset }, "✓ Deleted corrupted session")
586
+ await uploadPreKeys(MIN_PREKEY_COUNT)
587
+ logger.info("✓ Uploaded fresh pre-keys")
588
+ await delay(500)
589
+ logger.info({ jid: jidToReset }, "✓ Emergency recovery complete - ready for retry")
590
+ } catch (recoveryErr) { logger.error({ err: recoveryErr, jid: jidToReset }, "✗ Emergency recovery failed") }
591
+ }).catch(err => logger.error({ err }, "Recovery promise failed"))
1127
592
  }
1128
-
1129
- // Handle pre-key errors
1130
- if (isPreKeyError) {
1131
- logger.info({ error: errorMessage }, "PreKey error detected, uploading before retry")
593
+ retryMutex.mutex(async () => {
1132
594
  try {
1133
- await uploadPreKeys(MIN_PREKEY_COUNT)
1134
- await delay(1000)
1135
- } catch (uploadErr) {
1136
- logger.error({ uploadErr }, "Pre-key upload failed during retry")
595
+ if (!ws.isOpen) { logger.debug({ node }, "Connection closed, skipping retry"); return }
596
+ if (isPreKeyError) {
597
+ logger.info({ error: errorMessage }, "PreKey error detected, uploading before retry")
598
+ try {
599
+ await uploadPreKeys(MIN_PREKEY_COUNT)
600
+ await delay(1000)
601
+ } catch (uploadErr) { logger.error({ uploadErr }, "Pre-key upload failed during retry") }
602
+ }
603
+ const encNode = getBinaryNodeChild(node, "enc")
604
+ await sendRetryRequest(node, !encNode || isBadMacError || isMessageCounterError || isPreKeyError)
605
+ if (retryRequestDelayMs) await delay(retryRequestDelayMs)
606
+ } catch (err) {
607
+ logger.error({ err, isBadMacError, isPreKeyError }, "Retry mechanism failed")
608
+ try {
609
+ const encNode = getBinaryNodeChild(node, "enc")
610
+ await sendRetryRequest(node, !encNode)
611
+ } catch (retryErr) { logger.error({ retryErr }, "Final retry attempt failed") }
1137
612
  }
1138
- }
1139
-
1140
- const encNode = getBinaryNodeChild(node, "enc")
1141
- // Force include keys for Bad MAC or PreKey errors
1142
- await sendRetryRequest(node, !encNode || isBadMacError || isMessageCounterError || isPreKeyError)
1143
-
1144
- if (retryRequestDelayMs) {
1145
- await delay(retryRequestDelayMs)
1146
- }
1147
- } catch (err) {
1148
- logger.error({ err, isBadMacError, isPreKeyError }, "Retry mechanism failed")
1149
- try {
1150
- const encNode = getBinaryNodeChild(node, "enc")
1151
- await sendRetryRequest(node, !encNode)
1152
- } catch (retryErr) {
1153
- logger.error({ retryErr }, "Final retry attempt failed")
1154
- }
613
+ await sendMessageAck(node, NACK_REASONS.UnhandledError)
614
+ })
1155
615
  }
1156
-
1157
- await sendMessageAck(node, NACK_REASONS.UnhandledError)
1158
- })
1159
- } else {
1160
- // no type in the receipt => message delivered
1161
- let type = undefined
1162
- let participant = msg.key.participant
1163
- if (category === "peer") {
1164
- // special peer message
1165
- type = "peer_msg"
1166
- } else if (msg.key.fromMe) {
1167
- // message was sent by us from a different device
1168
- type = "sender"
1169
- // need to specially handle this case
1170
- if (isLidUser(msg.key.remoteJid) || isLidUser(msg.key.remoteJidAlt)) {
1171
- participant = author // TODO: investigate sending receipts to LIDs and not PNs
616
+ } else {
617
+ if (messageRetryManager && msg.key.id) messageRetryManager.cancelPendingPhoneRequest(msg.key.id)
618
+ const isNewsletter = isJidNewsletter(msg.key.remoteJid)
619
+ if (!isNewsletter) {
620
+ let type = undefined
621
+ let participant = msg.key.participant
622
+ if (category === "peer") type = "peer_msg"
623
+ else if (msg.key.fromMe) { type = "sender"; if (isLidUser(msg.key.remoteJid) || isLidUser(msg.key.remoteJidAlt)) participant = author }
624
+ else if (!sendActiveReceipts) type = "inactive"
625
+ await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type)
626
+ const isAnyHistoryMsg = getHistoryMsg(msg.message)
627
+ if (isAnyHistoryMsg) {
628
+ const jid = jidNormalizedUser(msg.key.remoteJid)
629
+ await sendReceipt(jid, undefined, [msg.key.id], "hist_sync")
630
+ }
631
+ } else {
632
+ await sendMessageAck(node)
633
+ logger.debug({ key: msg.key }, "processed newsletter message without receipts")
1172
634
  }
1173
- } else if (!sendActiveReceipts) {
1174
- type = "inactive"
1175
- }
1176
-
1177
- await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type)
1178
-
1179
- // send ack for history message
1180
- const isAnyHistoryMsg = getHistoryMsg(msg.message)
1181
- if (isAnyHistoryMsg) {
1182
- const jid = jidNormalizedUser(msg.key.remoteJid)
1183
- await sendReceipt(jid, undefined, [msg.key.id], "hist_sync")
1184
635
  }
1185
- }
1186
-
1187
- cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid)
1188
- await upsertMessage(msg, node.attrs.offline ? "append" : "notify")
1189
- })
1190
- } catch (error) {
1191
- logger.error({ error, node: binaryNodeToString(node) }, "error in handling message")
636
+ cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid)
637
+ await upsertMessage(msg, node.attrs.offline ? "append" : "notify")
638
+ })
639
+ } catch (error) { logger.error({ error, node: binaryNodeToString(node) }, "error in handling message") }
1192
640
  }
1193
- }
1194
-
641
+
1195
642
  const handleCall = async (node) => {
1196
643
  const { attrs } = node
1197
644
  const [infoChild] = getAllBinaryNodeChildren(node)
1198
645
  const status = getCallStatusFromNode(infoChild)
1199
- if (!infoChild) {
1200
- throw new Boom("Missing call info in call node")
1201
- }
646
+ if (!infoChild) throw new Boom("Missing call info in call node")
1202
647
  const callId = infoChild.attrs["call-id"]
1203
648
  const from = infoChild.attrs.from || infoChild.attrs["call-creator"]
1204
- const call = {
1205
- chatId: attrs.from,
1206
- from,
1207
- id: callId,
1208
- date: new Date(+attrs.t * 1000),
1209
- offline: !!attrs.offline,
1210
- status,
1211
- }
1212
- if (status === "offer") {
1213
- call.isVideo = !!getBinaryNodeChild(infoChild, "video")
1214
- call.isGroup = infoChild.attrs.type === "group" || !!infoChild.attrs["group-jid"]
1215
- call.groupJid = infoChild.attrs["group-jid"]
1216
- await callOfferCache.set(call.id, call)
1217
- }
649
+ const call = { chatId: attrs.from, from, callerPn: infoChild.attrs["caller_pn"], id: callId, date: new Date(+attrs.t * 1000), offline: !!attrs.offline, status }
650
+ if (status === "offer") { call.isVideo = !!getBinaryNodeChild(infoChild, "video"); call.isGroup = infoChild.attrs.type === "group" || !!infoChild.attrs["group-jid"]; call.groupJid = infoChild.attrs["group-jid"]; await callOfferCache.set(call.id, call) }
1218
651
  const existingCall = await callOfferCache.get(call.id)
1219
- // use existing call info to populate this event
1220
- if (existingCall) {
1221
- call.isVideo = existingCall.isVideo
1222
- call.isGroup = existingCall.isGroup
1223
- }
1224
- // delete data once call has ended
1225
- if (status === "reject" || status === "accept" || status === "timeout" || status === "terminate") {
1226
- await callOfferCache.del(call.id)
1227
- }
652
+ if (existingCall) { call.isVideo = existingCall.isVideo; call.isGroup = existingCall.isGroup; call.callerPn = call.callerPn || existingCall.callerPn }
653
+ if (status === "reject" || status === "accept" || status === "timeout" || status === "terminate") await callOfferCache.del(call.id)
1228
654
  ev.emit("call", [call])
1229
655
  await sendMessageAck(node)
1230
656
  }
657
+
1231
658
  const handleBadAck = async ({ attrs }) => {
1232
659
  const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id }
1233
- // WARNING: REFRAIN FROM ENABLING THIS FOR NOW. IT WILL CAUSE A LOOP
1234
- // // current hypothesis is that if pash is sent in the ack
1235
- // // it means -- the message hasn't reached all devices yet
1236
- // // we'll retry sending the message here
1237
- // if(attrs.phash) {
1238
- // logger.info({ attrs }, 'received phash in ack, resending message...')
1239
- // const msg = await getMessage(key)
1240
- // if(msg) {
1241
- // await relayMessage(key.remoteJid!, msg, { messageId: key.id!, useUserDevicesCache: false })
1242
- // } else {
1243
- // logger.warn({ attrs }, 'could not send message again, as it was not found')
1244
- // }
1245
- // }
1246
- // error in acknowledgement,
1247
- // device could not display the message
1248
660
  if (attrs.error) {
1249
661
  logger.warn({ attrs }, "received error in ack")
1250
- ev.emit("messages.update", [
1251
- {
1252
- key,
1253
- update: {
1254
- status: WAMessageStatus.ERROR,
1255
- messageStubParameters: [attrs.error],
1256
- },
1257
- },
1258
- ])
662
+ ev.emit("messages.update", [{ key, update: { status: WAMessageStatus.ERROR, messageStubParameters: [attrs.error] } }])
1259
663
  }
1260
664
  }
1261
- /// processes a node with the given function
1262
- /// and adds the task to the existing buffer if we're buffering events
665
+
1263
666
  const processNodeWithBuffer = async (node, identifier, exec) => {
1264
667
  ev.buffer()
1265
668
  await execTask()
1266
669
  ev.flush()
1267
- function execTask() {
1268
- return exec(node, false).catch((err) => onUnexpectedError(err, identifier))
1269
- }
670
+ function execTask() { return exec(node, false).catch((err) => onUnexpectedError(err, identifier)) }
1270
671
  }
672
+
673
+ const yieldToEventLoop = () => new Promise(resolve => setImmediate(resolve))
674
+
1271
675
  const makeOfflineNodeProcessor = () => {
1272
- const nodeProcessorMap = new Map([
1273
- ["message", handleMessage],
1274
- ["call", handleCall],
1275
- ["receipt", handleReceipt],
1276
- ["notification", handleNotification],
1277
- ])
676
+ const nodeProcessorMap = new Map([["message", handleMessage], ["call", handleCall], ["receipt", handleReceipt], ["notification", handleNotification]])
1278
677
  const nodes = []
1279
678
  let isProcessing = false
679
+ const BATCH_SIZE = 10
1280
680
  const enqueue = (type, node) => {
1281
681
  nodes.push({ type, node })
1282
- if (isProcessing) {
1283
- return
1284
- }
682
+ if (isProcessing) return
1285
683
  isProcessing = true
1286
684
  const promise = async () => {
685
+ let processedInBatch = 0
1287
686
  while (nodes.length && ws.isOpen) {
1288
687
  const { type, node } = nodes.shift()
1289
688
  const nodeProcessor = nodeProcessorMap.get(type)
1290
- if (!nodeProcessor) {
1291
- onUnexpectedError(new Error(`unknown offline node type: ${type}`), "processing offline node")
1292
- continue
1293
- }
689
+ if (!nodeProcessor) { onUnexpectedError(new Error(`unknown offline node type: ${type}`), "processing offline node"); continue }
1294
690
  await nodeProcessor(node)
691
+ processedInBatch++
692
+ if (processedInBatch >= BATCH_SIZE) { processedInBatch = 0; await yieldToEventLoop() }
1295
693
  }
1296
694
  isProcessing = false
1297
695
  }
@@ -1299,74 +697,37 @@ export const makeMessagesRecvSocket = (config) => {
1299
697
  }
1300
698
  return { enqueue }
1301
699
  }
700
+
1302
701
  const offlineNodeProcessor = makeOfflineNodeProcessor()
1303
- const processNode = (type, node, identifier, exec) => {
702
+
703
+ const processNode = async (type, node, identifier, exec) => {
1304
704
  const isOffline = !!node.attrs.offline
1305
- if (isOffline) {
1306
- offlineNodeProcessor.enqueue(type, node)
1307
- } else {
1308
- processNodeWithBuffer(node, identifier, exec)
1309
- }
705
+ if (isOffline) offlineNodeProcessor.enqueue(type, node)
706
+ else await processNodeWithBuffer(node, identifier, exec)
1310
707
  }
1311
- // recv a message
1312
- ws.on("CB:message", (node) => {
1313
- processNode("message", node, "processing message", handleMessage)
1314
- })
1315
- ws.on("CB:call", async (node) => {
1316
- processNode("call", node, "handling call", handleCall)
1317
- })
1318
- ws.on("CB:receipt", (node) => {
1319
- processNode("receipt", node, "handling receipt", handleReceipt)
1320
- })
1321
- ws.on("CB:notification", async (node) => {
1322
- processNode("notification", node, "handling notification", handleNotification)
1323
- })
1324
- ws.on("CB:ack,class:message", (node) => {
1325
- handleBadAck(node).catch((error) => onUnexpectedError(error, "handling bad ack"))
1326
- })
1327
- ev.on("call", ([call]) => {
1328
- if (!call) {
1329
- return
1330
- }
1331
- // missed call + group call notification message generation
708
+
709
+ ws.on("CB:message", async (node) => { await processNode("message", node, "processing message", handleMessage) })
710
+ ws.on("CB:call", async (node) => { await processNode("call", node, "handling call", handleCall) })
711
+ ws.on("CB:receipt", async (node) => { await processNode("receipt", node, "handling receipt", handleReceipt) })
712
+ ws.on("CB:notification", async (node) => { await processNode("notification", node, "handling notification", handleNotification) })
713
+ ws.on("CB:ack,class:message", (node) => { handleBadAck(node).catch((error) => onUnexpectedError(error, "handling bad ack")) })
714
+
715
+ ev.on("call", async ([call]) => {
716
+ if (!call) return
1332
717
  if (call.status === "timeout" || (call.status === "offer" && call.isGroup)) {
1333
- const msg = {
1334
- key: {
1335
- remoteJid: call.chatId,
1336
- id: call.id,
1337
- fromMe: false,
1338
- },
1339
- messageTimestamp: unixTimestampSeconds(call.date),
1340
- }
718
+ const msg = { key: { remoteJid: call.chatId, id: call.id, fromMe: false }, messageTimestamp: unixTimestampSeconds(call.date) }
1341
719
  if (call.status === "timeout") {
1342
- if (call.isGroup) {
1343
- msg.messageStubType = call.isVideo
1344
- ? WAMessageStubType.CALL_MISSED_GROUP_VIDEO
1345
- : WAMessageStubType.CALL_MISSED_GROUP_VOICE
1346
- } else {
1347
- msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE
1348
- }
1349
- } else {
1350
- msg.message = { call: { callKey: Buffer.from(call.id) } }
1351
- }
720
+ if (call.isGroup) msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_GROUP_VIDEO : WAMessageStubType.CALL_MISSED_GROUP_VOICE
721
+ else msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE
722
+ } else msg.message = { call: { callKey: Buffer.from(call.id) } }
1352
723
  const protoMsg = proto.WebMessageInfo.fromObject(msg)
1353
- upsertMessage(protoMsg, call.offline ? "append" : "notify")
724
+ await upsertMessage(protoMsg, call.offline ? "append" : "notify")
1354
725
  }
1355
726
  })
727
+
1356
728
  ev.on("connection.update", ({ isOnline }) => {
1357
- if (typeof isOnline !== "undefined") {
1358
- sendActiveReceipts = isOnline
1359
- logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`)
1360
- }
729
+ if (typeof isOnline !== "undefined") { sendActiveReceipts = isOnline; logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`) }
1361
730
  })
1362
- return {
1363
- ...sock,
1364
- sendMessageAck,
1365
- sendRetryRequest,
1366
- rejectCall,
1367
- fetchMessageHistory,
1368
- requestPlaceholderResend,
1369
- messageRetryManager,
1370
- }
1371
- }
1372
- //# sourceMappingURL=messages-recv.js.map
731
+
732
+ return { ...sock, sendMessageAck, sendRetryRequest, rejectCall, fetchMessageHistory, requestPlaceholderResend, messageRetryManager }
733
+ }