@nexustechpro/baileys 2.0.2 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +924 -1299
  3. package/lib/Defaults/baileys-version.json +6 -2
  4. package/lib/Defaults/index.js +172 -172
  5. package/lib/Signal/libsignal.js +380 -292
  6. package/lib/Signal/lid-mapping.js +264 -171
  7. package/lib/Socket/Client/index.js +2 -2
  8. package/lib/Socket/Client/types.js +10 -10
  9. package/lib/Socket/Client/websocket.js +45 -310
  10. package/lib/Socket/business.js +375 -375
  11. package/lib/Socket/chats.js +909 -963
  12. package/lib/Socket/communities.js +430 -430
  13. package/lib/Socket/groups.js +342 -342
  14. package/lib/Socket/index.js +22 -22
  15. package/lib/Socket/messages-recv.js +777 -743
  16. package/lib/Socket/messages-send.js +295 -305
  17. package/lib/Socket/mex.js +50 -50
  18. package/lib/Socket/newsletter.js +148 -148
  19. package/lib/Socket/nexus-handler.js +75 -261
  20. package/lib/Socket/socket.js +709 -1201
  21. package/lib/Store/index.js +5 -5
  22. package/lib/Store/make-cache-manager-store.js +81 -81
  23. package/lib/Store/make-in-memory-store.js +416 -416
  24. package/lib/Store/make-ordered-dictionary.js +81 -81
  25. package/lib/Store/object-repository.js +30 -30
  26. package/lib/Types/Auth.js +1 -1
  27. package/lib/Types/Bussines.js +1 -1
  28. package/lib/Types/Call.js +1 -1
  29. package/lib/Types/Chat.js +7 -7
  30. package/lib/Types/Contact.js +1 -1
  31. package/lib/Types/Events.js +1 -1
  32. package/lib/Types/GroupMetadata.js +1 -1
  33. package/lib/Types/Label.js +24 -24
  34. package/lib/Types/LabelAssociation.js +6 -6
  35. package/lib/Types/Message.js +10 -10
  36. package/lib/Types/Newsletter.js +28 -28
  37. package/lib/Types/Product.js +1 -1
  38. package/lib/Types/Signal.js +1 -1
  39. package/lib/Types/Socket.js +2 -2
  40. package/lib/Types/State.js +12 -12
  41. package/lib/Types/USync.js +1 -1
  42. package/lib/Types/index.js +25 -25
  43. package/lib/Utils/auth-utils.js +264 -256
  44. package/lib/Utils/baileys-event-stream.js +55 -55
  45. package/lib/Utils/browser-utils.js +27 -27
  46. package/lib/Utils/business.js +228 -230
  47. package/lib/Utils/chat-utils.js +694 -764
  48. package/lib/Utils/crypto.js +109 -135
  49. package/lib/Utils/decode-wa-message.js +310 -314
  50. package/lib/Utils/event-buffer.js +547 -547
  51. package/lib/Utils/generics.js +297 -297
  52. package/lib/Utils/history.js +91 -83
  53. package/lib/Utils/index.js +21 -20
  54. package/lib/Utils/key-store.js +17 -0
  55. package/lib/Utils/link-preview.js +97 -98
  56. package/lib/Utils/logger.js +2 -2
  57. package/lib/Utils/lt-hash.js +47 -47
  58. package/lib/Utils/make-mutex.js +39 -39
  59. package/lib/Utils/message-retry-manager.js +148 -148
  60. package/lib/Utils/messages-media.js +534 -534
  61. package/lib/Utils/messages.js +705 -705
  62. package/lib/Utils/noise-handler.js +255 -255
  63. package/lib/Utils/pre-key-manager.js +105 -105
  64. package/lib/Utils/process-message.js +412 -412
  65. package/lib/Utils/signal.js +160 -158
  66. package/lib/Utils/use-multi-file-auth-state.js +120 -120
  67. package/lib/Utils/validate-connection.js +194 -194
  68. package/lib/WABinary/constants.js +1300 -1300
  69. package/lib/WABinary/decode.js +237 -237
  70. package/lib/WABinary/encode.js +232 -232
  71. package/lib/WABinary/generic-utils.js +252 -211
  72. package/lib/WABinary/index.js +5 -5
  73. package/lib/WABinary/jid-utils.js +279 -95
  74. package/lib/WABinary/types.js +1 -1
  75. package/lib/WAM/BinaryInfo.js +9 -9
  76. package/lib/WAM/constants.js +22852 -22852
  77. package/lib/WAM/encode.js +149 -149
  78. package/lib/WAM/index.js +3 -3
  79. package/lib/WAUSync/Protocols/USyncContactProtocol.js +28 -28
  80. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +53 -53
  81. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -26
  82. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -37
  83. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -50
  84. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -28
  85. package/lib/WAUSync/Protocols/index.js +4 -4
  86. package/lib/WAUSync/USyncQuery.js +93 -93
  87. package/lib/WAUSync/USyncUser.js +22 -22
  88. package/lib/WAUSync/index.js +3 -3
  89. package/lib/index.js +66 -66
  90. package/package.json +171 -144
  91. package/lib/Signal/Group/ciphertext-message.js +0 -12
  92. package/lib/Signal/Group/group-session-builder.js +0 -30
  93. package/lib/Signal/Group/group_cipher.js +0 -100
  94. package/lib/Signal/Group/index.js +0 -12
  95. package/lib/Signal/Group/keyhelper.js +0 -18
  96. package/lib/Signal/Group/sender-chain-key.js +0 -26
  97. package/lib/Signal/Group/sender-key-distribution-message.js +0 -63
  98. package/lib/Signal/Group/sender-key-message.js +0 -66
  99. package/lib/Signal/Group/sender-key-name.js +0 -48
  100. package/lib/Signal/Group/sender-key-record.js +0 -41
  101. package/lib/Signal/Group/sender-key-state.js +0 -84
  102. package/lib/Signal/Group/sender-message-key.js +0 -26
@@ -1,744 +1,778 @@
1
- import NodeCache from "@cacheable/node-cache"
2
- import { Boom } from "@hapi/boom"
3
- import { randomBytes } from "crypto"
4
- import { proto } from "../../WAProto/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
- import { WAMessageStatus, WAMessageStubType } from "../Types/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"
8
- import { makeMutex } from "../Utils/make-mutex.js"
9
- import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from "../WABinary/index.js"
10
- import { extractGroupMetadata } from "./groups.js"
11
- import { makeMessagesSocket } from "./messages-send.js"
12
-
13
- export const makeMessagesRecvSocket = (config) => {
14
- const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config
15
- const sock = makeMessagesSocket(config)
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()
21
- const retryMutex = makeMutex()
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
-
28
- let sendActiveReceipts = false
29
-
30
- const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
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 }
33
- return sendPeerDataOperationMessage(pdoMessage)
34
- }
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)
44
- return sendPeerDataOperationMessage(pdoMessage)
45
- }
46
-
47
- const handleMexNewsletterNotification = async (node) => {
48
- const mexNode = getBinaryNodeChild(node, "mex")
49
- if (!mexNode?.content) { /*logger.warn({ node }, "Invalid mex newsletter notification");*/ return }
50
- let data
51
- try { data = JSON.parse(mexNode.content.toString()) } catch (error) { /*logger.error({ err: error, node }, "Failed to parse mex newsletter notification");*/ return }
52
- const operation = data?.operation
53
- const updates = data?.updates
54
- if (!updates || !operation) { /*logger.warn({ data }, "Invalid mex newsletter notification content");*/ return }
55
- logger.info({ operation, updates }, "got mex newsletter notification")
56
- switch (operation) {
57
- case "NotificationNewsletterUpdate":
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 })
59
- break
60
- case "NotificationNewsletterAdminPromote":
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" })
62
- break
63
- default:
64
- logger.info({ operation, data }, "Unhandled mex newsletter notification")
65
- break
66
- }
67
- }
68
-
69
- const handleNewsletterNotification = async (node) => {
70
- const from = node.attrs.from
71
- const child = getAllBinaryNodeChildren(node)[0]
72
- const author = node.attrs.participant
73
- logger.info({ from, child }, "got newsletter notification")
74
- switch (child.tag) {
75
- case "reaction":
76
- ev.emit("newsletter.reaction", { id: from, server_id: child.attrs.message_id, reaction: { code: getBinaryNodeChildString(child, "reaction"), count: 1 } })
77
- break
78
- case "view":
79
- ev.emit("newsletter.view", { id: from, server_id: child.attrs.message_id, count: parseInt(child.content?.toString() || "0", 10) })
80
- break
81
- case "participant":
82
- ev.emit("newsletter-participants.update", { id: from, author, user: child.attrs.jid, action: child.attrs.action, new_role: child.attrs.role })
83
- break
84
- case "update":
85
- const settingsNode = getBinaryNodeChild(child, "settings")
86
- if (settingsNode) {
87
- const update = {}
88
- const nameNode = getBinaryNodeChild(settingsNode, "name")
89
- if (nameNode?.content) update.name = nameNode.content.toString()
90
- const descriptionNode = getBinaryNodeChild(settingsNode, "description")
91
- if (descriptionNode?.content) update.description = descriptionNode.content.toString()
92
- ev.emit("newsletter-settings.update", { id: from, update })
93
- }
94
- break
95
- case "message":
96
- const plaintextNode = getBinaryNodeChild(child, "plaintext")
97
- if (plaintextNode?.content) {
98
- try {
99
- const contentBuf = typeof plaintextNode.content === "string" ? Buffer.from(plaintextNode.content, "binary") : Buffer.from(plaintextNode.content)
100
- const messageProto = proto.Message.decode(contentBuf).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()
102
- await upsertMessage(fullMessage, "append")
103
- logger.info("Processed plaintext newsletter message")
104
- } catch (error) { logger.error({ error }, "Failed to decode plaintext newsletter message") }
105
- }
106
- break
107
- default:
108
- logger.warn({ node }, "Unknown newsletter notification")
109
- break
110
- }
111
- }
112
-
113
- const sendMessageAck = async ({ tag, attrs, content }, errorCode) => {
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
120
- logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, "sent ack")
121
- try {
122
- await sendNode(stanza)
123
- } catch (error) {
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
126
- }
127
- }
128
-
129
- const rejectCall = async (callId, callFrom) => {
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 }] }
131
- await query(stanza)
132
- }
133
-
134
- const sendRetryRequest = async (node, forceIncludeKeys = false) => {
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)
149
- }
150
- const key = `${msgId}:${msgKey?.participant}`
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
173
- }
174
- } catch (error) { logger.warn({ error, fromJid }, "failed to check session recreation") }
175
- }
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
- }
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")
205
- }
206
-
207
- const handleEncryptNotification = async (node) => {
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()
215
- } else {
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")
218
- }
219
- }
220
-
221
- const handleGroupNotification = (fullNode, child, msg) => {
222
- const actingParticipantLid = fullNode.attrs.participant
223
- const actingParticipantPn = fullNode.attrs.participant_pn
224
- const affectedParticipantLid = getBinaryNodeChild(child, "participant")?.attrs?.jid || actingParticipantLid
225
- const affectedParticipantPn = getBinaryNodeChild(child, "participant")?.attrs?.phone_number || actingParticipantPn
226
- switch (child?.tag) {
227
- case "create":
228
- const metadata = extractGroupMetadata(child)
229
- msg.messageStubType = WAMessageStubType.GROUP_CREATE
230
- msg.messageStubParameters = [metadata.subject]
231
- msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn }
232
- ev.emit("chats.upsert", [{ id: metadata.id, name: metadata.subject, conversationTimestamp: metadata.creation }])
233
- ev.emit("groups.upsert", [{ ...metadata, author: actingParticipantLid, authorPn: actingParticipantPn }])
234
- break
235
- case "ephemeral":
236
- case "not_ephemeral":
237
- msg.message = { protocolMessage: { type: proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: +(child.attrs.expiration || 0) } }
238
- break
239
- case "modify":
240
- const oldNumber = getBinaryNodeChildren(child, "participant").map((p) => p.attrs.jid)
241
- msg.messageStubParameters = oldNumber || []
242
- msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER
243
- break
244
- case "promote":
245
- case "demote":
246
- case "remove":
247
- case "add":
248
- case "leave":
249
- const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`
250
- msg.messageStubType = WAMessageStubType[stubType]
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
253
- msg.messageStubParameters = participants.map((a) => JSON.stringify(a))
254
- break
255
- case "subject":
256
- msg.messageStubType = WAMessageStubType.GROUP_CHANGE_SUBJECT
257
- msg.messageStubParameters = [child.attrs.subject]
258
- break
259
- case "description":
260
- const description = getBinaryNodeChild(child, "body")?.content?.toString()
261
- msg.messageStubType = WAMessageStubType.GROUP_CHANGE_DESCRIPTION
262
- msg.messageStubParameters = description ? [description] : undefined
263
- break
264
- case "announcement":
265
- case "not_announcement":
266
- msg.messageStubType = WAMessageStubType.GROUP_CHANGE_ANNOUNCE
267
- msg.messageStubParameters = [child.tag === "announcement" ? "on" : "off"]
268
- break
269
- case "locked":
270
- case "unlocked":
271
- msg.messageStubType = WAMessageStubType.GROUP_CHANGE_RESTRICT
272
- msg.messageStubParameters = [child.tag === "locked" ? "on" : "off"]
273
- break
274
- case "invite":
275
- msg.messageStubType = WAMessageStubType.GROUP_CHANGE_INVITE_LINK
276
- msg.messageStubParameters = [child.attrs.code]
277
- break
278
- case "member_add_mode":
279
- const addMode = child.content
280
- if (addMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBER_ADD_MODE; msg.messageStubParameters = [addMode.toString()] }
281
- break
282
- case "membership_approval_mode":
283
- const approvalMode = getBinaryNodeChild(child, "group_join")
284
- if (approvalMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE; msg.messageStubParameters = [approvalMode.attrs.state] }
285
- break
286
- case "created_membership_requests":
287
- msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
288
- msg.messageStubParameters = [JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }), "created", child.attrs.request_method]
289
- break
290
- case "revoked_membership_requests":
291
- const isDenied = areJidsSameUser(affectedParticipantLid, actingParticipantLid)
292
- msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
293
- msg.messageStubParameters = [JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }), isDenied ? "revoked" : "rejected"]
294
- break
295
- }
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
-
315
- const processNotification = async (node) => {
316
- const result = {}
317
- const [child] = getAllBinaryNodeChildren(node)
318
- const nodeType = node.attrs.type
319
- const from = jidNormalizedUser(node.attrs.from)
320
- switch (nodeType) {
321
- case "privacy_token":
322
- await handlePrivacyTokenNotification(node)
323
- break
324
- case "newsletter":
325
- await handleNewsletterNotification(node)
326
- break
327
- case "mex":
328
- await handleMexNewsletterNotification(node)
329
- break
330
- case "w:gp2":
331
- handleGroupNotification(node, child, result)
332
- break
333
- case "mediaretry":
334
- const event = decodeMediaRetryNode(node)
335
- ev.emit("messages.media-update", [event])
336
- break
337
- case "encrypt":
338
- await handleEncryptNotification(node)
339
- break
340
- case "devices":
341
- const devices = getBinaryNodeChildren(child, "device")
342
- if (areJidsSameUser(child.attrs.jid, authState.creds.me.id) || areJidsSameUser(child.attrs.lid, authState.creds.me.lid)) {
343
- const deviceData = devices.map((d) => ({ id: d.attrs.jid, lid: d.attrs.lid }))
344
- logger.info({ deviceData }, "my own devices changed")
345
- }
346
- break
347
- case "server_sync":
348
- const update = getBinaryNodeChild(node, "collection")
349
- if (update) {
350
- const name = update.attrs.name
351
- await resyncAppState([name], false)
352
- }
353
- break
354
- case "picture":
355
- const setPicture = getBinaryNodeChild(node, "set")
356
- const delPicture = getBinaryNodeChild(node, "delete")
357
- ev.emit("contacts.update", [{ id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || "", imgUrl: setPicture ? "changed" : "removed" }])
358
- if (isJidGroup(from)) {
359
- const node = setPicture || delPicture
360
- result.messageStubType = WAMessageStubType.GROUP_CHANGE_ICON
361
- if (setPicture) result.messageStubParameters = [setPicture.attrs.id]
362
- result.participant = node?.attrs.author
363
- result.key = { ...(result.key || {}), participant: setPicture?.attrs.author }
364
- }
365
- break
366
- case "account_sync":
367
- if (child.tag === "disappearing_mode") {
368
- const newDuration = +child.attrs.duration
369
- const timestamp = +child.attrs.t
370
- logger.info({ newDuration }, "updated account disappearing mode")
371
- ev.emit("creds.update", { accountSettings: { ...authState.creds.accountSettings, defaultDisappearingMode: { ephemeralExpiration: newDuration, ephemeralSettingTimestamp: timestamp } } })
372
- } else if (child.tag === "blocklist") {
373
- const blocklists = getBinaryNodeChildren(child, "item")
374
- for (const { attrs } of blocklists) {
375
- const blocklist = [attrs.jid]
376
- const type = attrs.action === "block" ? "add" : "remove"
377
- ev.emit("blocklist.update", { blocklist, type })
378
- }
379
- }
380
- break
381
- case "link_code_companion_reg":
382
- const linkCodeCompanionReg = getBinaryNodeChild(node, "link_code_companion_reg")
383
- const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, "link_code_pairing_ref"))
384
- const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, "primary_identity_pub"))
385
- const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, "link_code_pairing_wrapped_primary_ephemeral_pub"))
386
- const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped)
387
- const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey)
388
- const random = randomBytes(32)
389
- const linkCodeSalt = randomBytes(32)
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])
392
- const encryptIv = randomBytes(12)
393
- const encrypted = aesEncryptGCM(encryptPayload, linkCodePairingExpanded, encryptIv, Buffer.alloc(0))
394
- const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted])
395
- const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey)
396
- const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random])
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 }] }] })
399
- authState.creds.registered = true
400
- ev.emit("creds.update", authState.creds)
401
- break
402
- }
403
- if (Object.keys(result).length) return result
404
- }
405
-
406
- async function decipherLinkPublicKey(data) {
407
- const buffer = toRequiredBuffer(data)
408
- const salt = buffer.slice(0, 32)
409
- const secretKey = await derivePairingCodeKey(authState.creds.pairingCode, salt)
410
- const iv = buffer.slice(32, 48)
411
- const payload = buffer.slice(48, 80)
412
- return aesDecryptCTR(payload, secretKey, iv)
413
- }
414
-
415
- function toRequiredBuffer(data) {
416
- if (data === undefined) throw new Boom("Invalid buffer", { statusCode: 400 })
417
- return data instanceof Buffer ? data : Buffer.from(data)
418
- }
419
-
420
- const willSendMessageAgain = async (id, participant) => {
421
- const key = `${id}:${participant}`
422
- const retryCount = (await msgRetryCache.get(key)) || 0
423
- return retryCount < maxMsgRetryCount
424
- }
425
-
426
- const updateSendMessageAgainCount = async (id, participant) => {
427
- const key = `${id}:${participant}`
428
- const newValue = ((await msgRetryCache.get(key)) || 0) + 1
429
- await msgRetryCache.set(key, newValue)
430
- }
431
-
432
- const sendMessagesAgain = async (key, ids, retryNode) => {
433
- const remoteJid = key.remoteJid
434
- const participant = key.participant || remoteJid
435
- const retryCount = +retryNode.attrs.count || 1
436
- const msgs = []
437
- for (const id of ids) {
438
- let msg
439
- if (messageRetryManager) {
440
- const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id)
441
- if (cachedMsg) { msg = cachedMsg.message; logger.debug({ jid: remoteJid, id }, "found message in retry cache"); messageRetryManager.markRetrySuccess(id) }
442
- }
443
- if (!msg) {
444
- msg = await getMessage({ ...key, id })
445
- if (msg) { logger.debug({ jid: remoteJid, id }, "found message via getMessage"); if (messageRetryManager) messageRetryManager.markRetrySuccess(id) }
446
- }
447
- msgs.push(msg)
448
- }
449
- const sendToAll = !jidDecode(participant)?.device
450
- let shouldRecreateSession = false
451
- let recreateReason = ""
452
- if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
453
- try {
454
- const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
455
- const hasSession = await signalRepository.validateSession(participant)
456
- const result = messageRetryManager.shouldRecreateSession(participant, retryCount, hasSession.exists)
457
- shouldRecreateSession = result.recreate
458
- recreateReason = result.reason
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") }
461
- }
462
- await assertSessions([participant], false);
463
- if (isJidGroup(remoteJid)) await authState.keys.set({ "sender-key-memory": { [remoteJid]: null } })
464
- logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason }, "preparing retry recp")
465
- for (const [i, msg] of msgs.entries()) {
466
- if (!ids[i]) continue
467
- if (msg && (await willSendMessageAgain(ids[i], participant))) {
468
- await updateSendMessageAgainCount(ids[i], participant)
469
- const msgRelayOpts = { messageId: ids[i] }
470
- if (sendToAll) msgRelayOpts.useUserDevicesCache = false
471
- else msgRelayOpts.participant = { jid: participant, count: +retryNode.attrs.count }
472
- await relayMessage(key.remoteJid, msg, msgRelayOpts)
473
- } else logger.debug({ jid: key.remoteJid, id: ids[i] }, "recv retry request, but message not available")
474
- }
475
- }
476
-
477
- const handleReceipt = async (node) => {
478
- const { attrs, content } = node
479
- const isLid = attrs.from.includes("lid")
480
- const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id)
481
- const remoteJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient
482
- const fromMe = !attrs.recipient || ((attrs.type === "retry" || attrs.type === "sender") && isNodeFromMe)
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 }
485
- const ids = [attrs.id]
486
- if (Array.isArray(content)) {
487
- const items = getBinaryNodeChildren(content[0], "item")
488
- ids.push(...items.map((i) => i.attrs.id))
489
- }
490
- try {
491
- await Promise.all([processingMutex.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 } })))
498
- }
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 {
513
- logger.info({ attrs, key, participant: key.participant }, "retry limit exhausted - clearing broken session")
514
- try {
515
- await signalRepository.deleteSession([key.participant])
516
- logger.debug({ participant: key.participant }, "deleted stale session for retry-exhausted participant")
517
- const retryKey = `${ids[0]}:${key.participant}`
518
- await msgRetryCache.del(retryKey)
519
- logger.debug({ retryKey }, "cleared retry count cache")
520
- } catch (err) {
521
- logger.error({ err, participant: key.participant }, "failed to clear session/cache at retry exhaustion")
522
- }
523
- }
524
- }
525
- })])
526
- } finally {
527
- await sendMessageAck(node)
528
- }
529
- }
530
-
531
- const handleNotification = async (node) => {
532
- const remoteJid = node.attrs.from
533
- if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) { logger.debug({ remoteJid, id: node.attrs.id }, "ignored notification"); await sendMessageAck(node); return }
534
- try {
535
- await Promise.all([notificationMutex.mutex(async () => {
536
- const msg = await processNotification(node)
537
- if (msg) {
538
- const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id)
539
- const { senderAlt: participantAlt, addressingMode } = extractAddressingContext(node)
540
- msg.key = { remoteJid, fromMe, participant: node.attrs.participant, participantAlt, addressingMode, id: node.attrs.id, ...(msg.key || {}) }
541
- msg.participant ?? (msg.participant = node.attrs.participant)
542
- msg.messageTimestamp = +node.attrs.t
543
- const fullMsg = proto.WebMessageInfo.fromObject(msg)
544
- await upsertMessage(fullMsg, "append")
545
- }
546
- })])
547
- } finally {
548
- await sendMessageAck(node)
549
- }
550
- }
551
-
552
- const handleMessage = async (node) => {
553
- 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 }
554
- const encNode = getBinaryNodeChild(node, "enc")
555
- if (encNode && encNode.attrs.type === "msmsg") { logger.debug({ key: node.attrs.key }, "ignored msmsg"); await sendMessageAck(node, NACK_REASONS.MissingMessageSecret); return }
556
- const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || "", signalRepository, logger)
557
- const alt = msg.key.participantAlt || msg.key.remoteJidAlt
558
- if (!!alt) {
559
- const altServer = jidDecode(alt)?.server
560
- const primaryJid = msg.key.participant || msg.key.remoteJid
561
- if (altServer === "lid") { if (!(await signalRepository.lidMapping.getPNForLID(alt))) { await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]); await signalRepository.migrateSession(primaryJid, alt) } }
562
- else { await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]); await signalRepository.migrateSession(alt, primaryJid) }
563
- }
564
- 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") }
565
- try {
566
- await messageMutex.mutex(async () => {
567
- await decrypt()
568
- if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== "peer") {
569
- if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) return sendMessageAck(node, NACK_REASONS.ParsingError)
570
- if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
571
- const unavailableNode = getBinaryNodeChild(node, "unavailable")
572
- const unavailableType = unavailableNode?.attrs?.type
573
- 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) }
574
- const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp)
575
- if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) { logger.debug({ msgId: msg.key.id, messageAge }, "skipping placeholder resend for old message"); return sendMessageAck(node) }
576
- const cleanKey = { remoteJid: msg.key.remoteJid, fromMe: msg.key.fromMe, id: msg.key.id, participant: msg.key.participant }
577
- const msgData = { key: msg.key, messageTimestamp: msg.messageTimestamp, pushName: msg.pushName, participant: msg.participant, verifiedBizName: msg.verifiedBizName }
578
- 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") })
579
- await sendMessageAck(node)
580
- } else {
581
- if (isJidStatusBroadcast(msg.key.remoteJid)) {
582
- const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp)
583
- 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) }
584
- }
585
- const errorMessage = msg?.messageStubParameters?.[0] || ""
586
- const isPreKeyError = errorMessage.includes("PreKey")
587
- const isBadMacError = errorMessage.includes("Bad MAC")
588
- const isMessageCounterError = errorMessage.includes("Key used already or never filled")
589
- logger.debug(`[handleMessage] Failed decryption - PreKey: ${isPreKeyError}, BadMAC: ${isBadMacError}`)
590
- if (isBadMacError || isMessageCounterError) {
591
- const jidToReset = msg.key.participant || msg.key.remoteJid
592
- logger.error({ jid: jidToReset, error: errorMessage }, "BAD MAC ERROR - Corrupted session detected, initiating emergency recovery")
593
- Promise.resolve().then(async () => {
594
- try {
595
- await signalRepository.deleteSession([jidToReset])
596
- logger.info({ jid: jidToReset }, "✓ Deleted corrupted session")
597
- await uploadPreKeys(MIN_PREKEY_COUNT)
598
- logger.info("✓ Uploaded fresh pre-keys")
599
- await delay(500)
600
- logger.info({ jid: jidToReset }, "✓ Emergency recovery complete - ready for retry")
601
- } catch (recoveryErr) { logger.error({ err: recoveryErr, jid: jidToReset }, "✗ Emergency recovery failed") }
602
- }).catch(err => logger.error({ err }, "Recovery promise failed"))
603
- }
604
- retryMutex.mutex(async () => {
605
- try {
606
- if (!ws.isOpen) { logger.debug({ node }, "Connection closed, skipping retry"); return }
607
- if (isPreKeyError) {
608
- logger.info({ error: errorMessage }, "PreKey error detected, uploading before retry")
609
- try {
610
- await uploadPreKeys(MIN_PREKEY_COUNT)
611
- await delay(1000)
612
- } catch (uploadErr) { logger.error({ uploadErr }, "Pre-key upload failed during retry") }
613
- }
614
- const encNode = getBinaryNodeChild(node, "enc")
615
- await sendRetryRequest(node, !encNode || isBadMacError || isMessageCounterError || isPreKeyError)
616
- if (retryRequestDelayMs) await delay(retryRequestDelayMs)
617
- } catch (err) {
618
- logger.error({ err, isBadMacError, isPreKeyError }, "Retry mechanism failed")
619
- try {
620
- const encNode = getBinaryNodeChild(node, "enc")
621
- await sendRetryRequest(node, !encNode)
622
- } catch (retryErr) { logger.error({ retryErr }, "Final retry attempt failed") }
623
- }
624
- await sendMessageAck(node, NACK_REASONS.UnhandledError)
625
- })
626
- }
627
- } else {
628
- if (messageRetryManager && msg.key.id) messageRetryManager.cancelPendingPhoneRequest(msg.key.id)
629
- const isNewsletter = isJidNewsletter(msg.key.remoteJid)
630
- if (!isNewsletter) {
631
- let type = undefined
632
- let participant = msg.key.participant
633
- if (category === "peer") type = "peer_msg"
634
- else if (msg.key.fromMe) { type = "sender"; if (isLidUser(msg.key.remoteJid) || isLidUser(msg.key.remoteJidAlt)) participant = author }
635
- else if (!sendActiveReceipts) type = "inactive"
636
- await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type)
637
- const isAnyHistoryMsg = getHistoryMsg(msg.message)
638
- if (isAnyHistoryMsg) {
639
- const jid = jidNormalizedUser(msg.key.remoteJid)
640
- await sendReceipt(jid, undefined, [msg.key.id], "hist_sync")
641
- }
642
- } else {
643
- await sendMessageAck(node)
644
- logger.debug({ key: msg.key }, "processed newsletter message without receipts")
645
- }
646
- }
647
- cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid)
648
- await upsertMessage(msg, node.attrs.offline ? "append" : "notify")
649
- })
650
- } catch (error) { logger.error({ error, node: binaryNodeToString(node) }, "error in handling message") }
651
- }
652
-
653
- const handleCall = async (node) => {
654
- const { attrs } = node
655
- const [infoChild] = getAllBinaryNodeChildren(node)
656
- const status = getCallStatusFromNode(infoChild)
657
- if (!infoChild) throw new Boom("Missing call info in call node")
658
- const callId = infoChild.attrs["call-id"]
659
- const from = infoChild.attrs.from || infoChild.attrs["call-creator"]
660
- const call = { chatId: attrs.from, from, callerPn: infoChild.attrs["caller_pn"], id: callId, date: new Date(+attrs.t * 1000), offline: !!attrs.offline, status }
661
- 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) }
662
- const existingCall = await callOfferCache.get(call.id)
663
- if (existingCall) { call.isVideo = existingCall.isVideo; call.isGroup = existingCall.isGroup; call.callerPn = call.callerPn || existingCall.callerPn }
664
- if (status === "reject" || status === "accept" || status === "timeout" || status === "terminate") await callOfferCache.del(call.id)
665
- ev.emit("call", [call])
666
- await sendMessageAck(node)
667
- }
668
-
669
- const handleBadAck = async ({ attrs }) => {
670
- const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id }
671
- if (attrs.error) {
672
- logger.warn({ attrs }, "received error in ack")
673
- ev.emit("messages.update", [{ key, update: { status: WAMessageStatus.ERROR, messageStubParameters: [attrs.error] } }])
674
- }
675
- }
676
-
677
- const processNodeWithBuffer = async (node, identifier, exec) => {
678
- ev.buffer()
679
- await execTask()
680
- ev.flush()
681
- function execTask() { return exec(node, false).catch((err) => onUnexpectedError(err, identifier)) }
682
- }
683
-
684
- const yieldToEventLoop = () => new Promise(resolve => setImmediate(resolve))
685
-
686
- const makeOfflineNodeProcessor = () => {
687
- const nodeProcessorMap = new Map([["message", handleMessage], ["call", handleCall], ["receipt", handleReceipt], ["notification", handleNotification]])
688
- const nodes = []
689
- let isProcessing = false
690
- const BATCH_SIZE = 10
691
- const enqueue = (type, node) => {
692
- nodes.push({ type, node })
693
- if (isProcessing) return
694
- isProcessing = true
695
- const promise = async () => {
696
- let processedInBatch = 0
697
- while (nodes.length && ws.isOpen) {
698
- const { type, node } = nodes.shift()
699
- const nodeProcessor = nodeProcessorMap.get(type)
700
- if (!nodeProcessor) { onUnexpectedError(new Error(`unknown offline node type: ${type}`), "processing offline node"); continue }
701
- await nodeProcessor(node)
702
- processedInBatch++
703
- if (processedInBatch >= BATCH_SIZE) { processedInBatch = 0; await yieldToEventLoop() }
704
- }
705
- isProcessing = false
706
- }
707
- promise().catch((error) => onUnexpectedError(error, "processing offline nodes"))
708
- }
709
- return { enqueue }
710
- }
711
-
712
- const offlineNodeProcessor = makeOfflineNodeProcessor()
713
-
714
- const processNode = async (type, node, identifier, exec) => {
715
- const isOffline = !!node.attrs.offline
716
- if (isOffline) offlineNodeProcessor.enqueue(type, node)
717
- else await processNodeWithBuffer(node, identifier, exec)
718
- }
719
-
720
- ws.on("CB:message", async (node) => { await processNode("message", node, "processing message", handleMessage) })
721
- ws.on("CB:call", async (node) => { await processNode("call", node, "handling call", handleCall) })
722
- ws.on("CB:receipt", async (node) => { await processNode("receipt", node, "handling receipt", handleReceipt) })
723
- ws.on("CB:notification", async (node) => { await processNode("notification", node, "handling notification", handleNotification) })
724
- ws.on("CB:ack,class:message", (node) => { handleBadAck(node).catch((error) => onUnexpectedError(error, "handling bad ack")) })
725
-
726
- ev.on("call", async ([call]) => {
727
- if (!call) return
728
- if (call.status === "timeout" || (call.status === "offer" && call.isGroup)) {
729
- const msg = { key: { remoteJid: call.chatId, id: call.id, fromMe: false }, messageTimestamp: unixTimestampSeconds(call.date) }
730
- if (call.status === "timeout") {
731
- if (call.isGroup) msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_GROUP_VIDEO : WAMessageStubType.CALL_MISSED_GROUP_VOICE
732
- else msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE
733
- } else msg.message = { call: { callKey: Buffer.from(call.id) } }
734
- const protoMsg = proto.WebMessageInfo.fromObject(msg)
735
- await upsertMessage(protoMsg, call.offline ? "append" : "notify")
736
- }
737
- })
738
-
739
- ev.on("connection.update", ({ isOnline }) => {
740
- if (typeof isOnline !== "undefined") { sendActiveReceipts = isOnline; logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`) }
741
- })
742
-
743
- return { ...sock, sendMessageAck, sendRetryRequest, rejectCall, fetchMessageHistory, requestPlaceholderResend, messageRetryManager }
1
+ import NodeCache from "@cacheable/node-cache"
2
+ import { Boom } from "@hapi/boom"
3
+ import { randomBytes } from "crypto"
4
+ import { proto } from "../../WAProto/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
+ import { WAMessageStatus, WAMessageStubType } from "../Types/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"
8
+ import { makeMutex } from "../Utils/make-mutex.js"
9
+ import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET, jidEncode } from "../WABinary/index.js"
10
+ import { extractGroupMetadata } from "./groups.js"
11
+ import { makeMessagesSocket } from "./messages-send.js"
12
+
13
+ export const makeMessagesRecvSocket = (config) => {
14
+ const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config
15
+ const sock = makeMessagesSocket(config)
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 retryMutex = makeMutex()
21
+
22
+ const msgRetryCache = config.msgRetryCounterCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, useClones: false })
23
+ const callOfferCache = config.callOfferCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, useClones: false })
24
+ const placeholderResendCache = config.placeholderResendCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, useClones: false })
25
+ const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false })
26
+
27
+ let sendActiveReceipts = false
28
+
29
+ const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
30
+ if (!authState.creds.me?.id) throw new Boom("Not authenticated")
31
+ const pdoMessage = { historySyncOnDemandRequest: { chatJid: oldestMsgKey.remoteJid, oldestMsgFromMe: oldestMsgKey.fromMe, oldestMsgId: oldestMsgKey.id, oldestMsgTimestampMs: oldestMsgTimestamp, onDemandMsgCount: count }, peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND }
32
+ return sendPeerDataOperationMessage(pdoMessage)
33
+ }
34
+
35
+ const requestPlaceholderResend = async (messageKey, msgData) => {
36
+ if (!authState.creds.me?.id) throw new Boom("Not authenticated")
37
+ if (await placeholderResendCache.get(messageKey?.id)) { logger.debug({ messageKey }, "already requested resend"); return }
38
+ else await placeholderResendCache.set(messageKey?.id, msgData || true)
39
+ await delay(2000)
40
+ if (!(await placeholderResendCache.get(messageKey?.id))) { logger.debug({ messageKey }, "message received while resend requested"); return "RESOLVED" }
41
+ const pdoMessage = { placeholderMessageResendRequest: [{ messageKey }], peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND }
42
+ 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)
43
+ return sendPeerDataOperationMessage(pdoMessage)
44
+ }
45
+
46
+ const handleMexNewsletterNotification = async (node) => {
47
+ const mexNode = getBinaryNodeChild(node, "mex")
48
+ if (!mexNode?.content) { /*logger.warn({ node }, "Invalid mex newsletter notification");*/ return }
49
+ let data
50
+ try { data = JSON.parse(mexNode.content.toString()) } catch (error) { /*logger.error({ err: error, node }, "Failed to parse mex newsletter notification");*/ return }
51
+ const operation = data?.operation
52
+ const updates = data?.updates
53
+ if (!updates || !operation) { /*logger.warn({ data }, "Invalid mex newsletter notification content");*/ return }
54
+ logger.info({ operation, updates }, "got mex newsletter notification")
55
+ switch (operation) {
56
+ case "NotificationNewsletterUpdate":
57
+ 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 })
58
+ break
59
+ case "NotificationNewsletterAdminPromote":
60
+ 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" })
61
+ break
62
+ default:
63
+ logger.info({ operation, data }, "Unhandled mex newsletter notification")
64
+ break
65
+ }
66
+ }
67
+
68
+ const handleNewsletterNotification = async (node) => {
69
+ const from = node.attrs.from
70
+ const child = getAllBinaryNodeChildren(node)[0]
71
+ const author = node.attrs.participant
72
+ logger.info({ from, child }, "got newsletter notification")
73
+ switch (child.tag) {
74
+ case "reaction":
75
+ ev.emit("newsletter.reaction", { id: from, server_id: child.attrs.message_id, reaction: { code: getBinaryNodeChildString(child, "reaction"), count: 1 } })
76
+ break
77
+ case "view":
78
+ ev.emit("newsletter.view", { id: from, server_id: child.attrs.message_id, count: parseInt(child.content?.toString() || "0", 10) })
79
+ break
80
+ case "participant":
81
+ ev.emit("newsletter-participants.update", { id: from, author, user: child.attrs.jid, action: child.attrs.action, new_role: child.attrs.role })
82
+ break
83
+ case "update":
84
+ const settingsNode = getBinaryNodeChild(child, "settings")
85
+ if (settingsNode) {
86
+ const update = {}
87
+ const nameNode = getBinaryNodeChild(settingsNode, "name")
88
+ if (nameNode?.content) update.name = nameNode.content.toString()
89
+ const descriptionNode = getBinaryNodeChild(settingsNode, "description")
90
+ if (descriptionNode?.content) update.description = descriptionNode.content.toString()
91
+ ev.emit("newsletter-settings.update", { id: from, update })
92
+ }
93
+ break
94
+ case "message":
95
+ const plaintextNode = getBinaryNodeChild(child, "plaintext")
96
+ if (plaintextNode?.content) {
97
+ try {
98
+ const contentBuf = typeof plaintextNode.content === "string" ? Buffer.from(plaintextNode.content, "binary") : Buffer.from(plaintextNode.content)
99
+ const messageProto = proto.Message.decode(contentBuf).toJSON()
100
+ 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()
101
+ await upsertMessage(fullMessage, "append")
102
+ logger.info("Processed plaintext newsletter message")
103
+ } catch (error) { logger.error({ error }, "Failed to decode plaintext newsletter message") }
104
+ }
105
+ break
106
+ default:
107
+ logger.warn({ node }, "Unknown newsletter notification")
108
+ break
109
+ }
110
+ }
111
+
112
+ const sendMessageAck = async ({ tag, attrs, content }, errorCode) => {
113
+ const stanza = { tag: "ack", attrs: { id: attrs.id, to: attrs.from, class: tag } }
114
+ if (!!errorCode) stanza.attrs.error = errorCode.toString()
115
+ if (!!attrs.participant) stanza.attrs.participant = attrs.participant
116
+ if (!!attrs.recipient) stanza.attrs.recipient = attrs.recipient
117
+ if (!!attrs.type && (tag !== "message" || getBinaryNodeChild({ tag, attrs, content }, "unavailable") || errorCode !== 0)) stanza.attrs.type = attrs.type
118
+ if (tag === "message" && getBinaryNodeChild({ tag, attrs, content }, "unavailable")) stanza.attrs.from = authState.creds.me.id
119
+ logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, "sent ack")
120
+ try {
121
+ await sendNode(stanza)
122
+ } catch (error) {
123
+ 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")
124
+ else throw error
125
+ }
126
+ }
127
+
128
+ const rejectCall = async (callId, callFrom) => {
129
+ 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 }] }
130
+ await query(stanza)
131
+ }
132
+
133
+ const offerCall = async (toJid, isVideo = false) => {
134
+ const callId = randomBytes(16).toString('hex').toUpperCase().substring(0, 64)
135
+ const offerContent = []
136
+
137
+ offerContent.push({ tag: 'audio', attrs: { enc: 'opus', rate: '16000' }, content: undefined })
138
+ offerContent.push({ tag: 'audio', attrs: { enc: 'opus', rate: '8000' }, content: undefined })
139
+
140
+ if (isVideo) {
141
+ offerContent.push({
142
+ tag: 'video',
143
+ attrs: {
144
+ orientation: '0',
145
+ screen_width: '1920',
146
+ screen_height: '1080',
147
+ device_orientation: '0',
148
+ enc: 'vp8',
149
+ dec: 'vp8'
150
+ }
151
+ })
152
+ }
153
+
154
+ offerContent.push({ tag: 'net', attrs: { medium: '3' }, content: undefined })
155
+ offerContent.push({ tag: 'capability', attrs: { ver: '1' }, content: new Uint8Array([1, 4, 255, 131, 207, 4]) })
156
+ offerContent.push({ tag: 'encopt', attrs: { keygen: '2' }, content: undefined })
157
+
158
+ const encKey = randomBytes(32)
159
+ const devices = (await getUSyncDevices([toJid], true, false))
160
+ .map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device))
161
+
162
+ await assertSessions(devices, true)
163
+
164
+ const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(
165
+ devices,
166
+ { call: { callKey: encKey } },
167
+ {}
168
+ )
169
+
170
+ offerContent.push({ tag: 'destination', attrs: {}, content: destinations })
171
+
172
+ if (shouldIncludeDeviceIdentity) {
173
+ offerContent.push({
174
+ tag: 'device-identity',
175
+ attrs: {},
176
+ content: encodeSignedDeviceIdentity(authState.creds.account, true)
177
+ })
178
+ }
179
+
180
+ await query({
181
+ tag: 'call',
182
+ attrs: { to: toJid },
183
+ content: [{
184
+ tag: 'offer',
185
+ attrs: { 'call-id': callId, 'call-creator': authState.creds.me.id },
186
+ content: offerContent
187
+ }]
188
+ })
189
+
190
+ return { callId, toJid, isVideo }
191
+ }
192
+
193
+ const sendRetryRequest = async (node, forceIncludeKeys = false) => {
194
+ const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || "")
195
+ const { key: msgKey } = fullMessage
196
+ const msgId = msgKey.id
197
+ if (messageRetryManager) {
198
+ if (messageRetryManager.hasExceededMaxRetries(msgId)) { logger.debug({ msgId }, "reached retry limit with new retry manager, clearing"); messageRetryManager.markRetryFailed(msgId); return }
199
+ const retryCount = messageRetryManager.incrementRetryCount(msgId)
200
+ const key = `${msgId}:${msgKey?.participant}`
201
+ await msgRetryCache.set(key, retryCount)
202
+ } else {
203
+ const key = `${msgId}:${msgKey?.participant}`
204
+ let retryCount = (await msgRetryCache.get(key)) || 0
205
+ if (retryCount >= maxMsgRetryCount) { logger.debug({ retryCount, msgId }, "reached retry limit, clearing"); await msgRetryCache.del(key); return }
206
+ retryCount += 1
207
+ await msgRetryCache.set(key, retryCount)
208
+ }
209
+ const key = `${msgId}:${msgKey?.participant}`
210
+ const retryCount = (await msgRetryCache.get(key)) || 1
211
+ const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
212
+ const fromJid = node.attrs.from
213
+ let shouldRecreateSession = false
214
+ let recreateReason = ""
215
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
216
+ try {
217
+ const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
218
+ const hasSession = await signalRepository.validateSession(fromJid)
219
+ let lidJid = null
220
+ if (isPnUser(fromJid)) lidJid = await signalRepository.lidMapping.getLIDForPN(fromJid)
221
+ const result = messageRetryManager.shouldRecreateSession(fromJid, retryCount, hasSession.exists)
222
+ shouldRecreateSession = result.recreate
223
+ recreateReason = result.reason
224
+ if (shouldRecreateSession) {
225
+ logger.debug({ fromJid, lidJid, retryCount, reason: recreateReason }, "recreating session for retry")
226
+ await authState.keys.set({ session: { [sessionId]: null } })
227
+ if (lidJid) {
228
+ const lidSessionId = signalRepository.jidToSignalProtocolAddress(lidJid)
229
+ await authState.keys.set({ session: { [lidSessionId]: null } })
230
+ }
231
+ forceIncludeKeys = true
232
+ }
233
+ } catch (error) { logger.warn({ error, fromJid }, "failed to check session recreation") }
234
+ }
235
+ if (retryCount <= 2) {
236
+ if (messageRetryManager) {
237
+ messageRetryManager.schedulePhoneRequest(msgId, async () => {
238
+ try {
239
+ const requestId = await requestPlaceholderResend(msgKey)
240
+ logger.debug(`sendRetryRequest: requested placeholder resend (${requestId}) for message ${msgId} (scheduled)`)
241
+ } catch (error) { logger.warn({ error, msgId }, "failed to send scheduled phone request") }
242
+ })
243
+ } else {
244
+ const msgId = await requestPlaceholderResend(msgKey)
245
+ logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`)
246
+ }
247
+ }
248
+ const deviceIdentity = encodeSignedDeviceIdentity(account, true)
249
+ await authState.keys.transaction(async () => {
250
+ 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) }] }
251
+ if (node.attrs.recipient) receipt.attrs.recipient = node.attrs.recipient
252
+ if (node.attrs.participant) receipt.attrs.participant = node.attrs.participant
253
+ if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
254
+ const { update, preKeys } = await getNextPreKeys(authState, 1)
255
+ const [keyId] = Object.keys(preKeys)
256
+ const key = preKeys[+keyId]
257
+ const content = receipt.content
258
+ 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 }] })
259
+ ev.emit("creds.update", update)
260
+ }
261
+ await sendNode(receipt)
262
+ logger.info({ msgAttrs: node.attrs, retryCount }, "sent retry receipt")
263
+ }, authState?.creds?.me?.id || "sendRetryRequest")
264
+ }
265
+
266
+ const handleEncryptNotification = async (node) => {
267
+ const from = node.attrs.from
268
+ if (from === S_WHATSAPP_NET) {
269
+ const countChild = getBinaryNodeChild(node, "count")
270
+ const count = +countChild.attrs.value
271
+ const shouldUploadMorePreKeys = count < MIN_PREKEY_COUNT
272
+ logger.debug({ count, shouldUploadMorePreKeys }, "recv pre-key count")
273
+ if (shouldUploadMorePreKeys) await uploadPreKeys()
274
+ } else {
275
+ const identityNode = getBinaryNodeChild(node, 'identity')
276
+ if (identityNode) { logger.info({ jid: from }, 'identity changed') } else { logger.info({ node }, 'unknown encrypt notification') }
277
+ }
278
+ }
279
+
280
+ const handleGroupNotification = (fullNode, child, msg) => {
281
+ const actingParticipantLid = fullNode.attrs.participant
282
+ const actingParticipantPn = fullNode.attrs.participant_pn
283
+ const affectedParticipantLid = getBinaryNodeChild(child, "participant")?.attrs?.jid || actingParticipantLid
284
+ const affectedParticipantPn = getBinaryNodeChild(child, "participant")?.attrs?.phone_number || actingParticipantPn
285
+ switch (child?.tag) {
286
+ case "create":
287
+ const metadata = extractGroupMetadata(child)
288
+ msg.messageStubType = WAMessageStubType.GROUP_CREATE
289
+ msg.messageStubParameters = [metadata.subject]
290
+ msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn }
291
+ ev.emit("chats.upsert", [{ id: metadata.id, name: metadata.subject, conversationTimestamp: metadata.creation }])
292
+ ev.emit("groups.upsert", [{ ...metadata, author: actingParticipantLid, authorPn: actingParticipantPn }])
293
+ break
294
+ case "ephemeral":
295
+ case "not_ephemeral":
296
+ msg.message = { protocolMessage: { type: proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: +(child.attrs.expiration || 0) } }
297
+ break
298
+ case 'modify':
299
+ const modifiedParticipants = getBinaryNodeChildren(child, 'participant').map(({ attrs }) => ({ id: attrs.jid, phoneNumber: isPnUser(attrs.phone_number) ? attrs.phone_number : undefined, lid: isLidUser(attrs.lid) ? attrs.lid : undefined, admin: attrs.type || null }))
300
+ msg.messageStubParameters = modifiedParticipants.map(a => JSON.stringify(a))
301
+ msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER
302
+ break
303
+ case "promote":
304
+ case "demote":
305
+ case "remove":
306
+ case "add":
307
+ case "leave":
308
+ const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`
309
+ msg.messageStubType = WAMessageStubType[stubType]
310
+ 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 }))
311
+ if (participants.length === 1 && (areJidsSameUser(participants[0].id, actingParticipantLid) || areJidsSameUser(participants[0].id, actingParticipantPn)) && child.tag === "remove") msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE
312
+ msg.messageStubParameters = participants.map((a) => JSON.stringify(a))
313
+ break
314
+ case "subject":
315
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_SUBJECT
316
+ msg.messageStubParameters = [child.attrs.subject]
317
+ break
318
+ case "description":
319
+ const description = getBinaryNodeChild(child, "body")?.content?.toString()
320
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_DESCRIPTION
321
+ msg.messageStubParameters = description ? [description] : undefined
322
+ break
323
+ case "announcement":
324
+ case "not_announcement":
325
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_ANNOUNCE
326
+ msg.messageStubParameters = [child.tag === "announcement" ? "on" : "off"]
327
+ break
328
+ case "locked":
329
+ case "unlocked":
330
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_RESTRICT
331
+ msg.messageStubParameters = [child.tag === "locked" ? "on" : "off"]
332
+ break
333
+ case "invite":
334
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_INVITE_LINK
335
+ msg.messageStubParameters = [child.attrs.code]
336
+ break
337
+ case "member_add_mode":
338
+ const addMode = child.content
339
+ if (addMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBER_ADD_MODE; msg.messageStubParameters = [addMode.toString()] }
340
+ break
341
+ case "membership_approval_mode":
342
+ const approvalMode = getBinaryNodeChild(child, "group_join")
343
+ if (approvalMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE; msg.messageStubParameters = [approvalMode.attrs.state] }
344
+ break
345
+ case "created_membership_requests":
346
+ msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
347
+ msg.messageStubParameters = [JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }), "created", child.attrs.request_method]
348
+ break
349
+ case "revoked_membership_requests":
350
+ const isDenied = areJidsSameUser(affectedParticipantLid, actingParticipantLid)
351
+ msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
352
+ msg.messageStubParameters = [JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }), isDenied ? "revoked" : "rejected"]
353
+ break
354
+ }
355
+ }
356
+
357
+ const handlePrivacyTokenNotification = async (node) => {
358
+ const tokensNode = getBinaryNodeChild(node, "tokens")
359
+ const from = jidNormalizedUser(node.attrs.from)
360
+ if (!tokensNode) return
361
+ const tokenNodes = getBinaryNodeChildren(tokensNode, "token")
362
+ for (const tokenNode of tokenNodes) {
363
+ const { attrs, content } = tokenNode
364
+ const type = attrs.type
365
+ const timestamp = attrs.t
366
+ if (type === "trusted_contact" && content instanceof Buffer) {
367
+ logger.debug({ from, timestamp, tcToken: content }, "received trusted contact token")
368
+ await authState.keys.set({ tctoken: { [from]: { token: content, timestamp } } })
369
+ ev.emit("chats.update", [{ id: from, tcToken: content }])
370
+ }
371
+ }
372
+ }
373
+
374
+ const processNotification = async (node) => {
375
+ const result = {}
376
+ const [child] = getAllBinaryNodeChildren(node)
377
+ const nodeType = node.attrs.type
378
+ const from = jidNormalizedUser(node.attrs.from)
379
+ switch (nodeType) {
380
+ case "privacy_token":
381
+ await handlePrivacyTokenNotification(node)
382
+ break
383
+ case "newsletter":
384
+ await handleNewsletterNotification(node)
385
+ break
386
+ case "mex":
387
+ await handleMexNewsletterNotification(node)
388
+ break
389
+ case "w:gp2":
390
+ handleGroupNotification(node, child, result)
391
+ break
392
+ case "mediaretry":
393
+ const event = decodeMediaRetryNode(node)
394
+ ev.emit("messages.media-update", [event])
395
+ break
396
+ case "encrypt":
397
+ await handleEncryptNotification(node)
398
+ break
399
+ case "devices":
400
+ const devices = getBinaryNodeChildren(child, "device")
401
+ if (areJidsSameUser(child.attrs.jid, authState.creds.me.id) || areJidsSameUser(child.attrs.lid, authState.creds.me.lid)) {
402
+ const deviceData = devices.map((d) => ({ id: d.attrs.jid, lid: d.attrs.lid }))
403
+ logger.info({ deviceData }, "my own devices changed")
404
+ }
405
+ break
406
+ case "server_sync":
407
+ const update = getBinaryNodeChild(node, "collection")
408
+ if (update) {
409
+ const name = update.attrs.name
410
+ await resyncAppState([name], false)
411
+ }
412
+ break
413
+ case "picture":
414
+ const setPicture = getBinaryNodeChild(node, "set")
415
+ const delPicture = getBinaryNodeChild(node, "delete")
416
+ ev.emit("contacts.update", [{ id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || "", imgUrl: setPicture ? "changed" : "removed" }])
417
+ if (isJidGroup(from)) {
418
+ const node = setPicture || delPicture
419
+ result.messageStubType = WAMessageStubType.GROUP_CHANGE_ICON
420
+ if (setPicture) result.messageStubParameters = [setPicture.attrs.id]
421
+ result.participant = node?.attrs.author
422
+ result.key = { ...(result.key || {}), participant: setPicture?.attrs.author }
423
+ }
424
+ break
425
+ case "account_sync":
426
+ if (child.tag === "disappearing_mode") {
427
+ const newDuration = +child.attrs.duration
428
+ const timestamp = +child.attrs.t
429
+ logger.info({ newDuration }, "updated account disappearing mode")
430
+ ev.emit("creds.update", { accountSettings: { ...authState.creds.accountSettings, defaultDisappearingMode: { ephemeralExpiration: newDuration, ephemeralSettingTimestamp: timestamp } } })
431
+ } else if (child.tag === "blocklist") {
432
+ const blocklists = getBinaryNodeChildren(child, "item")
433
+ for (const { attrs } of blocklists) {
434
+ const blocklist = [attrs.jid]
435
+ const type = attrs.action === "block" ? "add" : "remove"
436
+ ev.emit("blocklist.update", { blocklist, type })
437
+ }
438
+ }
439
+ break
440
+ case "link_code_companion_reg":
441
+ const linkCodeCompanionReg = getBinaryNodeChild(node, "link_code_companion_reg")
442
+ const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, "link_code_pairing_ref"))
443
+ const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, "primary_identity_pub"))
444
+ const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, "link_code_pairing_wrapped_primary_ephemeral_pub"))
445
+ const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped)
446
+ const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey)
447
+ const random = randomBytes(32)
448
+ const linkCodeSalt = randomBytes(32)
449
+ const linkCodePairingExpanded = hkdf(companionSharedKey, 32, { salt: linkCodeSalt, info: "link_code_pairing_key_bundle_encryption_key" })
450
+ const encryptPayload = Buffer.concat([Buffer.from(authState.creds.signedIdentityKey.public), primaryIdentityPublicKey, random])
451
+ const encryptIv = randomBytes(12)
452
+ const encrypted = aesEncryptGCM(encryptPayload, linkCodePairingExpanded, encryptIv, Buffer.alloc(0))
453
+ const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted])
454
+ const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey)
455
+ const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random])
456
+ authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: "adv_secret" })).toString("base64")
457
+ 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 }] }] })
458
+ authState.creds.registered = true
459
+ ev.emit("creds.update", authState.creds)
460
+ break
461
+ }
462
+ if (Object.keys(result).length) return result
463
+ }
464
+
465
+ async function decipherLinkPublicKey(data) {
466
+ const buffer = toRequiredBuffer(data)
467
+ const salt = buffer.slice(0, 32)
468
+ const secretKey = await derivePairingCodeKey(authState.creds.pairingCode, salt)
469
+ const iv = buffer.slice(32, 48)
470
+ const payload = buffer.slice(48, 80)
471
+ return aesDecryptCTR(payload, secretKey, iv)
472
+ }
473
+
474
+ function toRequiredBuffer(data) {
475
+ if (data === undefined) throw new Boom("Invalid buffer", { statusCode: 400 })
476
+ return data instanceof Buffer ? data : Buffer.from(data)
477
+ }
478
+
479
+ const willSendMessageAgain = async (id, participant) => {
480
+ const key = `${id}:${participant}`
481
+ const retryCount = (await msgRetryCache.get(key)) || 0
482
+ return retryCount < maxMsgRetryCount
483
+ }
484
+
485
+ const updateSendMessageAgainCount = async (id, participant) => {
486
+ const key = `${id}:${participant}`
487
+ const newValue = ((await msgRetryCache.get(key)) || 0) + 1
488
+ await msgRetryCache.set(key, newValue)
489
+ }
490
+
491
+ const sendMessagesAgain = async (key, ids, retryNode) => {
492
+ const remoteJid = key.remoteJid
493
+ const participant = key.participant || remoteJid
494
+ const retryCount = +retryNode.attrs.count || 1
495
+ const msgs = []
496
+ for (const id of ids) {
497
+ let msg
498
+ if (messageRetryManager) {
499
+ const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id)
500
+ if (cachedMsg) { msg = cachedMsg.message; logger.debug({ jid: remoteJid, id }, "found message in retry cache"); messageRetryManager.markRetrySuccess(id) }
501
+ }
502
+ if (!msg) {
503
+ msg = await getMessage({ ...key, id })
504
+ if (msg) { logger.debug({ jid: remoteJid, id }, "found message via getMessage"); if (messageRetryManager) messageRetryManager.markRetrySuccess(id) }
505
+ }
506
+ msgs.push(msg)
507
+ }
508
+ const sendToAll = !jidDecode(participant)?.device
509
+ let shouldRecreateSession = false
510
+ let recreateReason = ""
511
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
512
+ try {
513
+ const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
514
+ const hasSession = await signalRepository.validateSession(participant)
515
+ const result = messageRetryManager.shouldRecreateSession(participant, retryCount, hasSession.exists)
516
+ shouldRecreateSession = result.recreate
517
+ recreateReason = result.reason
518
+ if (shouldRecreateSession) { logger.debug({ participant, retryCount, reason: recreateReason }, "recreating session for outgoing retry"); await authState.keys.set({ session: { [sessionId]: null } }) }
519
+ } catch (error) { logger.warn({ error, participant }, "failed to check session recreation for outgoing retry") }
520
+ }
521
+ await assertSessions([participant], false);
522
+ if (isJidGroup(remoteJid)) await authState.keys.set({ "sender-key-memory": { [remoteJid]: null } })
523
+ logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason }, "preparing retry recp")
524
+ for (const [i, msg] of msgs.entries()) {
525
+ if (!ids[i]) continue
526
+ if (msg && (await willSendMessageAgain(ids[i], participant))) {
527
+ await updateSendMessageAgainCount(ids[i], participant)
528
+ const msgRelayOpts = { messageId: ids[i] }
529
+ if (sendToAll) msgRelayOpts.useUserDevicesCache = false
530
+ else msgRelayOpts.participant = { jid: participant, count: +retryNode.attrs.count }
531
+ await relayMessage(key.remoteJid, msg, msgRelayOpts)
532
+ } else logger.debug({ jid: key.remoteJid, id: ids[i] }, "recv retry request, but message not available")
533
+ }
534
+ }
535
+
536
+ const handleReceipt = async (node) => {
537
+ const { attrs, content } = node
538
+ const isLid = attrs.from.includes("lid")
539
+ const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id)
540
+ const remoteJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient
541
+ const fromMe = !attrs.recipient || ((attrs.type === "retry" || attrs.type === "sender") && isNodeFromMe)
542
+ const key = { remoteJid, id: "", fromMe, participant: attrs.participant }
543
+ if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) { logger.debug({ remoteJid }, "ignoring receipt from jid"); await sendMessageAck(node); return }
544
+ const ids = [attrs.id]
545
+ if (Array.isArray(content)) {
546
+ const items = getBinaryNodeChildren(content[0], "item")
547
+ ids.push(...items.map((i) => i.attrs.id))
548
+ }
549
+ try {
550
+ await Promise.all([processingMutex.mutex(async () => {
551
+ const status = getStatusFromReceiptType(attrs.type)
552
+ if (typeof status !== "undefined" && (status >= proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)) {
553
+ if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
554
+ if (attrs.participant) {
555
+ const updateKey = status === proto.WebMessageInfo.Status.DELIVERY_ACK ? "receiptTimestamp" : "readTimestamp"
556
+ ev.emit("message-receipt.update", ids.map((id) => ({ key: { ...key, id }, receipt: { userJid: jidNormalizedUser(attrs.participant), [updateKey]: +attrs.t } })))
557
+ }
558
+ } else ev.emit("messages.update", ids.map((id) => ({ key: { ...key, id }, update: { status, messageTimestamp: toNumber(+(attrs.t ?? 0)) } })))
559
+ }
560
+ if (attrs.type === "retry") {
561
+ key.participant = key.participant || attrs.from
562
+ const retryNode = getBinaryNodeChild(node, "retry")
563
+ if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
564
+ if (key.fromMe) {
565
+ try {
566
+ await updateSendMessageAgainCount(ids[0], key.participant)
567
+ logger.debug({ attrs, key }, "recv retry request")
568
+ await sendMessagesAgain(key, ids, retryNode)
569
+ } catch (error) { logger.error({ key, ids, trace: error instanceof Error ? error.stack : "Unknown error" }, "error in sending message again") }
570
+ } else logger.info({ attrs, key }, "recv retry for not fromMe message")
571
+ } else {
572
+ logger.info({ attrs, key, participant: key.participant }, "retry limit exhausted - clearing broken session")
573
+ try {
574
+ await signalRepository.deleteSession([key.participant])
575
+ logger.debug({ participant: key.participant }, "deleted stale session for retry-exhausted participant")
576
+ const retryKey = `${ids[0]}:${key.participant}`
577
+ await msgRetryCache.del(retryKey)
578
+ logger.debug({ retryKey }, "cleared retry count cache")
579
+ } catch (err) {
580
+ logger.error({ err, participant: key.participant }, "failed to clear session/cache at retry exhaustion")
581
+ }
582
+ }
583
+ }
584
+ })])
585
+ } finally {
586
+ await sendMessageAck(node)
587
+ }
588
+ }
589
+
590
+ const handleNotification = async (node) => {
591
+ const remoteJid = node.attrs.from
592
+ if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) { logger.debug({ remoteJid, id: node.attrs.id }, "ignored notification"); await sendMessageAck(node); return }
593
+ try {
594
+ await Promise.all([notificationMutex.mutex(async () => {
595
+ const msg = await processNotification(node)
596
+ if (msg) {
597
+ const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id)
598
+ const { senderAlt: participantAlt, addressingMode } = extractAddressingContext(node)
599
+ msg.key = { remoteJid, fromMe, participant: node.attrs.participant, participantAlt, addressingMode, id: node.attrs.id, ...(msg.key || {}) }
600
+ msg.participant ?? (msg.participant = node.attrs.participant)
601
+ msg.messageTimestamp = +node.attrs.t
602
+ const fullMsg = proto.WebMessageInfo.fromObject(msg)
603
+ await upsertMessage(fullMsg, "append")
604
+ }
605
+ })])
606
+ } finally {
607
+ await sendMessageAck(node)
608
+ }
609
+ }
610
+
611
+ const handleMessage = async (node) => {
612
+ 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 }
613
+ const encNode = getBinaryNodeChild(node, "enc")
614
+ if (encNode && encNode.attrs.type === "msmsg") { logger.debug({ key: node.attrs.key }, "ignored msmsg"); await sendMessageAck(node, NACK_REASONS.MissingMessageSecret); return }
615
+ const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || "", signalRepository, logger)
616
+ const alt = msg.key.participantAlt || msg.key.remoteJidAlt
617
+ if (!!alt) {
618
+ const altServer = jidDecode(alt)?.server
619
+ const primaryJid = msg.key.participant || msg.key.remoteJid
620
+ if (altServer === "lid") { if (!(await signalRepository.lidMapping.getPNForLID(alt))) { await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]); await signalRepository.migrateSession(primaryJid, alt) } }
621
+ else { await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]); await signalRepository.migrateSession(alt, primaryJid) }
622
+ }
623
+ 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") }
624
+ try {
625
+ await messageMutex.mutex(async () => {
626
+ await decrypt()
627
+ if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== "peer") {
628
+ if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) return sendMessageAck(node, NACK_REASONS.ParsingError)
629
+ if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
630
+ const unavailableNode = getBinaryNodeChild(node, "unavailable")
631
+ const unavailableType = unavailableNode?.attrs?.type
632
+ 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) }
633
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp)
634
+ if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) { logger.debug({ msgId: msg.key.id, messageAge }, "skipping placeholder resend for old message"); return sendMessageAck(node) }
635
+ const cleanKey = { remoteJid: msg.key.remoteJid, fromMe: msg.key.fromMe, id: msg.key.id, participant: msg.key.participant }
636
+ const msgData = { key: msg.key, messageTimestamp: msg.messageTimestamp, pushName: msg.pushName, participant: msg.participant, verifiedBizName: msg.verifiedBizName }
637
+ 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") })
638
+ await sendMessageAck(node)
639
+ } else {
640
+ if ((msg.messageStubParameters?.[0] || '').includes('InvalidPreKeyId')) { logger.debug({ msgId: msg.key.id, remoteJid: msg.key.remoteJid }, 'skipping retry for InvalidPreKeyId, sender must re-establish session'); return sendMessageAck(node, NACK_REASONS.UnhandledError) }
641
+ if (isJidStatusBroadcast(msg.key.remoteJid)) {
642
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp)
643
+ 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) }
644
+ }
645
+ retryMutex.mutex(async () => {
646
+ try {
647
+ if (!ws.isOpen) { logger.debug({ node }, "Connection closed, skipping retry"); return }
648
+ const encNode = getBinaryNodeChild(node, "enc")
649
+ await sendRetryRequest(node, true)
650
+ if (retryRequestDelayMs) await delay(retryRequestDelayMs)
651
+ } catch (err) {
652
+ logger.error({ err }, "Retry mechanism failed")
653
+ try {
654
+ const encNode = getBinaryNodeChild(node, "enc")
655
+ await sendRetryRequest(node, !encNode)
656
+ } catch (retryErr) { logger.error({ retryErr }, "Final retry attempt failed") }
657
+ }
658
+ await sendMessageAck(node, NACK_REASONS.UnhandledError)
659
+ })
660
+ }
661
+ } else {
662
+ if (messageRetryManager && msg.key.id) messageRetryManager.cancelPendingPhoneRequest(msg.key.id)
663
+ const isNewsletter = isJidNewsletter(msg.key.remoteJid)
664
+ if (!isNewsletter) {
665
+ let type = undefined
666
+ let participant = msg.key.participant
667
+ if (category === "peer") type = "peer_msg"
668
+ else if (msg.key.fromMe) { type = "sender"; if (isLidUser(msg.key.remoteJid) || isLidUser(msg.key.remoteJidAlt)) participant = author }
669
+ else if (!sendActiveReceipts) type = "inactive"
670
+ await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type)
671
+ const isAnyHistoryMsg = getHistoryMsg(msg.message)
672
+ if (isAnyHistoryMsg) {
673
+ const jid = jidNormalizedUser(msg.key.remoteJid)
674
+ await sendReceipt(jid, undefined, [msg.key.id], "hist_sync")
675
+ }
676
+ } else {
677
+ await sendMessageAck(node)
678
+ logger.debug({ key: msg.key }, "processed newsletter message without receipts")
679
+ }
680
+ }
681
+ cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid)
682
+ await upsertMessage(msg, node.attrs.offline ? "append" : "notify")
683
+ })
684
+ } catch (error) { logger.error({ error, stack: error?.stack, msg: error?.message || String(error), node: binaryNodeToString(node) }, "error in handling message") }
685
+ }
686
+
687
+ const handleCall = async (node) => {
688
+ const { attrs } = node
689
+ const [infoChild] = getAllBinaryNodeChildren(node)
690
+ const status = getCallStatusFromNode(infoChild)
691
+ if (!infoChild) throw new Boom("Missing call info in call node")
692
+ const callId = infoChild.attrs["call-id"]
693
+ const from = infoChild.attrs.from || infoChild.attrs["call-creator"]
694
+ const call = { chatId: attrs.from, from, callerPn: infoChild.attrs["caller_pn"], id: callId, date: new Date(+attrs.t * 1000), offline: !!attrs.offline, status }
695
+ 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) }
696
+ const existingCall = await callOfferCache.get(call.id)
697
+ if (existingCall) { call.isVideo = existingCall.isVideo; call.isGroup = existingCall.isGroup; call.callerPn = call.callerPn || existingCall.callerPn }
698
+ if (status === "reject" || status === "accept" || status === "timeout" || status === "terminate") await callOfferCache.del(call.id)
699
+ ev.emit("call", [call])
700
+ await sendMessageAck(node)
701
+ }
702
+
703
+ const handleBadAck = async ({ attrs }) => {
704
+ const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id }
705
+ if (attrs.error) {
706
+ logger.warn({ attrs }, "received error in ack")
707
+ ev.emit("messages.update", [{ key, update: { status: WAMessageStatus.ERROR, messageStubParameters: [attrs.error] } }])
708
+ }
709
+ }
710
+
711
+ const processNodeWithBuffer = async (node, identifier, exec) => {
712
+ ev.buffer()
713
+ await execTask()
714
+ ev.flush()
715
+ function execTask() { return exec(node, false).catch((err) => onUnexpectedError(err, identifier)) }
716
+ }
717
+
718
+ const yieldToEventLoop = () => new Promise(resolve => setImmediate(resolve))
719
+
720
+ const makeOfflineNodeProcessor = () => {
721
+ const nodeProcessorMap = new Map([["message", handleMessage], ["call", handleCall], ["receipt", handleReceipt], ["notification", handleNotification]])
722
+ const nodes = []
723
+ let isProcessing = false
724
+ const BATCH_SIZE = 10
725
+ const enqueue = (type, node) => {
726
+ nodes.push({ type, node })
727
+ if (isProcessing) return
728
+ isProcessing = true
729
+ const promise = async () => {
730
+ let processedInBatch = 0
731
+ while (nodes.length && ws.isOpen) {
732
+ const { type, node } = nodes.shift()
733
+ const nodeProcessor = nodeProcessorMap.get(type)
734
+ if (!nodeProcessor) { onUnexpectedError(new Error(`unknown offline node type: ${type}`), "processing offline node"); continue }
735
+ await nodeProcessor(node)
736
+ processedInBatch++
737
+ if (processedInBatch >= BATCH_SIZE) { processedInBatch = 0; await yieldToEventLoop() }
738
+ }
739
+ isProcessing = false
740
+ }
741
+ promise().catch((error) => onUnexpectedError(error, "processing offline nodes"))
742
+ }
743
+ return { enqueue }
744
+ }
745
+
746
+ const offlineNodeProcessor = makeOfflineNodeProcessor()
747
+
748
+ const processNode = async (type, node, identifier, exec) => {
749
+ const isOffline = !!node.attrs.offline
750
+ if (isOffline) offlineNodeProcessor.enqueue(type, node)
751
+ else await processNodeWithBuffer(node, identifier, exec)
752
+ }
753
+
754
+ ws.on("CB:message", async (node) => { await processNode("message", node, "processing message", handleMessage) })
755
+ ws.on("CB:call", async (node) => { await processNode("call", node, "handling call", handleCall) })
756
+ ws.on("CB:receipt", async (node) => { await processNode("receipt", node, "handling receipt", handleReceipt) })
757
+ ws.on("CB:notification", async (node) => { await processNode("notification", node, "handling notification", handleNotification) })
758
+ ws.on("CB:ack,class:message", (node) => { handleBadAck(node).catch((error) => onUnexpectedError(error, "handling bad ack")) })
759
+
760
+ ev.on("call", async ([call]) => {
761
+ if (!call) return
762
+ if (call.status === "timeout" || (call.status === "offer" && call.isGroup)) {
763
+ const msg = { key: { remoteJid: call.chatId, id: call.id, fromMe: false }, messageTimestamp: unixTimestampSeconds(call.date) }
764
+ if (call.status === "timeout") {
765
+ if (call.isGroup) msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_GROUP_VIDEO : WAMessageStubType.CALL_MISSED_GROUP_VOICE
766
+ else msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE
767
+ } else msg.message = { call: { callKey: Buffer.from(call.id) } }
768
+ const protoMsg = proto.WebMessageInfo.fromObject(msg)
769
+ await upsertMessage(protoMsg, call.offline ? "append" : "notify")
770
+ }
771
+ })
772
+
773
+ ev.on("connection.update", ({ isOnline }) => {
774
+ if (typeof isOnline !== "undefined") { sendActiveReceipts = isOnline; logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`) }
775
+ })
776
+
777
+ return { ...sock, sendMessageAck, sendRetryRequest, rejectCall, offerCall, fetchMessageHistory, requestPlaceholderResend, messageRetryManager }
744
778
  }