@nexustechpro/baileys 2.0.2 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +924 -1299
  3. package/WAProto/index.js +22 -18
  4. package/lib/Defaults/baileys-version.json +6 -2
  5. package/lib/Defaults/index.js +173 -172
  6. package/lib/Signal/libsignal.js +395 -292
  7. package/lib/Signal/lid-mapping.js +264 -171
  8. package/lib/Socket/Client/index.js +2 -2
  9. package/lib/Socket/Client/types.js +10 -10
  10. package/lib/Socket/Client/websocket.js +45 -310
  11. package/lib/Socket/business.js +375 -375
  12. package/lib/Socket/chats.js +916 -963
  13. package/lib/Socket/communities.js +430 -430
  14. package/lib/Socket/groups.js +342 -342
  15. package/lib/Socket/index.js +21 -22
  16. package/lib/Socket/messages-recv.js +963 -743
  17. package/lib/Socket/messages-send.js +273 -321
  18. package/lib/Socket/mex.js +50 -50
  19. package/lib/Socket/newsletter.js +148 -148
  20. package/lib/Socket/nexus-handler.js +296 -247
  21. package/lib/Socket/registration.js +50 -33
  22. package/lib/Socket/socket.js +872 -1201
  23. package/lib/Store/index.js +5 -5
  24. package/lib/Store/make-cache-manager-store.js +81 -81
  25. package/lib/Store/make-in-memory-store.js +416 -416
  26. package/lib/Store/make-ordered-dictionary.js +81 -81
  27. package/lib/Store/object-repository.js +30 -30
  28. package/lib/Types/Auth.js +1 -1
  29. package/lib/Types/Bussines.js +1 -1
  30. package/lib/Types/Call.js +1 -1
  31. package/lib/Types/Chat.js +7 -7
  32. package/lib/Types/Contact.js +1 -1
  33. package/lib/Types/Events.js +1 -1
  34. package/lib/Types/GroupMetadata.js +1 -1
  35. package/lib/Types/Label.js +24 -24
  36. package/lib/Types/LabelAssociation.js +6 -6
  37. package/lib/Types/Message.js +10 -10
  38. package/lib/Types/Newsletter.js +37 -29
  39. package/lib/Types/Product.js +1 -1
  40. package/lib/Types/Signal.js +1 -1
  41. package/lib/Types/Socket.js +2 -2
  42. package/lib/Types/State.js +55 -12
  43. package/lib/Types/USync.js +1 -1
  44. package/lib/Types/index.js +25 -25
  45. package/lib/Utils/auth-utils.js +264 -256
  46. package/lib/Utils/baileys-event-stream.js +55 -55
  47. package/lib/Utils/browser-utils.js +27 -27
  48. package/lib/Utils/business.js +228 -230
  49. package/lib/Utils/chat-utils.js +726 -764
  50. package/lib/Utils/companion-reg-client-utils.js +34 -0
  51. package/lib/Utils/crypto.js +109 -135
  52. package/lib/Utils/decode-wa-message.js +342 -314
  53. package/lib/Utils/event-buffer.js +547 -547
  54. package/lib/Utils/generics.js +295 -297
  55. package/lib/Utils/history.js +91 -83
  56. package/lib/Utils/index.js +25 -20
  57. package/lib/Utils/key-store.js +17 -0
  58. package/lib/Utils/link-preview.js +107 -98
  59. package/lib/Utils/logger.js +2 -2
  60. package/lib/Utils/lt-hash.js +47 -47
  61. package/lib/Utils/make-mutex.js +39 -39
  62. package/lib/Utils/message-retry-manager.js +148 -148
  63. package/lib/Utils/messages-media.js +579 -535
  64. package/lib/Utils/messages.js +821 -706
  65. package/lib/Utils/noise-handler.js +255 -255
  66. package/lib/Utils/pre-key-manager.js +105 -105
  67. package/lib/Utils/process-message.js +430 -412
  68. package/lib/Utils/reporting-utils.js +155 -0
  69. package/lib/Utils/signal.js +191 -159
  70. package/lib/Utils/sync-action-utils.js +33 -0
  71. package/lib/Utils/tc-token-utils.js +162 -0
  72. package/lib/Utils/use-multi-file-auth-state.js +120 -120
  73. package/lib/Utils/validate-connection.js +194 -194
  74. package/lib/WABinary/constants.js +1306 -1300
  75. package/lib/WABinary/decode.js +237 -237
  76. package/lib/WABinary/encode.js +232 -232
  77. package/lib/WABinary/generic-utils.js +252 -211
  78. package/lib/WABinary/index.js +6 -5
  79. package/lib/WABinary/jid-utils.js +279 -95
  80. package/lib/WABinary/types.js +1 -1
  81. package/lib/WAM/BinaryInfo.js +9 -9
  82. package/lib/WAM/constants.js +22852 -22852
  83. package/lib/WAM/encode.js +149 -149
  84. package/lib/WAM/index.js +3 -3
  85. package/lib/WAUSync/Protocols/USyncContactProtocol.js +28 -28
  86. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +53 -53
  87. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -26
  88. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -37
  89. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -50
  90. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -28
  91. package/lib/WAUSync/Protocols/index.js +4 -4
  92. package/lib/WAUSync/USyncQuery.js +93 -93
  93. package/lib/WAUSync/USyncUser.js +22 -22
  94. package/lib/WAUSync/index.js +3 -3
  95. package/lib/index.js +65 -66
  96. package/package.json +172 -143
  97. package/lib/Signal/Group/ciphertext-message.js +0 -12
  98. package/lib/Signal/Group/group-session-builder.js +0 -30
  99. package/lib/Signal/Group/group_cipher.js +0 -100
  100. package/lib/Signal/Group/index.js +0 -12
  101. package/lib/Signal/Group/keyhelper.js +0 -18
  102. package/lib/Signal/Group/sender-chain-key.js +0 -26
  103. package/lib/Signal/Group/sender-key-distribution-message.js +0 -63
  104. package/lib/Signal/Group/sender-key-message.js +0 -66
  105. package/lib/Signal/Group/sender-key-name.js +0 -48
  106. package/lib/Signal/Group/sender-key-record.js +0 -41
  107. package/lib/Signal/Group/sender-key-state.js +0 -84
  108. package/lib/Signal/Group/sender-message-key.js +0 -26
@@ -1,744 +1,964 @@
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, extractE2ESessionFromRetryReceipt, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey, ACCOUNT_RESTRICTED_TEXT, buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveTcTokenJid, resolveIssuanceJid, storeTcTokensFromIqResult, TC_TOKEN_INDEX_KEY } from "../Utils/index.js"
8
+ import { makeMutex } from "../Utils/make-mutex.js"
9
+ import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, getBinaryNodeChildUInt, 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, registerSocketEndHandler, issuePrivacyTokens, fetchAccountReachoutTimelock } = sock
17
+
18
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
19
+ const messageMutex = makeMutex()
20
+ const notificationMutex = 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 (node, errorCode) => {
114
+ const { tag, attrs, content } = node
115
+ const hasUnavailable = !!getBinaryNodeChild({ tag, attrs, content }, 'unavailable')
116
+ const stanza = { tag: 'ack', attrs: { id: attrs.id, to: attrs.from, class: tag } }
117
+ if (errorCode) stanza.attrs.error = errorCode.toString()
118
+ if (attrs.participant) stanza.attrs.participant = attrs.participant
119
+ if (attrs.recipient) stanza.attrs.recipient = attrs.recipient
120
+ // include type always when present (upstream), but also force-include on unavailable/error
121
+ if (attrs.type || hasUnavailable || errorCode) stanza.attrs.type = attrs.type
122
+ // include from for all message-class ACKs (upstream), not just unavailable
123
+ if (tag === 'message' && authState.creds.me?.id) stanza.attrs.from = authState.creds.me.id
124
+ logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, 'sent ack')
125
+ try {
126
+ await sendNode(stanza)
127
+ } catch (error) {
128
+ if (error?.output?.statusCode === 428 || error?.message?.includes('Connection'))
129
+ logger.warn({ id: attrs.id, error: error?.message }, 'Failed to send ACK (connection closed) - message already received')
130
+ else throw error
131
+ }
132
+ }
133
+
134
+ const rejectCall = async (callId, callFrom) => {
135
+ 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 }] }
136
+ await query(stanza)
137
+ }
138
+
139
+ const offerCall = async (toJid, isVideo = false) => {
140
+ const callId = randomBytes(16).toString('hex').toUpperCase().substring(0, 64)
141
+ const offerContent = []
142
+
143
+ offerContent.push({ tag: 'audio', attrs: { enc: 'opus', rate: '16000' }, content: undefined })
144
+ offerContent.push({ tag: 'audio', attrs: { enc: 'opus', rate: '8000' }, content: undefined })
145
+
146
+ if (isVideo) {
147
+ offerContent.push({
148
+ tag: 'video',
149
+ attrs: {
150
+ orientation: '0',
151
+ screen_width: '1920',
152
+ screen_height: '1080',
153
+ device_orientation: '0',
154
+ enc: 'vp8',
155
+ dec: 'vp8'
156
+ }
157
+ })
158
+ }
159
+
160
+ offerContent.push({ tag: 'net', attrs: { medium: '3' }, content: undefined })
161
+ offerContent.push({ tag: 'capability', attrs: { ver: '1' }, content: new Uint8Array([1, 4, 255, 131, 207, 4]) })
162
+ offerContent.push({ tag: 'encopt', attrs: { keygen: '2' }, content: undefined })
163
+
164
+ const encKey = randomBytes(32)
165
+ const devices = (await getUSyncDevices([toJid], true, false))
166
+ .map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device))
167
+
168
+ await assertSessions(devices, true)
169
+
170
+ const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(
171
+ devices,
172
+ { call: { callKey: encKey } },
173
+ {}
174
+ )
175
+
176
+ offerContent.push({ tag: 'destination', attrs: {}, content: destinations })
177
+
178
+ if (shouldIncludeDeviceIdentity) {
179
+ offerContent.push({
180
+ tag: 'device-identity',
181
+ attrs: {},
182
+ content: encodeSignedDeviceIdentity(authState.creds.account, true)
183
+ })
184
+ }
185
+
186
+ await query({
187
+ tag: 'call',
188
+ attrs: { to: toJid },
189
+ content: [{
190
+ tag: 'offer',
191
+ attrs: { 'call-id': callId, 'call-creator': authState.creds.me.id },
192
+ content: offerContent
193
+ }]
194
+ })
195
+
196
+ return { callId, toJid, isVideo }
197
+ }
198
+
199
+ const sendRetryRequest = async (node, forceIncludeKeys = false) => {
200
+ const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || "")
201
+ const { key: msgKey } = fullMessage
202
+ const msgId = msgKey.id
203
+ if (messageRetryManager) {
204
+ if (messageRetryManager.hasExceededMaxRetries(msgId)) { logger.debug({ msgId }, "reached retry limit with new retry manager, clearing"); messageRetryManager.markRetryFailed(msgId); return }
205
+ const retryCount = messageRetryManager.incrementRetryCount(msgId)
206
+ const key = `${msgId}:${msgKey?.participant}`
207
+ await msgRetryCache.set(key, retryCount)
208
+ } else {
209
+ const key = `${msgId}:${msgKey?.participant}`
210
+ let retryCount = (await msgRetryCache.get(key)) || 0
211
+ if (retryCount >= maxMsgRetryCount) { logger.debug({ retryCount, msgId }, "reached retry limit, clearing"); await msgRetryCache.del(key); return }
212
+ retryCount += 1
213
+ await msgRetryCache.set(key, retryCount)
214
+ }
215
+ const key = `${msgId}:${msgKey?.participant}`
216
+ const retryCount = (await msgRetryCache.get(key)) || 1
217
+ const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
218
+ const fromJid = node.attrs.from
219
+ let shouldRecreateSession = false
220
+ let recreateReason = ""
221
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
222
+ try {
223
+ const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
224
+ const hasSession = await signalRepository.validateSession(fromJid)
225
+ let lidJid = null
226
+ if (isPnUser(fromJid)) lidJid = await signalRepository.lidMapping.getLIDForPN(fromJid)
227
+ const result = messageRetryManager.shouldRecreateSession(fromJid, retryCount, hasSession.exists)
228
+ shouldRecreateSession = result.recreate
229
+ recreateReason = result.reason
230
+ if (shouldRecreateSession) {
231
+ logger.debug({ fromJid, lidJid, retryCount, reason: recreateReason }, "recreating session for retry")
232
+ await authState.keys.set({ session: { [sessionId]: null } })
233
+ if (lidJid) {
234
+ const lidSessionId = signalRepository.jidToSignalProtocolAddress(lidJid)
235
+ await authState.keys.set({ session: { [lidSessionId]: null } })
236
+ }
237
+ forceIncludeKeys = true
238
+ }
239
+ } catch (error) { logger.warn({ error, fromJid }, "failed to check session recreation") }
240
+ }
241
+ if (retryCount <= 2) {
242
+ if (messageRetryManager) {
243
+ messageRetryManager.schedulePhoneRequest(msgId, async () => {
244
+ try {
245
+ const requestId = await requestPlaceholderResend(msgKey)
246
+ logger.debug(`sendRetryRequest: requested placeholder resend (${requestId}) for message ${msgId} (scheduled)`)
247
+ } catch (error) { logger.warn({ error, msgId }, "failed to send scheduled phone request") }
248
+ })
249
+ } else {
250
+ const msgId = await requestPlaceholderResend(msgKey)
251
+ logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`)
252
+ }
253
+ }
254
+ const deviceIdentity = encodeSignedDeviceIdentity(account, true)
255
+ await authState.keys.transaction(async () => {
256
+ 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) }] }
257
+ if (node.attrs.recipient) receipt.attrs.recipient = node.attrs.recipient
258
+ if (node.attrs.participant) receipt.attrs.participant = node.attrs.participant
259
+ if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
260
+ const { update, preKeys } = await getNextPreKeys(authState, 1)
261
+ const [keyId] = Object.keys(preKeys)
262
+ const key = preKeys[+keyId]
263
+ const content = receipt.content
264
+ 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 }] })
265
+ ev.emit("creds.update", update)
266
+ }
267
+ await sendNode(receipt)
268
+ logger.info({ msgAttrs: node.attrs, retryCount }, "sent retry receipt")
269
+ }, authState?.creds?.me?.id || "sendRetryRequest")
270
+ }
271
+
272
+ const reissueTcTokenAfterIdentityChange = (from) => {
273
+ void (async () => {
274
+ const normalizedJid = jidNormalizedUser(from)
275
+ const tcJid = await resolveTcTokenJid(normalizedJid, getLIDForPN)
276
+ const tcTokenData = await authState.keys.get('tctoken', [tcJid])
277
+ const senderTs = tcTokenData?.[tcJid]?.senderTimestamp
278
+ if (senderTs === null || senderTs === undefined || isTcTokenExpired(senderTs)) return
279
+ logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken')
280
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
281
+ const issueJid = await resolveIssuanceJid(normalizedJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID)
282
+ const result = await issuePrivacyTokens([issueJid], senderTs)
283
+ await storeTcTokensFromIqResult({ result, fallbackJid: tcJid, keys: authState.keys, getLIDForPN, onNewJidStored: trackTcTokenJid })
284
+ })().catch(err => logger.debug({ jid: from, err: err?.message }, 'failed to re-issue tctoken after identity change'))
285
+ }
286
+
287
+ const handleEncryptNotification = async (node) => {
288
+ const from = node.attrs.from
289
+ if (from === S_WHATSAPP_NET) {
290
+ const stanzaId = node.attrs.id
291
+ if (stanzaId && inFlightPreKeyLow.has(stanzaId)) return
292
+ const countChild = getBinaryNodeChild(node, "count")
293
+ const count = +countChild.attrs.value
294
+ const shouldUploadMorePreKeys = count < MIN_PREKEY_COUNT
295
+ logger.debug({ count, shouldUploadMorePreKeys }, "recv pre-key count")
296
+ if (shouldUploadMorePreKeys) {
297
+ if (stanzaId) inFlightPreKeyLow.add(stanzaId)
298
+ try { await uploadPreKeys() } finally { if (stanzaId) inFlightPreKeyLow.delete(stanzaId) }
299
+ }
300
+ } else {
301
+ const result = await handleIdentityChange(node, { meId: authState.creds.me?.id, meLid: authState.creds.me?.lid, validateSession: signalRepository.validateSession, assertSessions, debounceCache: identityAssertDebounce, logger, onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange })
302
+ if (result.action === 'no_identity_node') logger.info({ node }, 'unknown encrypt notification')
303
+ }
304
+ }
305
+
306
+ const inFlightPreKeyLow = new Set()
307
+
308
+ const handleGroupNotification = (fullNode, child, msg) => {
309
+ const actingParticipantLid = fullNode.attrs.participant
310
+ const actingParticipantPn = fullNode.attrs.participant_pn
311
+ const affectedParticipantLid = getBinaryNodeChild(child, "participant")?.attrs?.jid || actingParticipantLid
312
+ const affectedParticipantPn = getBinaryNodeChild(child, "participant")?.attrs?.phone_number || actingParticipantPn
313
+ switch (child?.tag) {
314
+ case "create":
315
+ const metadata = extractGroupMetadata(child)
316
+ msg.messageStubType = WAMessageStubType.GROUP_CREATE
317
+ msg.messageStubParameters = [metadata.subject]
318
+ msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn }
319
+ ev.emit("chats.upsert", [{ id: metadata.id, name: metadata.subject, conversationTimestamp: metadata.creation }])
320
+ ev.emit("groups.upsert", [{ ...metadata, author: actingParticipantLid, authorPn: actingParticipantPn }])
321
+ break
322
+ case "ephemeral":
323
+ case "not_ephemeral":
324
+ msg.message = { protocolMessage: { type: proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: +(child.attrs.expiration || 0) } }
325
+ break
326
+ case 'modify':
327
+ 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 }))
328
+ msg.messageStubParameters = modifiedParticipants.map(a => JSON.stringify(a))
329
+ msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER
330
+ break
331
+ case "promote":
332
+ case "demote":
333
+ case "remove":
334
+ case "add":
335
+ case "leave":
336
+ const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`
337
+ msg.messageStubType = WAMessageStubType[stubType]
338
+ 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 }))
339
+ if (participants.length === 1 && (areJidsSameUser(participants[0].id, actingParticipantLid) || areJidsSameUser(participants[0].id, actingParticipantPn)) && child.tag === "remove") msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE
340
+ msg.messageStubParameters = participants.map((a) => JSON.stringify(a))
341
+ break
342
+ case "subject":
343
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_SUBJECT
344
+ msg.messageStubParameters = [child.attrs.subject]
345
+ break
346
+ case "description":
347
+ const description = getBinaryNodeChild(child, "body")?.content?.toString()
348
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_DESCRIPTION
349
+ msg.messageStubParameters = description ? [description] : undefined
350
+ break
351
+ case "announcement":
352
+ case "not_announcement":
353
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_ANNOUNCE
354
+ msg.messageStubParameters = [child.tag === "announcement" ? "on" : "off"]
355
+ break
356
+ case "locked":
357
+ case "unlocked":
358
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_RESTRICT
359
+ msg.messageStubParameters = [child.tag === "locked" ? "on" : "off"]
360
+ break
361
+ case "invite":
362
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_INVITE_LINK
363
+ msg.messageStubParameters = [child.attrs.code]
364
+ break
365
+ case "member_add_mode":
366
+ const addMode = child.content
367
+ if (addMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBER_ADD_MODE; msg.messageStubParameters = [addMode.toString()] }
368
+ break
369
+ case "membership_approval_mode":
370
+ const approvalMode = getBinaryNodeChild(child, "group_join")
371
+ if (approvalMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE; msg.messageStubParameters = [approvalMode.attrs.state] }
372
+ break
373
+ case "created_membership_requests":
374
+ msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
375
+ msg.messageStubParameters = [JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }), "created", child.attrs.request_method]
376
+ break
377
+ case "revoked_membership_requests":
378
+ const isDenied = areJidsSameUser(affectedParticipantLid, actingParticipantLid)
379
+ msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
380
+ msg.messageStubParameters = [JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }), isDenied ? "revoked" : "rejected"]
381
+ break
382
+ }
383
+ }
384
+
385
+ const handlePrivacyTokenNotification = async (node) => {
386
+ const tokensNode = getBinaryNodeChild(node, "tokens")
387
+ if (!tokensNode) return
388
+ const from = jidNormalizedUser(node.attrs.from)
389
+ const senderLid = node.attrs.sender_lid && isLidUser(jidNormalizedUser(node.attrs.sender_lid)) ? jidNormalizedUser(node.attrs.sender_lid) : undefined
390
+ const fallbackJid = senderLid ?? (await resolveTcTokenJid(from, getLIDForPN))
391
+ logger.debug({ from, storageJid: fallbackJid }, 'processing privacy token notification')
392
+ await storeTcTokensFromIqResult({ result: node, fallbackJid, keys: authState.keys, getLIDForPN, onNewJidStored: trackTcTokenJid })
393
+ }
394
+
395
+ const handleDevicesNotification = async (node) => {
396
+ const [child] = getAllBinaryNodeChildren(node)
397
+ const from = jidNormalizedUser(node.attrs.from)
398
+ if (!child) { logger.debug({ from }, 'devices notification missing child, skipping'); return }
399
+ const tag = child.tag
400
+ const deviceHash = child.attrs.device_hash
401
+ const devices = getBinaryNodeChildren(child, 'device')
402
+ if (areJidsSameUser(from, authState.creds.me.id) || areJidsSameUser(from, authState.creds.me.lid)) { const deviceJids = devices.map(d => d.attrs.jid); logger.info({ deviceJids }, 'got my own devices') }
403
+ if (!devices.length) { logger.debug({ from, tag }, 'no devices in notification, skipping'); return }
404
+ const decoded = []
405
+ for (const d of devices) {
406
+ const jid = d.attrs.jid
407
+ if (!jid) continue
408
+ const parts = jidDecode(jid)
409
+ if (!parts) { logger.debug({ jid }, 'failed to decode device jid, skipping'); continue }
410
+ decoded.push({ jid, user: parts.user, server: parts.server, device: parts.device })
411
+ }
412
+ if (!decoded.length) return
413
+ await sock.devicesMutex.mutex(async () => {
414
+ const byUser = new Map()
415
+ for (const d of decoded) { const list = byUser.get(d.user) || []; list.push(d); byUser.set(d.user, list) }
416
+ for (const [user, entries] of byUser) {
417
+ if (tag === 'update') { logger.debug({ user }, `${user}'s device list updated, dropping cached devices`); await sock.userDevicesCache?.del(user); continue }
418
+ if (tag === 'remove') await signalRepository.deleteSession(entries.map(e => e.jid))
419
+ const existingCache = (await sock.userDevicesCache?.get(user)) || []
420
+ if (!existingCache.length) { logger.debug({ user, tag }, 'device list not cached, deferring to USync refresh'); continue }
421
+ const affected = new Set(entries.map(e => e.device))
422
+ let updatedDevices
423
+ switch (tag) {
424
+ case 'add':
425
+ logger.info({ deviceHash, count: entries.length }, 'devices added')
426
+ updatedDevices = [...existingCache.filter(d => !affected.has(d.device)), ...entries.map(e => ({ user: e.user, server: e.server, device: e.device }))]
427
+ break
428
+ case 'remove':
429
+ logger.info({ deviceHash, count: entries.length }, 'devices removed')
430
+ updatedDevices = existingCache.filter(d => !affected.has(d.device))
431
+ break
432
+ default: logger.debug({ tag }, 'unknown device list change tag'); continue
433
+ }
434
+ if (updatedDevices.length === 0) await sock.userDevicesCache?.del(user)
435
+ else await sock.userDevicesCache?.set(user, updatedDevices)
436
+ }
437
+ })
438
+ }
439
+
440
+ const processNotification = async (node) => {
441
+ const result = {}
442
+ const [child] = getAllBinaryNodeChildren(node)
443
+ const nodeType = node.attrs.type
444
+ const from = jidNormalizedUser(node.attrs.from)
445
+ switch (nodeType) {
446
+ case "privacy_token":
447
+ await handlePrivacyTokenNotification(node)
448
+ break
449
+ case "newsletter":
450
+ await handleNewsletterNotification(node)
451
+ break
452
+ case "mex":
453
+ await handleMexNewsletterNotification(node)
454
+ break
455
+ case "w:gp2":
456
+ handleGroupNotification(node, child, result)
457
+ break
458
+ case "mediaretry":
459
+ const event = decodeMediaRetryNode(node)
460
+ ev.emit("messages.media-update", [event])
461
+ break
462
+ case "encrypt":
463
+ await handleEncryptNotification(node)
464
+ break
465
+ case "devices":
466
+ try { await handleDevicesNotification(node) } catch (error) { logger.error({ error, node }, 'failed to handle devices notification') }
467
+ break
468
+ case "server_sync":
469
+ const update = getBinaryNodeChild(node, "collection")
470
+ if (update) {
471
+ const name = update.attrs.name
472
+ await resyncAppState([name], false)
473
+ }
474
+ break
475
+ case "picture":
476
+ const setPicture = getBinaryNodeChild(node, "set")
477
+ const delPicture = getBinaryNodeChild(node, "delete")
478
+ ev.emit("contacts.update", [{ id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || "", imgUrl: setPicture ? "changed" : "removed" }])
479
+ if (isJidGroup(from)) {
480
+ const node = setPicture || delPicture
481
+ result.messageStubType = WAMessageStubType.GROUP_CHANGE_ICON
482
+ if (setPicture) result.messageStubParameters = [setPicture.attrs.id]
483
+ result.participant = node?.attrs.author
484
+ result.key = { ...(result.key || {}), participant: setPicture?.attrs.author }
485
+ }
486
+ break
487
+ case "account_sync":
488
+ if (child.tag === "disappearing_mode") {
489
+ const newDuration = +child.attrs.duration
490
+ const timestamp = +child.attrs.t
491
+ logger.info({ newDuration }, "updated account disappearing mode")
492
+ ev.emit("creds.update", { accountSettings: { ...authState.creds.accountSettings, defaultDisappearingMode: { ephemeralExpiration: newDuration, ephemeralSettingTimestamp: timestamp } } })
493
+ } else if (child.tag === "blocklist") {
494
+ const blocklists = getBinaryNodeChildren(child, "item")
495
+ for (const { attrs } of blocklists) {
496
+ const blocklist = [attrs.jid]
497
+ const type = attrs.action === "block" ? "add" : "remove"
498
+ ev.emit("blocklist.update", { blocklist, type })
499
+ }
500
+ }
501
+ break
502
+ case "link_code_companion_reg":
503
+ const linkCodeCompanionReg = getBinaryNodeChild(node, "link_code_companion_reg")
504
+ const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, "link_code_pairing_ref"))
505
+ const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, "primary_identity_pub"))
506
+ const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, "link_code_pairing_wrapped_primary_ephemeral_pub"))
507
+ const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped)
508
+ const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey)
509
+ const random = randomBytes(32)
510
+ const linkCodeSalt = randomBytes(32)
511
+ const linkCodePairingExpanded = hkdf(companionSharedKey, 32, { salt: linkCodeSalt, info: "link_code_pairing_key_bundle_encryption_key" })
512
+ const encryptPayload = Buffer.concat([Buffer.from(authState.creds.signedIdentityKey.public), primaryIdentityPublicKey, random])
513
+ const encryptIv = randomBytes(12)
514
+ const encrypted = aesEncryptGCM(encryptPayload, linkCodePairingExpanded, encryptIv, Buffer.alloc(0))
515
+ const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted])
516
+ const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey)
517
+ const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random])
518
+ authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: "adv_secret" })).toString("base64")
519
+ 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 }] }] })
520
+ authState.creds.registered = true
521
+ ev.emit("creds.update", authState.creds)
522
+ break
523
+ }
524
+ if (Object.keys(result).length) return result
525
+ }
526
+
527
+ const tcTokenKnownJids = new Set()
528
+ const tcTokenIndexLoaded = (async () => {
529
+ try { const jids = await readTcTokenIndex(authState.keys); for (const jid of jids) tcTokenKnownJids.add(jid); logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index') }
530
+ catch (err) { logger.warn({ err: err?.message }, 'failed to load tctoken index') }
531
+ })()
532
+
533
+ let tcTokenIndexTimer
534
+ async function flushTcTokenIndex() {
535
+ if (tcTokenIndexTimer) { clearTimeout(tcTokenIndexTimer); tcTokenIndexTimer = undefined }
536
+ const write = await buildMergedTcTokenIndexWrite(authState.keys, tcTokenKnownJids)
537
+ return authState.keys.set({ tctoken: write })
538
+ }
539
+
540
+ function scheduleTcTokenIndexSave() {
541
+ if (tcTokenIndexTimer) clearTimeout(tcTokenIndexTimer)
542
+ tcTokenIndexTimer = setTimeout(() => { tcTokenIndexTimer = undefined; flushTcTokenIndex().catch(err => logger.warn({ err: err?.message }, 'failed to save tctoken index')) }, 5000)
543
+ }
544
+
545
+ function trackTcTokenJid(jid) {
546
+ if (jid && jid !== TC_TOKEN_INDEX_KEY && !tcTokenKnownJids.has(jid)) { tcTokenKnownJids.add(jid); scheduleTcTokenIndexSave() }
547
+ }
548
+
549
+ async function decipherLinkPublicKey(data) {
550
+ const buffer = toRequiredBuffer(data)
551
+ const salt = buffer.slice(0, 32)
552
+ const secretKey = await derivePairingCodeKey(authState.creds.pairingCode, salt)
553
+ const iv = buffer.slice(32, 48)
554
+ const payload = buffer.slice(48, 80)
555
+ return aesDecryptCTR(payload, secretKey, iv)
556
+ }
557
+
558
+ function toRequiredBuffer(data) {
559
+ if (data === undefined) throw new Boom("Invalid buffer", { statusCode: 400 })
560
+ return data instanceof Buffer ? data : Buffer.from(data)
561
+ }
562
+
563
+ const willSendMessageAgain = async (id, participant) => {
564
+ const key = `${id}:${participant}`
565
+ const retryCount = (await msgRetryCache.get(key)) || 0
566
+ return retryCount < maxMsgRetryCount
567
+ }
568
+
569
+ const updateSendMessageAgainCount = async (id, participant) => {
570
+ const key = `${id}:${participant}`
571
+ const newValue = ((await msgRetryCache.get(key)) || 0) + 1
572
+ await msgRetryCache.set(key, newValue)
573
+ }
574
+
575
+ const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
576
+ const remoteJid = key.remoteJid
577
+ const participant = key.participant || remoteJid
578
+ const retryCount = +retryNode.attrs.count || 1
579
+ const msgId = ids[0]
580
+ const msgs = []
581
+ for (const id of ids) {
582
+ let msg
583
+ if (messageRetryManager) {
584
+ const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id)
585
+ if (cachedMsg) { msg = cachedMsg.message; logger.debug({ jid: remoteJid, id }, "found message in retry cache"); messageRetryManager.markRetrySuccess(id) }
586
+ }
587
+ if (!msg) {
588
+ msg = await getMessage({ ...key, id })
589
+ if (msg) { logger.debug({ jid: remoteJid, id }, "found message via getMessage"); if (messageRetryManager) messageRetryManager.markRetrySuccess(id) }
590
+ }
591
+ msgs.push(msg)
592
+ }
593
+ const sendToAll = !jidDecode(participant)?.device
594
+ const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
595
+ let injectedFromBundle = false
596
+ const bundle = extractE2ESessionFromRetryReceipt(receiptNode)
597
+ if (bundle) {
598
+ try { await signalRepository.injectE2ESession({ jid: participant, session: bundle }); injectedFromBundle = true; logger.debug({ participant, retryCount }, 'injected session from retry receipt key bundle') }
599
+ catch (error) { logger.warn({ error, participant }, 'failed to inject session from retry receipt') }
600
+ }
601
+ if (!injectedFromBundle && typeof signalRepository.getSessionInfo === 'function') {
602
+ const receivedRegId = getBinaryNodeChildUInt(receiptNode, 'registration', 4)
603
+ if (typeof receivedRegId === 'number' && Number.isInteger(receivedRegId)) {
604
+ const info = await signalRepository.getSessionInfo(participant)
605
+ if (info && info.registrationId !== 0 && info.registrationId !== receivedRegId) { logger.info({ participant, stored: info.registrationId, received: receivedRegId }, 'reg id mismatch on retry without bundle, deleting session'); await authState.keys.set({ session: { [sessionId]: null } }) }
606
+ }
607
+ }
608
+ const BASE_KEY_CHECK_RETRY = 2
609
+ if (msgId && messageRetryManager && typeof messageRetryManager.saveBaseKey === 'function') {
610
+ const info = typeof signalRepository.getSessionInfo === 'function' ? await signalRepository.getSessionInfo(participant) : undefined
611
+ if (info) {
612
+ if (retryCount === BASE_KEY_CHECK_RETRY) messageRetryManager.saveBaseKey(sessionId, msgId, info.baseKey)
613
+ else if (retryCount > BASE_KEY_CHECK_RETRY) {
614
+ if (messageRetryManager.hasSameBaseKey(sessionId, msgId, info.baseKey)) { logger.warn({ participant, retryCount }, 'base key collision on retry, forcing fresh session'); await authState.keys.set({ session: { [sessionId]: null } }) }
615
+ messageRetryManager.deleteBaseKey(sessionId, msgId)
616
+ }
617
+ }
618
+ }
619
+ let shouldRecreateSession = false
620
+ let recreateReason = ""
621
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
622
+ try {
623
+ const hasSession = await signalRepository.validateSession(participant)
624
+ const result = messageRetryManager.shouldRecreateSession(participant, retryCount, hasSession.exists)
625
+ shouldRecreateSession = result.recreate
626
+ recreateReason = result.reason
627
+ if (shouldRecreateSession) { logger.debug({ participant, retryCount, reason: recreateReason }, "recreating session for outgoing retry"); await authState.keys.set({ session: { [sessionId]: null } }) }
628
+ } catch (error) { logger.warn({ error, participant }, "failed to check session recreation for outgoing retry") }
629
+ }
630
+ if (!injectedFromBundle) await assertSessions([participant], true)
631
+ if (isJidGroup(remoteJid)) await authState.keys.set({ "sender-key-memory": { [remoteJid]: null } })
632
+ logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason, injectedFromBundle }, "preparing retry recp")
633
+ for (const [i, msg] of msgs.entries()) {
634
+ if (!ids[i]) continue
635
+ if (msg && (await willSendMessageAgain(ids[i], participant))) {
636
+ await updateSendMessageAgainCount(ids[i], participant)
637
+ const msgRelayOpts = { messageId: ids[i] }
638
+ if (sendToAll) msgRelayOpts.useUserDevicesCache = false
639
+ else msgRelayOpts.participant = { jid: participant, count: +retryNode.attrs.count }
640
+ await relayMessage(key.remoteJid, msg, msgRelayOpts)
641
+ } else logger.debug({ jid: key.remoteJid, id: ids[i] }, "recv retry request, but message not available")
642
+ }
643
+ }
644
+
645
+ const handleReceipt = async (node) => {
646
+ const { attrs, content } = node
647
+ const isLid = attrs.from.includes("lid")
648
+ const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id)
649
+ const remoteJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient
650
+ const fromMe = !attrs.recipient || ((attrs.type === "retry" || attrs.type === "sender") && isNodeFromMe)
651
+ const key = { remoteJid, id: "", fromMe, participant: attrs.participant }
652
+ if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) { logger.debug({ remoteJid }, "ignoring receipt from jid"); await sendMessageAck(node); return }
653
+ const ids = [attrs.id]
654
+ if (Array.isArray(content)) {
655
+ const items = getBinaryNodeChildren(content[0], "item")
656
+ ids.push(...items.map((i) => i.attrs.id))
657
+ }
658
+ try {
659
+ await Promise.all([processingMutex.mutex(async () => {
660
+ const status = getStatusFromReceiptType(attrs.type)
661
+ if (typeof status !== "undefined" && (status >= proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)) {
662
+ if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
663
+ if (attrs.participant) {
664
+ const updateKey = status === proto.WebMessageInfo.Status.DELIVERY_ACK ? "receiptTimestamp" : "readTimestamp"
665
+ ev.emit("message-receipt.update", ids.map((id) => ({ key: { ...key, id }, receipt: { userJid: jidNormalizedUser(attrs.participant), [updateKey]: +attrs.t } })))
666
+ }
667
+ } else ev.emit("messages.update", ids.map((id) => ({ key: { ...key, id }, update: { status, messageTimestamp: toNumber(+(attrs.t ?? 0)) } })))
668
+ }
669
+ if (attrs.type === "retry") {
670
+ key.participant = key.participant || attrs.from
671
+ const retryNode = getBinaryNodeChild(node, "retry")
672
+ if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
673
+ if (key.fromMe) {
674
+ try {
675
+ await updateSendMessageAgainCount(ids[0], key.participant)
676
+ logger.debug({ attrs, key }, "recv retry request")
677
+ await sendMessagesAgain(key, ids, retryNode, node)
678
+ } catch (error) { logger.error({ key, ids, trace: error instanceof Error ? error.stack : "Unknown error" }, "error in sending message again") }
679
+ } else logger.info({ attrs, key }, "recv retry for not fromMe message")
680
+ } else {
681
+ logger.info({ attrs, key, participant: key.participant }, "retry limit exhausted - clearing broken session")
682
+ try {
683
+ await signalRepository.deleteSession([key.participant])
684
+ logger.debug({ participant: key.participant }, "deleted stale session for retry-exhausted participant")
685
+ const retryKey = `${ids[0]}:${key.participant}`
686
+ await msgRetryCache.del(retryKey)
687
+ logger.debug({ retryKey }, "cleared retry count cache")
688
+ } catch (err) {
689
+ logger.error({ err, participant: key.participant }, "failed to clear session/cache at retry exhaustion")
690
+ }
691
+ }
692
+ }
693
+ })])
694
+ } finally {
695
+ await sendMessageAck(node)
696
+ }
697
+ }
698
+
699
+ const handleNotification = async (node) => {
700
+ const remoteJid = node.attrs.from
701
+ if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) { logger.debug({ remoteJid, id: node.attrs.id }, "ignored notification"); await sendMessageAck(node); return }
702
+ try {
703
+ await Promise.all([notificationMutex.mutex(async () => {
704
+ const msg = await processNotification(node)
705
+ if (msg) {
706
+ const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id)
707
+ const { senderAlt: participantAlt, addressingMode } = extractAddressingContext(node)
708
+ msg.key = { remoteJid, fromMe, participant: node.attrs.participant, participantAlt, addressingMode, id: node.attrs.id, ...(msg.key || {}) }
709
+ msg.participant ?? (msg.participant = node.attrs.participant)
710
+ msg.messageTimestamp = +node.attrs.t
711
+ const fullMsg = proto.WebMessageInfo.fromObject(msg)
712
+ await upsertMessage(fullMsg, "append")
713
+ }
714
+ })])
715
+ } finally {
716
+ await sendMessageAck(node)
717
+ }
718
+ }
719
+
720
+ const handleMessage = async (node) => {
721
+ 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 }
722
+ const encNode = getBinaryNodeChild(node, "enc")
723
+ if (encNode && encNode.attrs.type === "msmsg") { logger.debug({ key: node.attrs.key }, "ignored msmsg"); await sendMessageAck(node, NACK_REASONS.MissingMessageSecret); return }
724
+ const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || "", signalRepository, logger)
725
+ const alt = msg.key.participantAlt || msg.key.remoteJidAlt
726
+ if (!!alt) {
727
+ const altServer = jidDecode(alt)?.server
728
+ const primaryJid = msg.key.participant || msg.key.remoteJid
729
+ if (altServer === "lid") { if (!(await signalRepository.lidMapping.getPNForLID(alt))) { await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]); await signalRepository.migrateSession(primaryJid, alt) } }
730
+ else { await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]); await signalRepository.migrateSession(alt, primaryJid) }
731
+ }
732
+ 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") }
733
+ try {
734
+ await messageMutex.mutex(async () => {
735
+ await decrypt()
736
+ if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== "peer") {
737
+ if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) return sendMessageAck(node, NACK_REASONS.ParsingError)
738
+ if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
739
+ const unavailableNode = getBinaryNodeChild(node, "unavailable")
740
+ const unavailableType = unavailableNode?.attrs?.type
741
+ 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) }
742
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp)
743
+ if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) { logger.debug({ msgId: msg.key.id, messageAge }, "skipping placeholder resend for old message"); return sendMessageAck(node) }
744
+ const cleanKey = { remoteJid: msg.key.remoteJid, fromMe: msg.key.fromMe, id: msg.key.id, participant: msg.key.participant }
745
+ const msgData = { key: msg.key, messageTimestamp: msg.messageTimestamp, pushName: msg.pushName, participant: msg.participant, verifiedBizName: msg.verifiedBizName }
746
+ 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") })
747
+ await sendMessageAck(node)
748
+ } else {
749
+ 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) }
750
+ if (isJidStatusBroadcast(msg.key.remoteJid)) {
751
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp)
752
+ 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) }
753
+ }
754
+ retryMutex.mutex(async () => {
755
+ try {
756
+ if (!ws.isOpen) { logger.debug({ node }, "Connection closed, skipping retry"); return }
757
+ const encNode = getBinaryNodeChild(node, "enc")
758
+ await sendRetryRequest(node, true)
759
+ if (retryRequestDelayMs) await delay(retryRequestDelayMs)
760
+ } catch (err) {
761
+ logger.error({ err }, "Retry mechanism failed")
762
+ try {
763
+ const encNode = getBinaryNodeChild(node, "enc")
764
+ await sendRetryRequest(node, !encNode)
765
+ } catch (retryErr) { logger.error({ retryErr }, "Final retry attempt failed") }
766
+ }
767
+ await sendMessageAck(node, NACK_REASONS.UnhandledError)
768
+ })
769
+ }
770
+ } else {
771
+ if (messageRetryManager && msg.key.id) messageRetryManager.cancelPendingPhoneRequest(msg.key.id)
772
+ const isNewsletter = isJidNewsletter(msg.key.remoteJid)
773
+ if (!isNewsletter) {
774
+ let type = undefined
775
+ let participant = msg.key.participant
776
+ if (category === "peer") type = "peer_msg"
777
+ else if (msg.key.fromMe) { type = "sender"; if (isLidUser(msg.key.remoteJid) || isLidUser(msg.key.remoteJidAlt)) participant = author }
778
+ else if (!sendActiveReceipts) type = "inactive"
779
+ await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type)
780
+ const isAnyHistoryMsg = getHistoryMsg(msg.message)
781
+ if (isAnyHistoryMsg) {
782
+ const jid = jidNormalizedUser(msg.key.remoteJid)
783
+ await sendReceipt(jid, undefined, [msg.key.id], "hist_sync")
784
+ }
785
+ } else {
786
+ await sendMessageAck(node)
787
+ logger.debug({ key: msg.key }, "processed newsletter message without receipts")
788
+ }
789
+ }
790
+ cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid)
791
+ await upsertMessage(msg, node.attrs.offline ? "append" : "notify")
792
+ })
793
+ } catch (error) {
794
+ const isClosed = error?.message?.includes('Connection Closed') || error?.output?.statusCode === 428
795
+ if (isClosed) {
796
+ logger.debug({ msg: error?.message }, "Connection closed while handling message")
797
+ } else {
798
+ logger.error({ error, stack: error?.stack, msg: error?.message || String(error), node: binaryNodeToString(node) }, "error in handling message")
799
+ }
800
+ }
801
+ }
802
+
803
+ const handleCall = async (node) => {
804
+ const { attrs } = node
805
+ const [infoChild] = getAllBinaryNodeChildren(node)
806
+ const status = getCallStatusFromNode(infoChild)
807
+ if (!infoChild) throw new Boom("Missing call info in call node")
808
+ const callId = infoChild.attrs["call-id"]
809
+ const from = infoChild.attrs.from || infoChild.attrs["call-creator"]
810
+ const call = { chatId: attrs.from, from, callerPn: infoChild.attrs["caller_pn"], id: callId, date: new Date(+attrs.t * 1000), offline: !!attrs.offline, status }
811
+ 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) }
812
+ const existingCall = await callOfferCache.get(call.id)
813
+ if (existingCall) { call.isVideo = existingCall.isVideo; call.isGroup = existingCall.isGroup; call.callerPn = call.callerPn || existingCall.callerPn }
814
+ if (status === "reject" || status === "accept" || status === "timeout" || status === "terminate") await callOfferCache.del(call.id)
815
+ ev.emit("call", [call])
816
+ await sendMessageAck(node)
817
+ }
818
+
819
+ const inFlight463Recoveries = new Set()
820
+
821
+ const handleBadAck = async ({ attrs }) => {
822
+ const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id }
823
+ if (attrs.error) {
824
+ const isReachoutTimelocked = attrs.error === String(NACK_REASONS.SenderReachoutTimelocked)
825
+ if (attrs.error === SERVER_ERROR_CODES.MessageAccountRestriction) {
826
+ logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted or missing tctoken for contact')
827
+ const ackFrom = attrs.from
828
+ if (ackFrom && !inFlight463Recoveries.has(ackFrom)) {
829
+ inFlight463Recoveries.add(ackFrom)
830
+ void (async () => {
831
+ try {
832
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
833
+ const tcStorageJid = await resolveTcTokenJid(ackFrom, getLIDForPN)
834
+ const issueJid = await resolveIssuanceJid(ackFrom, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID)
835
+ const result = await issuePrivacyTokens([issueJid], unixTimestampSeconds())
836
+ await storeTcTokensFromIqResult({ result, fallbackJid: tcStorageJid, keys: authState.keys, getLIDForPN, onNewJidStored: trackTcTokenJid })
837
+ logger.debug({ from: ackFrom }, 'completed 463 token recovery issuance')
838
+ } catch (err) { logger.debug({ from: ackFrom, err: err?.message }, 'failed 463 token recovery issuance') }
839
+ finally { inFlight463Recoveries.delete(ackFrom) }
840
+ })()
841
+ }
842
+ } else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
843
+ logger.warn({ msgId: attrs.id, from: attrs.from }, 'smax-invalid (479): stale device session or malformed addressing')
844
+ } else if (isReachoutTimelocked) {
845
+ await fetchAccountReachoutTimelock().catch(err => logger.warn({ err }, 'failed to fetch reachout timelock'))
846
+ logger.warn({ attrs }, 'received error in ack')
847
+ } else {
848
+ logger.warn({ attrs }, 'received error in ack')
849
+ }
850
+ ev.emit("messages.update", [{ key, update: { status: WAMessageStatus.ERROR, messageStubParameters: isReachoutTimelocked ? [attrs.error, ACCOUNT_RESTRICTED_TEXT] : [attrs.error] } }])
851
+ }
852
+ }
853
+
854
+ const processNodeWithBuffer = async (node, identifier, exec) => {
855
+ ev.buffer()
856
+ await execTask()
857
+ ev.flush()
858
+ function execTask() { return exec(node, false).catch((err) => onUnexpectedError(err, identifier)) }
859
+ }
860
+
861
+ const yieldToEventLoop = () => new Promise(resolve => setImmediate(resolve))
862
+
863
+ const makeOfflineNodeProcessor = () => {
864
+ const nodeProcessorMap = new Map([["message", handleMessage], ["call", handleCall], ["receipt", handleReceipt], ["notification", handleNotification]])
865
+ const nodes = []
866
+ let isProcessing = false
867
+ const BATCH_SIZE = 10
868
+ const enqueue = (type, node) => {
869
+ nodes.push({ type, node })
870
+ if (isProcessing) return
871
+ isProcessing = true
872
+ const promise = async () => {
873
+ let processedInBatch = 0
874
+ while (nodes.length && ws.isOpen) {
875
+ const { type, node } = nodes.shift()
876
+ const nodeProcessor = nodeProcessorMap.get(type)
877
+ if (!nodeProcessor) { onUnexpectedError(new Error(`unknown offline node type: ${type}`), "processing offline node"); continue }
878
+ await nodeProcessor(node)
879
+ processedInBatch++
880
+ if (processedInBatch >= BATCH_SIZE) { processedInBatch = 0; await yieldToEventLoop() }
881
+ }
882
+ isProcessing = false
883
+ }
884
+ promise().catch((error) => onUnexpectedError(error, "processing offline nodes"))
885
+ }
886
+ return { enqueue }
887
+ }
888
+
889
+ const offlineNodeProcessor = makeOfflineNodeProcessor()
890
+
891
+ const processNode = async (type, node, identifier, exec) => {
892
+ const isOffline = !!node.attrs.offline
893
+ if (isOffline) offlineNodeProcessor.enqueue(type, node)
894
+ else await processNodeWithBuffer(node, identifier, exec)
895
+ }
896
+
897
+ ws.on("CB:message", async (node) => { await processNode("message", node, "processing message", handleMessage) })
898
+ ws.on("CB:call", async (node) => { await processNode("call", node, "handling call", handleCall) })
899
+ ws.on("CB:receipt", async (node) => { await processNode("receipt", node, "handling receipt", handleReceipt) })
900
+ ws.on("CB:notification", async (node) => { await processNode("notification", node, "handling notification", handleNotification) })
901
+ ws.on("CB:ack,class:message", (node) => { handleBadAck(node).catch((error) => onUnexpectedError(error, "handling bad ack")) })
902
+
903
+ ev.on("call", async ([call]) => {
904
+ if (!call) return
905
+ if (call.status === "timeout" || (call.status === "offer" && call.isGroup)) {
906
+ const msg = { key: { remoteJid: call.chatId, id: call.id, fromMe: false }, messageTimestamp: unixTimestampSeconds(call.date) }
907
+ if (call.status === "timeout") {
908
+ if (call.isGroup) msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_GROUP_VIDEO : WAMessageStubType.CALL_MISSED_GROUP_VOICE
909
+ else msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE
910
+ } else msg.message = { call: { callKey: Buffer.from(call.id) } }
911
+ const protoMsg = proto.WebMessageInfo.fromObject(msg)
912
+ await upsertMessage(protoMsg, call.offline ? "append" : "notify")
913
+ }
914
+ })
915
+
916
+ let lastTcTokenPruneTs = 0
917
+
918
+ async function pruneExpiredTcTokens() {
919
+ try {
920
+ await tcTokenIndexLoaded
921
+ const persisted = await readTcTokenIndex(authState.keys)
922
+ const allJids = new Set(tcTokenKnownJids)
923
+ for (const jid of persisted) allJids.add(jid)
924
+ if (!allJids.size) return
925
+ const jids = [...allJids]
926
+ const allTokens = await authState.keys.get('tctoken', jids)
927
+ const writes = {}, survivors = new Set()
928
+ let mutated = 0
929
+ for (const jid of jids) {
930
+ const entry = allTokens[jid]
931
+ if (!entry) { mutated++; continue }
932
+ const hasPeerToken = !!entry.token?.length
933
+ const peerTokenExpired = hasPeerToken && isTcTokenExpired(entry.timestamp)
934
+ const hasSenderTs = entry.senderTimestamp !== undefined
935
+ const senderTsExpired = hasSenderTs && isTcTokenExpired(entry.senderTimestamp)
936
+ const keepPeerToken = hasPeerToken && !peerTokenExpired
937
+ const keepSenderTs = hasSenderTs && !senderTsExpired
938
+ if (!keepPeerToken && !keepSenderTs) { writes[jid] = null; mutated++ }
939
+ else if (peerTokenExpired && keepSenderTs) { writes[jid] = { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp }; survivors.add(jid); mutated++ }
940
+ else survivors.add(jid)
941
+ }
942
+ if (mutated === 0) return
943
+ await authState.keys.set({ tctoken: { ...writes, [TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...survivors])) } } })
944
+ tcTokenKnownJids.clear()
945
+ for (const jid of survivors) tcTokenKnownJids.add(jid)
946
+ logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens')
947
+ } catch (err) { logger.warn({ err: err?.message }, 'failed to prune expired tctokens') }
948
+ }
949
+
950
+ ev.on("connection.update", ({ isOnline, connection }) => {
951
+ if (typeof isOnline !== "undefined") { sendActiveReceipts = isOnline; logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`) }
952
+ if (connection === 'close' && tcTokenIndexTimer) { clearTimeout(tcTokenIndexTimer); tcTokenIndexTimer = undefined; try { void Promise.resolve(flushTcTokenIndex()).catch(() => { }) } catch { } }
953
+ if (isOnline) { const now = Date.now(); const DAY_MS = 24 * 60 * 60 * 1000; if (now - lastTcTokenPruneTs >= DAY_MS) { lastTcTokenPruneTs = now; void pruneExpiredTcTokens() } }
954
+ })
955
+
956
+ registerSocketEndHandler(() => {
957
+ if (!config.msgRetryCounterCache && msgRetryCache.close) msgRetryCache.close()
958
+ if (!config.callOfferCache && callOfferCache.close) callOfferCache.close()
959
+ identityAssertDebounce.close()
960
+ sendActiveReceipts = false
961
+ })
962
+
963
+ return { ...sock, sendMessageAck, sendRetryRequest, rejectCall, offerCall, fetchMessageHistory, requestPlaceholderResend, messageRetryManager }
744
964
  }