@badzz88/baileys 8.5.6 → 8.5.7

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