@queenanya/baileys 9.2.1 → 9.4.1

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 (327) hide show
  1. package/README.md +349 -1171
  2. package/WAProto/fix-imports.js +74 -18
  3. package/WAProto/index.js +201 -160
  4. package/engine-requirements.js +7 -7
  5. package/lib/Defaults/index.d.ts +19 -0
  6. package/lib/Defaults/index.d.ts.map +1 -1
  7. package/lib/Defaults/index.js +32 -6
  8. package/lib/Defaults/index.js.map +1 -1
  9. package/lib/Signal/libsignal.d.ts.map +1 -1
  10. package/lib/Signal/libsignal.js +61 -2
  11. package/lib/Signal/libsignal.js.map +1 -1
  12. package/lib/Signal/lid-mapping.d.ts +5 -9
  13. package/lib/Signal/lid-mapping.d.ts.map +1 -1
  14. package/lib/Signal/lid-mapping.js +170 -70
  15. package/lib/Signal/lid-mapping.js.map +1 -1
  16. package/lib/Socket/Client/websocket.d.ts +1 -1
  17. package/lib/Socket/Client/websocket.d.ts.map +1 -1
  18. package/lib/Socket/Client/websocket.js +5 -1
  19. package/lib/Socket/Client/websocket.js.map +1 -1
  20. package/lib/Socket/business.d.ts +125 -5
  21. package/lib/Socket/business.d.ts.map +1 -1
  22. package/lib/Socket/business.js +11 -8
  23. package/lib/Socket/business.js.map +1 -1
  24. package/lib/Socket/chats.d.ts +22 -3
  25. package/lib/Socket/chats.d.ts.map +1 -1
  26. package/lib/Socket/chats.js +277 -58
  27. package/lib/Socket/chats.js.map +1 -1
  28. package/lib/Socket/communities.d.ts +125 -5
  29. package/lib/Socket/communities.d.ts.map +1 -1
  30. package/lib/Socket/groups.d.ts +19 -3
  31. package/lib/Socket/groups.d.ts.map +1 -1
  32. package/lib/Socket/groups.js +7 -1
  33. package/lib/Socket/groups.js.map +1 -1
  34. package/lib/Socket/index.d.ts +125 -5
  35. package/lib/Socket/index.d.ts.map +1 -1
  36. package/lib/Socket/index.js +0 -6
  37. package/lib/Socket/index.js.map +1 -1
  38. package/lib/Socket/messages-recv.d.ts +126 -6
  39. package/lib/Socket/messages-recv.d.ts.map +1 -1
  40. package/lib/Socket/messages-recv.js +771 -177
  41. package/lib/Socket/messages-recv.js.map +1 -1
  42. package/lib/Socket/messages-send.d.ts +129 -7
  43. package/lib/Socket/messages-send.d.ts.map +1 -1
  44. package/lib/Socket/messages-send.js +430 -119
  45. package/lib/Socket/messages-send.js.map +1 -1
  46. package/lib/Socket/newsletter.d.ts +20 -5
  47. package/lib/Socket/newsletter.d.ts.map +1 -1
  48. package/lib/Socket/newsletter.js +2 -47
  49. package/lib/Socket/newsletter.js.map +1 -1
  50. package/lib/Socket/socket.d.ts +3 -1
  51. package/lib/Socket/socket.d.ts.map +1 -1
  52. package/lib/Socket/socket.js +151 -29
  53. package/lib/Socket/socket.js.map +1 -1
  54. package/lib/Types/Auth.d.ts +2 -0
  55. package/lib/Types/Auth.d.ts.map +1 -1
  56. package/lib/Types/Call.d.ts +10 -1
  57. package/lib/Types/Call.d.ts.map +1 -1
  58. package/lib/Types/Contact.d.ts +2 -0
  59. package/lib/Types/Contact.d.ts.map +1 -1
  60. package/lib/Types/Events.d.ts +60 -6
  61. package/lib/Types/Events.d.ts.map +1 -1
  62. package/lib/Types/GroupMetadata.d.ts +4 -0
  63. package/lib/Types/GroupMetadata.d.ts.map +1 -1
  64. package/lib/Types/Message.d.ts +530 -16
  65. package/lib/Types/Message.d.ts.map +1 -1
  66. package/lib/Types/Message.js.map +1 -1
  67. package/lib/Types/Newsletter.d.ts +32 -45
  68. package/lib/Types/Newsletter.d.ts.map +1 -1
  69. package/lib/Types/Newsletter.js +25 -23
  70. package/lib/Types/Newsletter.js.map +1 -1
  71. package/lib/Types/State.d.ts +54 -0
  72. package/lib/Types/State.d.ts.map +1 -1
  73. package/lib/Types/State.js +42 -0
  74. package/lib/Types/State.js.map +1 -1
  75. package/lib/Types/index.d.ts +9 -0
  76. package/lib/Types/index.d.ts.map +1 -1
  77. package/lib/Types/index.js.map +1 -1
  78. package/lib/Utils/auth-utils.d.ts.map +1 -1
  79. package/lib/Utils/auth-utils.js +53 -20
  80. package/lib/Utils/auth-utils.js.map +1 -1
  81. package/lib/Utils/browser-utils.d.ts +13 -0
  82. package/lib/Utils/browser-utils.d.ts.map +1 -1
  83. package/lib/Utils/browser-utils.js +90 -10
  84. package/lib/Utils/browser-utils.js.map +1 -1
  85. package/lib/Utils/chat-utils.d.ts +30 -0
  86. package/lib/Utils/chat-utils.d.ts.map +1 -1
  87. package/lib/Utils/chat-utils.js +134 -59
  88. package/lib/Utils/chat-utils.js.map +1 -1
  89. package/lib/Utils/companion-reg-client-utils.d.ts +17 -0
  90. package/lib/Utils/companion-reg-client-utils.d.ts.map +1 -0
  91. package/lib/Utils/companion-reg-client-utils.js +34 -0
  92. package/lib/Utils/companion-reg-client-utils.js.map +1 -0
  93. package/lib/Utils/crypto.d.ts +4 -8
  94. package/lib/Utils/crypto.d.ts.map +1 -1
  95. package/lib/Utils/crypto.js +2 -26
  96. package/lib/Utils/crypto.js.map +1 -1
  97. package/lib/Utils/decode-wa-message.d.ts +12 -0
  98. package/lib/Utils/decode-wa-message.d.ts.map +1 -1
  99. package/lib/Utils/decode-wa-message.js +16 -0
  100. package/lib/Utils/decode-wa-message.js.map +1 -1
  101. package/lib/Utils/event-buffer.d.ts.map +1 -1
  102. package/lib/Utils/event-buffer.js +43 -8
  103. package/lib/Utils/event-buffer.js.map +1 -1
  104. package/lib/Utils/generics.d.ts +3 -1
  105. package/lib/Utils/generics.d.ts.map +1 -1
  106. package/lib/Utils/generics.js +17 -4
  107. package/lib/Utils/generics.js.map +1 -1
  108. package/lib/Utils/history.d.ts +8 -3
  109. package/lib/Utils/history.d.ts.map +1 -1
  110. package/lib/Utils/history.js +60 -16
  111. package/lib/Utils/history.js.map +1 -1
  112. package/lib/Utils/identity-change-handler.d.ts +44 -0
  113. package/lib/Utils/identity-change-handler.d.ts.map +1 -0
  114. package/lib/Utils/identity-change-handler.js +50 -0
  115. package/lib/Utils/identity-change-handler.js.map +1 -0
  116. package/lib/Utils/index.d.ts +6 -0
  117. package/lib/Utils/index.d.ts.map +1 -1
  118. package/lib/Utils/index.js +6 -0
  119. package/lib/Utils/index.js.map +1 -1
  120. package/lib/Utils/interactive-message.d.ts +201 -0
  121. package/lib/Utils/interactive-message.d.ts.map +1 -0
  122. package/lib/Utils/interactive-message.js +256 -0
  123. package/lib/Utils/interactive-message.js.map +1 -0
  124. package/lib/Utils/lt-hash.d.ts +7 -12
  125. package/lib/Utils/lt-hash.d.ts.map +1 -1
  126. package/lib/Utils/lt-hash.js +2 -42
  127. package/lib/Utils/lt-hash.js.map +1 -1
  128. package/lib/Utils/make-mutex.d.ts +1 -0
  129. package/lib/Utils/make-mutex.d.ts.map +1 -1
  130. package/lib/Utils/make-mutex.js +20 -27
  131. package/lib/Utils/make-mutex.js.map +1 -1
  132. package/lib/Utils/message-composer.d.ts +5 -0
  133. package/lib/Utils/message-composer.d.ts.map +1 -0
  134. package/lib/Utils/message-composer.js +5 -0
  135. package/lib/Utils/message-composer.js.map +1 -0
  136. package/lib/Utils/message-retry-manager.d.ts +30 -2
  137. package/lib/Utils/message-retry-manager.d.ts.map +1 -1
  138. package/lib/Utils/message-retry-manager.js +58 -5
  139. package/lib/Utils/message-retry-manager.js.map +1 -1
  140. package/lib/Utils/messages-media.d.ts +35 -5
  141. package/lib/Utils/messages-media.d.ts.map +1 -1
  142. package/lib/Utils/messages-media.js +171 -51
  143. package/lib/Utils/messages-media.js.map +1 -1
  144. package/lib/Utils/messages.d.ts +2 -0
  145. package/lib/Utils/messages.d.ts.map +1 -1
  146. package/lib/Utils/messages.js +475 -35
  147. package/lib/Utils/messages.js.map +1 -1
  148. package/lib/Utils/noise-handler.d.ts +4 -4
  149. package/lib/Utils/noise-handler.d.ts.map +1 -1
  150. package/lib/Utils/noise-handler.js +139 -85
  151. package/lib/Utils/noise-handler.js.map +1 -1
  152. package/lib/Utils/offline-node-processor.d.ts +17 -0
  153. package/lib/Utils/offline-node-processor.d.ts.map +1 -0
  154. package/lib/Utils/offline-node-processor.js +40 -0
  155. package/lib/Utils/offline-node-processor.js.map +1 -0
  156. package/lib/Utils/process-message.d.ts.map +1 -1
  157. package/lib/Utils/process-message.js +115 -16
  158. package/lib/Utils/process-message.js.map +1 -1
  159. package/lib/Utils/reporting-utils.d.ts +11 -0
  160. package/lib/Utils/reporting-utils.d.ts.map +1 -0
  161. package/lib/Utils/reporting-utils.js +258 -0
  162. package/lib/Utils/reporting-utils.js.map +1 -0
  163. package/lib/Utils/stanza-ack.d.ts +11 -0
  164. package/lib/Utils/stanza-ack.d.ts.map +1 -0
  165. package/lib/Utils/stanza-ack.js +38 -0
  166. package/lib/Utils/stanza-ack.js.map +1 -0
  167. package/lib/Utils/sync-action-utils.d.ts +19 -0
  168. package/lib/Utils/sync-action-utils.d.ts.map +1 -0
  169. package/lib/Utils/sync-action-utils.js +49 -0
  170. package/lib/Utils/sync-action-utils.js.map +1 -0
  171. package/lib/Utils/tc-token-utils.d.ts +37 -0
  172. package/lib/Utils/tc-token-utils.d.ts.map +1 -0
  173. package/lib/Utils/tc-token-utils.js +163 -0
  174. package/lib/Utils/tc-token-utils.js.map +1 -0
  175. package/lib/Utils/use-mongo-file-auth-state.d.ts +16 -0
  176. package/lib/Utils/use-mongo-file-auth-state.d.ts.map +1 -0
  177. package/lib/Utils/use-mongo-file-auth-state.js +60 -0
  178. package/lib/Utils/use-mongo-file-auth-state.js.map +1 -0
  179. package/lib/Utils/use-multi-file-auth-state.js +1 -1
  180. package/lib/Utils/use-multi-file-auth-state.js.map +1 -1
  181. package/lib/Utils/use-single-file-auth-state.d.ts.map +1 -1
  182. package/lib/Utils/use-single-file-auth-state.js.map +1 -1
  183. package/lib/Utils/validate-connection.d.ts.map +1 -1
  184. package/lib/Utils/validate-connection.js +11 -1
  185. package/lib/Utils/validate-connection.js.map +1 -1
  186. package/lib/WABinary/decode.d.ts.map +1 -1
  187. package/lib/WABinary/decode.js +24 -0
  188. package/lib/WABinary/decode.js.map +1 -1
  189. package/lib/WABinary/encode.js +5 -1
  190. package/lib/WABinary/encode.js.map +1 -1
  191. package/lib/WABinary/generic-utils.d.ts +10 -1
  192. package/lib/WABinary/generic-utils.d.ts.map +1 -1
  193. package/lib/WABinary/generic-utils.js +42 -8
  194. package/lib/WABinary/generic-utils.js.map +1 -1
  195. package/lib/WABinary/jid-utils.js.map +1 -1
  196. package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts.map +1 -1
  197. package/lib/WAUSync/Protocols/USyncContactProtocol.js +26 -3
  198. package/lib/WAUSync/Protocols/USyncContactProtocol.js.map +1 -1
  199. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts +10 -0
  200. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts.map +1 -0
  201. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
  202. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js.map +1 -0
  203. package/lib/WAUSync/Protocols/index.d.ts +1 -0
  204. package/lib/WAUSync/Protocols/index.d.ts.map +1 -1
  205. package/lib/WAUSync/Protocols/index.js +1 -0
  206. package/lib/WAUSync/Protocols/index.js.map +1 -1
  207. package/lib/WAUSync/USyncQuery.d.ts +1 -0
  208. package/lib/WAUSync/USyncQuery.d.ts.map +1 -1
  209. package/lib/WAUSync/USyncQuery.js +6 -2
  210. package/lib/WAUSync/USyncQuery.js.map +1 -1
  211. package/lib/WAUSync/USyncUser.d.ts +4 -0
  212. package/lib/WAUSync/USyncUser.d.ts.map +1 -1
  213. package/lib/WAUSync/USyncUser.js +8 -0
  214. package/lib/WAUSync/USyncUser.js.map +1 -1
  215. package/lib/addons/anti-delete.d.ts +72 -0
  216. package/lib/addons/anti-delete.d.ts.map +1 -0
  217. package/lib/addons/anti-delete.js +165 -0
  218. package/lib/addons/anti-delete.js.map +1 -0
  219. package/lib/addons/auto-reply.d.ts +67 -0
  220. package/lib/addons/auto-reply.d.ts.map +1 -0
  221. package/lib/addons/auto-reply.js +145 -0
  222. package/lib/addons/auto-reply.js.map +1 -0
  223. package/lib/addons/browser-presets.d.ts +16 -0
  224. package/lib/addons/browser-presets.d.ts.map +1 -0
  225. package/lib/addons/browser-presets.js +24 -0
  226. package/lib/addons/browser-presets.js.map +1 -0
  227. package/lib/addons/button-sender.d.ts +260 -0
  228. package/lib/addons/button-sender.d.ts.map +1 -0
  229. package/lib/addons/button-sender.js +771 -0
  230. package/lib/addons/button-sender.js.map +1 -0
  231. package/lib/addons/call-handler.d.ts +79 -0
  232. package/lib/addons/call-handler.d.ts.map +1 -0
  233. package/lib/addons/call-handler.js +342 -0
  234. package/lib/addons/call-handler.js.map +1 -0
  235. package/lib/addons/from-chats.d.ts +30 -0
  236. package/lib/addons/from-chats.d.ts.map +1 -0
  237. package/lib/addons/from-chats.js +38 -0
  238. package/lib/addons/from-chats.js.map +1 -0
  239. package/lib/addons/from-messages-recv.d.ts +59 -0
  240. package/lib/addons/from-messages-recv.d.ts.map +1 -0
  241. package/lib/addons/from-messages-recv.js +326 -0
  242. package/lib/addons/from-messages-recv.js.map +1 -0
  243. package/lib/addons/from-messages-send.d.ts +50 -0
  244. package/lib/addons/from-messages-send.d.ts.map +1 -0
  245. package/lib/addons/from-messages-send.js +148 -0
  246. package/lib/addons/from-messages-send.js.map +1 -0
  247. package/lib/addons/from-messages.d.ts +52 -0
  248. package/lib/addons/from-messages.d.ts.map +1 -0
  249. package/lib/addons/from-messages.js +304 -0
  250. package/lib/addons/from-messages.js.map +1 -0
  251. package/lib/addons/index.d.ts +67 -0
  252. package/lib/addons/index.d.ts.map +1 -0
  253. package/lib/addons/index.js +86 -0
  254. package/lib/addons/index.js.map +1 -0
  255. package/lib/addons/interactive-message.d.ts +201 -0
  256. package/lib/addons/interactive-message.d.ts.map +1 -0
  257. package/lib/addons/interactive-message.js +256 -0
  258. package/lib/addons/interactive-message.js.map +1 -0
  259. package/lib/addons/jid-plot.d.ts +49 -0
  260. package/lib/addons/jid-plot.d.ts.map +1 -0
  261. package/lib/addons/jid-plot.js +84 -0
  262. package/lib/addons/jid-plot.js.map +1 -0
  263. package/lib/addons/jid-plotting.d.ts +54 -0
  264. package/lib/addons/jid-plotting.d.ts.map +1 -0
  265. package/lib/addons/jid-plotting.js +150 -0
  266. package/lib/addons/jid-plotting.js.map +1 -0
  267. package/lib/addons/lid-support.d.ts +41 -0
  268. package/lib/addons/lid-support.d.ts.map +1 -0
  269. package/lib/addons/lid-support.js +42 -0
  270. package/lib/addons/lid-support.js.map +1 -0
  271. package/lib/addons/message-composer.d.ts +142 -0
  272. package/lib/addons/message-composer.d.ts.map +1 -0
  273. package/lib/addons/message-composer.js +377 -0
  274. package/lib/addons/message-composer.js.map +1 -0
  275. package/lib/addons/message-scheduler.d.ts +77 -0
  276. package/lib/addons/message-scheduler.d.ts.map +1 -0
  277. package/lib/addons/message-scheduler.js +108 -0
  278. package/lib/addons/message-scheduler.js.map +1 -0
  279. package/lib/addons/message-search.d.ts +51 -0
  280. package/lib/addons/message-search.d.ts.map +1 -0
  281. package/lib/addons/message-search.js +171 -0
  282. package/lib/addons/message-search.js.map +1 -0
  283. package/lib/addons/message-utils.d.ts +88 -0
  284. package/lib/addons/message-utils.d.ts.map +1 -0
  285. package/lib/addons/message-utils.js +292 -0
  286. package/lib/addons/message-utils.js.map +1 -0
  287. package/lib/addons/outgoing-calls.d.ts +64 -0
  288. package/lib/addons/outgoing-calls.d.ts.map +1 -0
  289. package/lib/addons/outgoing-calls.js +139 -0
  290. package/lib/addons/outgoing-calls.js.map +1 -0
  291. package/lib/addons/pairing-fix.d.ts +31 -0
  292. package/lib/addons/pairing-fix.d.ts.map +1 -0
  293. package/lib/addons/pairing-fix.js +74 -0
  294. package/lib/addons/pairing-fix.js.map +1 -0
  295. package/lib/addons/past-participants.d.ts +42 -0
  296. package/lib/addons/past-participants.d.ts.map +1 -0
  297. package/lib/addons/past-participants.js +41 -0
  298. package/lib/addons/past-participants.js.map +1 -0
  299. package/lib/addons/rich-response.d.ts +111 -0
  300. package/lib/addons/rich-response.d.ts.map +1 -0
  301. package/lib/addons/rich-response.js +152 -0
  302. package/lib/addons/rich-response.js.map +1 -0
  303. package/lib/addons/scheduling.d.ts +41 -0
  304. package/lib/addons/scheduling.d.ts.map +1 -0
  305. package/lib/addons/scheduling.js +110 -0
  306. package/lib/addons/scheduling.js.map +1 -0
  307. package/lib/addons/status-posting.d.ts +177 -0
  308. package/lib/addons/status-posting.d.ts.map +1 -0
  309. package/lib/addons/status-posting.js +240 -0
  310. package/lib/addons/status-posting.js.map +1 -0
  311. package/lib/addons/stickerpack.d.ts +37 -0
  312. package/lib/addons/stickerpack.d.ts.map +1 -0
  313. package/lib/addons/stickerpack.js +39 -0
  314. package/lib/addons/stickerpack.js.map +1 -0
  315. package/lib/addons/templates.d.ts +72 -0
  316. package/lib/addons/templates.d.ts.map +1 -0
  317. package/lib/addons/templates.js +145 -0
  318. package/lib/addons/templates.js.map +1 -0
  319. package/lib/addons/vcard.d.ts +59 -0
  320. package/lib/addons/vcard.d.ts.map +1 -0
  321. package/lib/addons/vcard.js +88 -0
  322. package/lib/addons/vcard.js.map +1 -0
  323. package/lib/index.d.ts +1 -0
  324. package/lib/index.d.ts.map +1 -1
  325. package/lib/index.js +1 -0
  326. package/lib/index.js.map +1 -1
  327. package/package.json +6 -3
@@ -3,17 +3,19 @@ import { Boom } from '@hapi/boom';
3
3
  import { randomBytes } from 'crypto';
4
4
  import Long from 'long';
5
5
  import { proto } from '../../WAProto/index.js';
6
- import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults/index.js';
7
- import { WAMessageStatus, WAMessageStubType } from '../Types/index.js';
8
- import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
6
+ import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults/index.js';
7
+ import { ReachoutTimelockEnforcementType, WAMessageStatus, WAMessageStubType } from '../Types/index.js';
8
+ import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
9
9
  import { makeMutex } from '../Utils/make-mutex.js';
10
- import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
10
+ import { buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveIssuanceJid, resolveTcTokenJid, storeTcTokensFromIqResult, TC_TOKEN_INDEX_KEY } from '../Utils/tc-token-utils.js';
11
+ import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
11
12
  import { extractGroupMetadata } from './groups.js';
12
13
  import { makeMessagesSocket } from './messages-send.js';
13
14
  export const makeMessagesRecvSocket = (config) => {
14
15
  const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
15
16
  const sock = makeMessagesSocket(config);
16
- const { ev, authState, ws, processingMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager } = sock;
17
+ const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, generateMessageTag, getUSyncDevices, createParticipantNodes, messageRetryManager, issuePrivacyTokens } = sock;
18
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
17
19
  /** this mutex ensures that each retryRequest will wait for the previous one to finish */
18
20
  const retryMutex = makeMutex();
19
21
  const msgRetryCache = config.msgRetryCounterCache ||
@@ -50,19 +52,21 @@ export const makeMessagesRecvSocket = (config) => {
50
52
  };
51
53
  return sendPeerDataOperationMessage(pdoMessage);
52
54
  };
53
- const requestPlaceholderResend = async (messageKey) => {
55
+ const requestPlaceholderResend = async (messageKey, msgData) => {
54
56
  if (!authState.creds.me?.id) {
55
57
  throw new Boom('Not authenticated');
56
58
  }
57
- if (placeholderResendCache.get(messageKey?.id)) {
59
+ if (await placeholderResendCache.get(messageKey?.id)) {
58
60
  logger.debug({ messageKey }, 'already requested resend');
59
61
  return;
60
62
  }
61
63
  else {
62
- await placeholderResendCache.set(messageKey?.id, true);
64
+ // Store original message data so PDO response handler can preserve
65
+ // metadata (LID details, timestamps, etc.) that the phone may omit
66
+ await placeholderResendCache.set(messageKey?.id, msgData || true);
63
67
  }
64
- await delay(5000);
65
- if (!placeholderResendCache.get(messageKey?.id)) {
68
+ await delay(2000);
69
+ if (!(await placeholderResendCache.get(messageKey?.id))) {
66
70
  logger.debug({ messageKey }, 'message received while resend requested');
67
71
  return 'RESOLVED';
68
72
  }
@@ -75,32 +79,152 @@ export const makeMessagesRecvSocket = (config) => {
75
79
  peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
76
80
  };
77
81
  setTimeout(async () => {
78
- if (placeholderResendCache.get(messageKey?.id)) {
79
- logger.debug({ messageKey }, 'PDO message without response after 15 seconds. Phone possibly offline');
82
+ if (await placeholderResendCache.get(messageKey?.id)) {
83
+ logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline');
80
84
  await placeholderResendCache.del(messageKey?.id);
81
85
  }
82
- }, 15000);
86
+ }, 8000);
83
87
  return sendPeerDataOperationMessage(pdoMessage);
84
88
  };
85
- // Handles mex newsletter notifications
86
- const handleMexNewsletterNotification = async (node) => {
89
+ const ENFORCEMENT_TYPE_VALUES = new Set(Object.values(ReachoutTimelockEnforcementType));
90
+ function isValidEnforcementType(value) {
91
+ return typeof value === 'string' && ENFORCEMENT_TYPE_VALUES.has(value);
92
+ }
93
+ // ── Top-level mex dispatcher (PR: feat-mex-notification-dispatch) ─────────
94
+ const handleMexNotification = async (node) => {
95
+ const updateNode = getBinaryNodeChild(node, 'update');
96
+ if (updateNode) {
97
+ const opName = updateNode.attrs?.op_name;
98
+ if (!opName) {
99
+ logger.warn({ node: binaryNodeToString(node) }, 'mex notification missing op_name');
100
+ return;
101
+ }
102
+ let mexResponse;
103
+ try {
104
+ mexResponse = JSON.parse(updateNode.content.toString());
105
+ }
106
+ catch (error) {
107
+ logger.error({ err: error, opName }, 'failed to parse mex notification JSON');
108
+ return;
109
+ }
110
+ if (mexResponse.errors?.length) {
111
+ logger.warn({ errors: mexResponse.errors, opName }, 'mex notification has GQL errors');
112
+ return;
113
+ }
114
+ const data = mexResponse.data;
115
+ if (!data) {
116
+ logger.warn({ opName }, 'mex notification has null data');
117
+ return;
118
+ }
119
+ logger.debug({ opName }, 'processing mex notification');
120
+ switch (opName) {
121
+ case 'NotificationUserReachoutTimelockUpdate':
122
+ handleReachoutTimelockNotification(data);
123
+ break;
124
+ case 'MessageCappingInfoNotification':
125
+ handleMessageCappingNotification(data);
126
+ break;
127
+ case 'NotificationLinkedProfilesUpdates': {
128
+ // PR: fix-mex-linked-profiles
129
+ const linkedProfiles = data.xwa2_notify_linked_profiles;
130
+ if (!linkedProfiles)
131
+ break;
132
+ const lid = linkedProfiles.jid;
133
+ for (const profile of linkedProfiles.added_profiles ?? []) {
134
+ const pn = typeof profile === 'string' ? profile : (profile?.pn ?? profile?.jid ?? null);
135
+ if (lid && pn)
136
+ ev.emit('lid-mapping.update', { lid, pn });
137
+ }
138
+ break;
139
+ }
140
+ // newsletter ops still use the legacy <mex> child structure
141
+ case 'NotificationNewsletterUpdate':
142
+ case 'NotificationNewsletterAdminPromote':
143
+ case 'NotificationNewsletterAdminDemote':
144
+ case 'NotificationNewsletterUserSettingChange':
145
+ case 'NotificationNewsletterJoin':
146
+ case 'NotificationNewsletterLeave':
147
+ case 'NotificationNewsletterStateChange':
148
+ case 'NotificationNewsletterAdminMetadataUpdate':
149
+ case 'NotificationNewsletterOwnerUpdate':
150
+ case 'NotificationNewsletterAdminInviteRevoke':
151
+ case 'NotificationNewsletterWamoSubStatusChange':
152
+ case 'NotificationNewsletterBlockUser':
153
+ case 'NotificationNewsletterPaidPartnership':
154
+ case 'NotificationNewsletterMilestone':
155
+ case 'NewsletterResponseStateUpdate':
156
+ await handleLegacyMexNewsletterNotification(node);
157
+ break;
158
+ default:
159
+ logger.debug({ opName }, 'unhandled mex notification');
160
+ break;
161
+ }
162
+ return;
163
+ }
164
+ await handleLegacyMexNewsletterNotification(node);
165
+ };
166
+ const handleReachoutTimelockNotification = (data) => {
167
+ const payload = data.xwa2_notify_account_reachout_timelock;
168
+ if (!payload) {
169
+ logger.warn('reachout timelock notification missing payload');
170
+ return;
171
+ }
172
+ if (!payload.is_active) {
173
+ logger.info('reachout timelock restriction lifted');
174
+ ev.emit('connection.update', {
175
+ reachoutTimeLock: {
176
+ isActive: false,
177
+ enforcementType: ReachoutTimelockEnforcementType.DEFAULT
178
+ }
179
+ });
180
+ return;
181
+ }
182
+ // WA Web defaults to now+60s when the server omits the expiry
183
+ const timeEnforcementEnds = payload.time_enforcement_ends
184
+ ? new Date(parseInt(payload.time_enforcement_ends, 10) * 1000)
185
+ : new Date(Date.now() + 60000);
186
+ const enforcementType = isValidEnforcementType(payload.enforcement_type)
187
+ ? payload.enforcement_type
188
+ : ReachoutTimelockEnforcementType.DEFAULT;
189
+ logger.info({ enforcementType, timeEnforcementEnds }, 'reachout timelock restriction set');
190
+ ev.emit('connection.update', {
191
+ reachoutTimeLock: {
192
+ isActive: true,
193
+ timeEnforcementEnds,
194
+ enforcementType
195
+ }
196
+ });
197
+ };
198
+ const handleMessageCappingNotification = (data) => {
199
+ const payload = data.xwa2_notify_new_chat_messages_capping_info_update;
200
+ if (!payload) {
201
+ logger.warn('message capping notification missing payload');
202
+ return;
203
+ }
204
+ logger.info({ payload }, 'received message capping update');
205
+ ev.emit('message-capping.update', payload);
206
+ };
207
+ // ── Legacy mex newsletter notification handler ────────────────────────────
208
+ const handleLegacyMexNewsletterNotification = async (node) => {
87
209
  const mexNode = getBinaryNodeChild(node, 'mex');
88
210
  if (!mexNode?.content) {
89
- logger.warn({ node }, 'Invalid mex newsletter notification');
211
+ logger.warn({ node: binaryNodeToString(node) }, 'invalid mex newsletter notification');
90
212
  return;
91
213
  }
92
- let data;
214
+ let parsed;
93
215
  try {
94
- data = JSON.parse(mexNode.content.toString());
216
+ // PR fix-mex-linked-profiles: handle binary-encoded content correctly
217
+ const payloadContent = mexNode.content;
218
+ const contentBuf = typeof payloadContent === 'string' ? Buffer.from(payloadContent, 'binary') : Buffer.from(payloadContent);
219
+ parsed = JSON.parse(contentBuf.toString());
95
220
  }
96
221
  catch (error) {
97
- logger.error({ err: error, node }, 'Failed to parse mex newsletter notification');
222
+ logger.error({ err: error, node: binaryNodeToString(node) }, 'failed to parse mex newsletter notification');
98
223
  return;
99
224
  }
100
- const operation = data?.operation;
101
- const updates = data?.updates;
225
+ const { operation, updates } = parsed;
102
226
  if (!updates || !operation) {
103
- logger.warn({ data }, 'Invalid mex newsletter notification content');
227
+ logger.warn({ parsed }, 'invalid mex newsletter notification content');
104
228
  return;
105
229
  }
106
230
  logger.info({ operation, updates }, 'got mex newsletter notification');
@@ -129,7 +253,7 @@ export const makeMessagesRecvSocket = (config) => {
129
253
  }
130
254
  break;
131
255
  default:
132
- logger.info({ operation, data }, 'Unhandled mex newsletter notification');
256
+ logger.info({ operation, parsed }, 'unhandled mex newsletter notification');
133
257
  break;
134
258
  }
135
259
  };
@@ -243,26 +367,275 @@ export const makeMessagesRecvSocket = (config) => {
243
367
  logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, 'sent ack');
244
368
  await sendNode(stanza);
245
369
  };
370
+ // ── Call handlers ─────────────────────────────────────────────────
246
371
  const rejectCall = async (callId, callFrom) => {
247
- const stanza = {
372
+ await query({
248
373
  tag: 'call',
249
- attrs: {
250
- from: authState.creds.me.id,
251
- to: callFrom
252
- },
374
+ attrs: { from: authState.creds.me.id, to: callFrom },
375
+ content: [
376
+ { tag: 'reject', attrs: { 'call-id': callId, 'call-creator': callFrom, count: '0' }, content: undefined }
377
+ ]
378
+ });
379
+ };
380
+ const offerCall = async (toJid, isVideo = false) => {
381
+ const callId = randomBytes(16).toString('hex').toUpperCase().substring(0, 64);
382
+ const offerContent = [];
383
+ if (isVideo) {
384
+ offerContent.push({
385
+ tag: 'video',
386
+ attrs: {
387
+ enc: 'vp8',
388
+ dec: 'vp8',
389
+ orientation: '0',
390
+ screen_width: '1920',
391
+ screen_height: '1080',
392
+ device_orientation: '0'
393
+ },
394
+ content: undefined
395
+ });
396
+ }
397
+ offerContent.push({ tag: 'audio', attrs: { enc: 'opus', rate: '16000' }, content: undefined });
398
+ offerContent.push({ tag: 'audio', attrs: { enc: 'opus', rate: '8000' }, content: undefined });
399
+ offerContent.push({ tag: 'net', attrs: { medium: '3' }, content: undefined });
400
+ offerContent.push({ tag: 'capability', attrs: { ver: '1' }, content: new Uint8Array([1, 4, 255, 131, 207, 4]) });
401
+ offerContent.push({ tag: 'encopt', attrs: { keygen: '2' }, content: undefined });
402
+ const encKey = randomBytes(32);
403
+ const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device));
404
+ await assertSessions(devices, true);
405
+ const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, { call: { callKey: new Uint8Array(encKey) } }, { count: '0' });
406
+ offerContent.push({ tag: 'destination', attrs: {}, content: destinations });
407
+ if (shouldIncludeDeviceIdentity) {
408
+ offerContent.push({
409
+ tag: 'device-identity',
410
+ attrs: {},
411
+ content: encodeSignedDeviceIdentity(authState.creds.account, true)
412
+ });
413
+ }
414
+ await query({
415
+ tag: 'call',
416
+ attrs: { id: generateMessageTag(), to: toJid },
417
+ content: [
418
+ { tag: 'offer', attrs: { 'call-id': callId, 'call-creator': authState.creds.me.id }, content: offerContent }
419
+ ]
420
+ });
421
+ return { id: callId, to: toJid };
422
+ };
423
+ const initiateCall = async (jid, options = {}) => {
424
+ const meId = authState.creds.me?.id;
425
+ if (!meId)
426
+ throw new Boom('Not authenticated');
427
+ const isVideo = !!options.isVideo;
428
+ const isGroup = isJidGroup(jid);
429
+ const result = await offerCall(jid, isVideo);
430
+ const callId = result.id;
431
+ await callOfferCache.set(callId, {
432
+ chatId: jid,
433
+ from: meId,
434
+ id: callId,
435
+ date: new Date(),
436
+ offline: false,
437
+ status: 'offer',
438
+ isVideo,
439
+ isGroup,
440
+ groupJid: isGroup ? jid : undefined
441
+ });
442
+ return { callId, to: jid, isVideo };
443
+ };
444
+ const terminateCall = async (callId, callTo, callCreator, reason, duration) => {
445
+ const meId = authState.creds.me?.id;
446
+ if (!meId)
447
+ throw new Boom('Not authenticated', { statusCode: 401 });
448
+ const attrs = { 'call-id': callId, 'call-creator': callCreator || meId };
449
+ if (reason)
450
+ attrs.reason = reason;
451
+ if (typeof duration === 'number') {
452
+ attrs.duration = String(duration);
453
+ attrs.audio_duration = String(duration);
454
+ }
455
+ await query({
456
+ tag: 'call',
457
+ attrs: { to: callTo, id: randomBytes(16).toString('hex').toUpperCase() },
458
+ content: [{ tag: 'terminate', attrs, content: undefined }]
459
+ });
460
+ await callOfferCache.del(callId);
461
+ };
462
+ const cancelCall = async (callId, callTo) => terminateCall(callId, callTo);
463
+ const acceptCall = async (callId, callFrom, isVideo) => {
464
+ const meId = authState.creds.me?.id;
465
+ if (!meId)
466
+ throw new Boom('Not authenticated', { statusCode: 401 });
467
+ const content = [{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }];
468
+ if (isVideo)
469
+ content.push({ tag: 'video', attrs: { dec: 'H264,AV1', device_orientation: '1' }, content: undefined });
470
+ content.push({ tag: 'net', attrs: { medium: '2' }, content: undefined }, { tag: 'encopt', attrs: { keygen: '2' }, content: undefined });
471
+ await query({
472
+ tag: 'call',
473
+ attrs: { from: meId, to: callFrom, id: randomBytes(16).toString('hex').toUpperCase() },
474
+ content: [{ tag: 'accept', attrs: { 'call-id': callId, 'call-creator': callFrom }, content }]
475
+ });
476
+ };
477
+ const preacceptCall = async (callId, callCreator, isVideo) => {
478
+ const content = [{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }];
479
+ if (isVideo) {
480
+ content.push({
481
+ tag: 'video',
482
+ attrs: { screen_width: '1080', screen_height: '2400', dec: 'H264,H265,AV1', device_orientation: '0' },
483
+ content: undefined
484
+ });
485
+ }
486
+ content.push({ tag: 'encopt', attrs: { keygen: '2' }, content: undefined }, { tag: 'capability', attrs: { ver: '1' }, content: undefined });
487
+ await query({
488
+ tag: 'call',
489
+ attrs: { to: callCreator, id: randomBytes(16).toString('hex').toUpperCase() },
490
+ content: [{ tag: 'preaccept', attrs: { 'call-id': callId, 'call-creator': callCreator }, content }]
491
+ });
492
+ };
493
+ const sendRelayLatency = async (callId, callCreator, relays, transactionId) => {
494
+ const attrs = { 'call-id': callId, 'call-creator': callCreator };
495
+ if (transactionId)
496
+ attrs['transaction-id'] = transactionId;
497
+ await sendNode({
498
+ tag: 'call',
499
+ attrs: { to: callCreator, id: randomBytes(16).toString('hex').toUpperCase() },
500
+ content: [
501
+ {
502
+ tag: 'relaylatency',
503
+ attrs,
504
+ content: relays.map(r => {
505
+ const a = {};
506
+ if (r.relayName)
507
+ a.relay_name = r.relayName;
508
+ a.latency = String(r.latency);
509
+ if (r.relayId)
510
+ a.relay_id = r.relayId;
511
+ if (r.dlBw !== undefined)
512
+ a.dl_bw = String(r.dlBw);
513
+ if (r.ulBw !== undefined)
514
+ a.ul_bw = String(r.ulBw);
515
+ return { tag: 'te', attrs: a, content: undefined };
516
+ })
517
+ }
518
+ ]
519
+ });
520
+ };
521
+ const sendTransport = async (callId, callCreator, to, candidates, round) => {
522
+ const attrs = {
523
+ 'call-id': callId,
524
+ 'call-creator': callCreator,
525
+ 'transport-message-type': '1'
526
+ };
527
+ if (round !== undefined)
528
+ attrs['p2p-cand-round'] = String(round);
529
+ await sendNode({
530
+ tag: 'call',
531
+ attrs: { to, id: randomBytes(16).toString('hex').toUpperCase() },
532
+ content: [
533
+ {
534
+ tag: 'transport',
535
+ attrs,
536
+ content: candidates.map(c => ({ tag: 'te', attrs: { priority: c.priority }, content: c.data }))
537
+ }
538
+ ]
539
+ });
540
+ };
541
+ const sendCallDuration = async (callId, callCreator, peer, audioDuration, callType = '1x1') => {
542
+ await sendNode({
543
+ tag: 'call',
544
+ attrs: { to: 'call', id: randomBytes(16).toString('hex').toUpperCase() },
253
545
  content: [
254
546
  {
255
- tag: 'reject',
547
+ tag: 'duration',
256
548
  attrs: {
257
549
  'call-id': callId,
258
- 'call-creator': callFrom,
259
- count: '0'
550
+ 'call-creator': callCreator,
551
+ peer,
552
+ audio_duration: String(audioDuration),
553
+ type: callType
260
554
  },
261
555
  content: undefined
262
556
  }
263
557
  ]
264
- };
265
- await query(stanza);
558
+ });
559
+ };
560
+ const muteCall = async (callId, callCreator, to, muted) => {
561
+ await sendNode({
562
+ tag: 'call',
563
+ attrs: { to, id: randomBytes(16).toString('hex').toUpperCase() },
564
+ content: [
565
+ {
566
+ tag: 'mute_v2',
567
+ attrs: { 'mute-state': muted ? '1' : '0', 'call-id': callId, 'call-creator': callCreator },
568
+ content: undefined
569
+ }
570
+ ]
571
+ });
572
+ };
573
+ const sendHeartbeat = async (callId, callCreator) => {
574
+ await sendNode({
575
+ tag: 'call',
576
+ attrs: { to: `${callId}@call`, id: randomBytes(16).toString('hex').toUpperCase() },
577
+ content: [{ tag: 'heartbeat', attrs: { 'call-id': callId, 'call-creator': callCreator }, content: undefined }]
578
+ });
579
+ };
580
+ const sendEncRekey = async (callId, callCreator, to, transactionId) => {
581
+ await sendNode({
582
+ tag: 'call',
583
+ attrs: { to, id: randomBytes(16).toString('hex').toUpperCase() },
584
+ content: [
585
+ {
586
+ tag: 'enc_rekey',
587
+ attrs: { 'transaction-id': transactionId, 'call-id': callId, 'call-creator': callCreator },
588
+ content: [
589
+ { tag: 'encopt', attrs: { keygen: '2' }, content: undefined },
590
+ { tag: 'enc', attrs: { v: '2', type: 'msg' }, content: undefined }
591
+ ]
592
+ }
593
+ ]
594
+ });
595
+ };
596
+ const sendVideoState = async (callId, callCreator, to, enabled, orientation = '1') => {
597
+ await sendNode({
598
+ tag: 'call',
599
+ attrs: { to, id: randomBytes(16).toString('hex').toUpperCase() },
600
+ content: [
601
+ {
602
+ tag: 'video',
603
+ attrs: {
604
+ 'call-id': callId,
605
+ 'call-creator': callCreator,
606
+ state: enabled ? '1' : '0',
607
+ device_orientation: orientation
608
+ },
609
+ content: undefined
610
+ }
611
+ ]
612
+ });
613
+ };
614
+ const queryCallLink = async (token, media = 'video') => {
615
+ return await query({
616
+ tag: 'call',
617
+ attrs: { to: 'call', id: randomBytes(16).toString('hex').toUpperCase() },
618
+ content: [{ tag: 'link_query', attrs: { media, token }, content: undefined }]
619
+ });
620
+ };
621
+ const joinCallLink = async (token, media = 'video') => {
622
+ const content = [
623
+ { tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined },
624
+ { tag: 'net', attrs: { medium: '2' }, content: undefined },
625
+ { tag: 'capability', attrs: { ver: '1' }, content: undefined }
626
+ ];
627
+ if (media === 'video') {
628
+ content.splice(1, 0, {
629
+ tag: 'video',
630
+ attrs: { screen_width: '1080', screen_height: '2400', dec: 'H264,H265,AV1', device_orientation: '0' },
631
+ content: undefined
632
+ });
633
+ }
634
+ return await query({
635
+ tag: 'call',
636
+ attrs: { to: 'call', id: randomBytes(16).toString('hex').toUpperCase() },
637
+ content: [{ tag: 'link_join', attrs: { media, token }, content }]
638
+ });
266
639
  };
267
640
  const sendRetryRequest = async (node, forceIncludeKeys = false) => {
268
641
  const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '');
@@ -300,12 +673,12 @@ export const makeMessagesRecvSocket = (config) => {
300
673
  // Check if we should recreate the session
301
674
  let shouldRecreateSession = false;
302
675
  let recreateReason = '';
303
- if (enableAutoSessionRecreation && messageRetryManager) {
676
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
304
677
  try {
305
678
  // Check if we have a session with this JID
306
679
  const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid);
307
680
  const hasSession = await signalRepository.validateSession(fromJid);
308
- const result = messageRetryManager.shouldRecreateSession(fromJid, retryCount, hasSession.exists);
681
+ const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists);
309
682
  shouldRecreateSession = result.recreate;
310
683
  recreateReason = result.reason;
311
684
  if (shouldRecreateSession) {
@@ -407,22 +780,16 @@ export const makeMessagesRecvSocket = (config) => {
407
780
  }
408
781
  }
409
782
  else {
410
- const identityNode = getBinaryNodeChild(node, 'identity');
411
- if (identityNode) {
412
- logger.info({ jid: from }, 'identity changed');
413
- if (identityAssertDebounce.get(from)) {
414
- logger.debug({ jid: from }, 'skipping identity assert (debounced)');
415
- return;
416
- }
417
- identityAssertDebounce.set(from, true);
418
- try {
419
- await assertSessions([from], true);
420
- }
421
- catch (error) {
422
- logger.warn({ error, jid: from }, 'failed to assert sessions after identity change');
423
- }
424
- }
425
- else {
783
+ const result = await handleIdentityChange(node, {
784
+ meId: authState.creds.me?.id,
785
+ meLid: authState.creds.me?.lid,
786
+ validateSession: signalRepository.validateSession,
787
+ assertSessions,
788
+ debounceCache: identityAssertDebounce,
789
+ logger,
790
+ onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange
791
+ });
792
+ if (result.action === 'no_identity_node') {
426
793
  logger.info({ node }, 'unknown encrypt notification');
427
794
  }
428
795
  }
@@ -431,6 +798,7 @@ export const makeMessagesRecvSocket = (config) => {
431
798
  // TODO: Support PN/LID (Here is only LID now)
432
799
  const actingParticipantLid = fullNode.attrs.participant;
433
800
  const actingParticipantPn = fullNode.attrs.participant_pn;
801
+ const actingParticipantUsername = fullNode.attrs.participant_username;
434
802
  const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid;
435
803
  const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn;
436
804
  switch (child?.tag) {
@@ -450,7 +818,8 @@ export const makeMessagesRecvSocket = (config) => {
450
818
  {
451
819
  ...metadata,
452
820
  author: actingParticipantLid,
453
- authorPn: actingParticipantPn
821
+ authorPn: actingParticipantPn,
822
+ authorUsername: actingParticipantUsername
454
823
  }
455
824
  ]);
456
825
  break;
@@ -556,24 +925,11 @@ export const makeMessagesRecvSocket = (config) => {
556
925
  const nodeType = node.attrs.type;
557
926
  const from = jidNormalizedUser(node.attrs.from);
558
927
  switch (nodeType) {
559
- case 'privacy_token':
560
- const tokenList = getBinaryNodeChildren(child, 'token');
561
- for (const { attrs, content } of tokenList) {
562
- const jid = attrs.jid;
563
- ev.emit('chats.update', [
564
- {
565
- id: jid,
566
- tcToken: content
567
- }
568
- ]);
569
- logger.debug({ jid }, 'got privacy token update');
570
- }
571
- break;
572
928
  case 'newsletter':
573
929
  await handleNewsletterNotification(node);
574
930
  break;
575
931
  case 'mex':
576
- await handleMexNewsletterNotification(node);
932
+ await handleMexNotification(node);
577
933
  break;
578
934
  case 'w:gp2':
579
935
  // TODO: HANDLE PARTICIPANT_PN
@@ -605,6 +961,7 @@ export const makeMessagesRecvSocket = (config) => {
605
961
  case 'picture':
606
962
  const setPicture = getBinaryNodeChild(node, 'set');
607
963
  const delPicture = getBinaryNodeChild(node, 'delete');
964
+ // TODO: WAJIDHASH stuff proper support inhouse
608
965
  ev.emit('contacts.update', [
609
966
  {
610
967
  id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
@@ -657,7 +1014,7 @@ export const makeMessagesRecvSocket = (config) => {
657
1014
  const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey);
658
1015
  const random = randomBytes(32);
659
1016
  const linkCodeSalt = randomBytes(32);
660
- const linkCodePairingExpanded = await hkdf(companionSharedKey, 32, {
1017
+ const linkCodePairingExpanded = hkdf(companionSharedKey, 32, {
661
1018
  salt: linkCodeSalt,
662
1019
  info: 'link_code_pairing_key_bundle_encryption_key'
663
1020
  });
@@ -671,7 +1028,7 @@ export const makeMessagesRecvSocket = (config) => {
671
1028
  const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted]);
672
1029
  const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey);
673
1030
  const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random]);
674
- authState.creds.advSecretKey = (await hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64');
1031
+ authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64');
675
1032
  await query({
676
1033
  tag: 'iq',
677
1034
  attrs: {
@@ -718,27 +1075,91 @@ export const makeMessagesRecvSocket = (config) => {
718
1075
  return result;
719
1076
  }
720
1077
  };
1078
+ /**
1079
+ * In-memory cache of storage JIDs with stored tctokens, seeded from the persisted index.
1080
+ */
1081
+ const tcTokenKnownJids = new Set();
1082
+ const tcTokenIndexLoaded = (async () => {
1083
+ try {
1084
+ const jids = await readTcTokenIndex(authState.keys);
1085
+ for (const jid of jids)
1086
+ tcTokenKnownJids.add(jid);
1087
+ logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index');
1088
+ }
1089
+ catch (err) {
1090
+ logger.warn({ err: err?.message }, 'failed to load tctoken index');
1091
+ }
1092
+ })();
1093
+ let tcTokenIndexTimer;
1094
+ async function flushTcTokenIndex() {
1095
+ if (tcTokenIndexTimer) {
1096
+ clearTimeout(tcTokenIndexTimer);
1097
+ tcTokenIndexTimer = undefined;
1098
+ }
1099
+ const write = await buildMergedTcTokenIndexWrite(authState.keys, tcTokenKnownJids);
1100
+ return authState.keys.set({ tctoken: write });
1101
+ }
1102
+ function scheduleTcTokenIndexSave() {
1103
+ if (tcTokenIndexTimer) {
1104
+ clearTimeout(tcTokenIndexTimer);
1105
+ }
1106
+ tcTokenIndexTimer = setTimeout(() => {
1107
+ tcTokenIndexTimer = undefined;
1108
+ flushTcTokenIndex().catch(err => {
1109
+ logger.warn({ err: err?.message }, 'failed to save tctoken index');
1110
+ });
1111
+ }, 5000);
1112
+ }
1113
+ function trackTcTokenJid(jid) {
1114
+ if (jid && jid !== TC_TOKEN_INDEX_KEY && !tcTokenKnownJids.has(jid)) {
1115
+ tcTokenKnownJids.add(jid);
1116
+ scheduleTcTokenIndexSave();
1117
+ }
1118
+ }
721
1119
  const handlePrivacyTokenNotification = async (node) => {
722
1120
  const tokensNode = getBinaryNodeChild(node, 'tokens');
723
- const from = jidNormalizedUser(node.attrs.from);
724
1121
  if (!tokensNode)
725
1122
  return;
726
- const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
727
- for (const tokenNode of tokenNodes) {
728
- const { attrs, content } = tokenNode;
729
- const type = attrs.type;
730
- const timestamp = attrs.t;
731
- if (type === 'trusted_contact' && content instanceof Buffer) {
732
- logger.debug({
733
- from,
734
- timestamp,
735
- tcToken: content
736
- }, 'received trusted contact token');
737
- await authState.keys.set({
738
- tctoken: { [from]: { token: content, timestamp } }
739
- });
1123
+ const from = jidNormalizedUser(node.attrs.from);
1124
+ const senderLid = node.attrs.sender_lid && isLidUser(jidNormalizedUser(node.attrs.sender_lid))
1125
+ ? jidNormalizedUser(node.attrs.sender_lid)
1126
+ : undefined;
1127
+ const fallbackJid = senderLid ?? (await resolveTcTokenJid(from, getLIDForPN));
1128
+ logger.debug({ from, storageJid: fallbackJid }, 'processing privacy token notification');
1129
+ await storeTcTokensFromIqResult({
1130
+ result: node,
1131
+ fallbackJid,
1132
+ keys: authState.keys,
1133
+ getLIDForPN,
1134
+ onNewJidStored: trackTcTokenJid
1135
+ });
1136
+ };
1137
+ /**
1138
+ * Fire-and-forget tctoken re-issuance after a peer's device identity changed.
1139
+ */
1140
+ const reissueTcTokenAfterIdentityChange = (from) => {
1141
+ void (async () => {
1142
+ const normalizedJid = jidNormalizedUser(from);
1143
+ const tcJid = await resolveTcTokenJid(normalizedJid, getLIDForPN);
1144
+ const tcTokenData = await authState.keys.get('tctoken', [tcJid]);
1145
+ const senderTs = tcTokenData?.[tcJid]?.senderTimestamp;
1146
+ if (senderTs === null || senderTs === undefined || isTcTokenExpired(senderTs)) {
1147
+ return;
740
1148
  }
741
- }
1149
+ logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken');
1150
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
1151
+ const issueJid = await resolveIssuanceJid(normalizedJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID);
1152
+ const result = await issuePrivacyTokens([issueJid], senderTs);
1153
+ await storeTcTokensFromIqResult({
1154
+ result,
1155
+ fallbackJid: tcJid,
1156
+ keys: authState.keys,
1157
+ getLIDForPN,
1158
+ onNewJidStored: trackTcTokenJid
1159
+ });
1160
+ })().catch(err => {
1161
+ logger.debug({ jid: from, err: err?.message }, 'failed to re-issue tctoken after identity change');
1162
+ });
742
1163
  };
743
1164
  async function decipherLinkPublicKey(data) {
744
1165
  const buffer = toRequiredBuffer(data);
@@ -802,11 +1223,11 @@ export const makeMessagesRecvSocket = (config) => {
802
1223
  // Check if we should recreate session for this retry
803
1224
  let shouldRecreateSession = false;
804
1225
  let recreateReason = '';
805
- if (enableAutoSessionRecreation && messageRetryManager) {
1226
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
806
1227
  try {
807
1228
  const sessionId = signalRepository.jidToSignalProtocolAddress(participant);
808
1229
  const hasSession = await signalRepository.validateSession(participant);
809
- const result = messageRetryManager.shouldRecreateSession(participant, retryCount, hasSession.exists);
1230
+ const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists);
810
1231
  shouldRecreateSession = result.recreate;
811
1232
  recreateReason = result.reason;
812
1233
  if (shouldRecreateSession) {
@@ -869,7 +1290,7 @@ export const makeMessagesRecvSocket = (config) => {
869
1290
  }
870
1291
  try {
871
1292
  await Promise.all([
872
- processingMutex.mutex(async () => {
1293
+ receiptMutex.mutex(async () => {
873
1294
  const status = getStatusFromReceiptType(attrs.type);
874
1295
  if (typeof status !== 'undefined' &&
875
1296
  // basically, we only want to know when a message from us has been delivered to/read by the other person
@@ -890,7 +1311,7 @@ export const makeMessagesRecvSocket = (config) => {
890
1311
  else {
891
1312
  ev.emit('messages.update', ids.map(id => ({
892
1313
  key: { ...key, id },
893
- update: { status }
1314
+ update: { status, messageTimestamp: toNumber(+(attrs.t ?? 0)) }
894
1315
  })));
895
1316
  }
896
1317
  }
@@ -933,7 +1354,7 @@ export const makeMessagesRecvSocket = (config) => {
933
1354
  }
934
1355
  try {
935
1356
  await Promise.all([
936
- processingMutex.mutex(async () => {
1357
+ notificationMutex.mutex(async () => {
937
1358
  const msg = await processNotification(node);
938
1359
  if (msg) {
939
1360
  const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id);
@@ -967,88 +1388,158 @@ export const makeMessagesRecvSocket = (config) => {
967
1388
  }
968
1389
  const encNode = getBinaryNodeChild(node, 'enc');
969
1390
  // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
970
- if (encNode && encNode.attrs.type === 'msmsg') {
1391
+ if (encNode?.attrs.type === 'msmsg') {
971
1392
  logger.debug({ key: node.attrs.key }, 'ignored msmsg');
972
1393
  await sendMessageAck(node, NACK_REASONS.MissingMessageSecret);
973
1394
  return;
974
1395
  }
975
- const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
976
- const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
977
- // store new mappings we didn't have before
978
- if (!!alt) {
979
- const altServer = jidDecode(alt)?.server;
980
- const primaryJid = msg.key.participant || msg.key.remoteJid;
981
- if (altServer === 'lid') {
982
- if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
983
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]);
984
- await signalRepository.migrateSession(primaryJid, alt);
1396
+ let acked = false;
1397
+ try {
1398
+ const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
1399
+ const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
1400
+ // store new mappings we didn't have before
1401
+ if (!!alt) {
1402
+ const altServer = jidDecode(alt)?.server;
1403
+ const primaryJid = msg.key.participant || msg.key.remoteJid;
1404
+ if (altServer === 'lid') {
1405
+ if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
1406
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]);
1407
+ await signalRepository.migrateSession(primaryJid, alt);
1408
+ }
1409
+ }
1410
+ else {
1411
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
1412
+ await signalRepository.migrateSession(alt, primaryJid);
985
1413
  }
986
1414
  }
987
- else {
988
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
989
- await signalRepository.migrateSession(alt, primaryJid);
1415
+ // Cache for retry receipts BEFORE decrypt — so retry logic works even if decryption throws
1416
+ if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
1417
+ messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
1418
+ logger.debug({ jid: msg.key.remoteJid, id: msg.key.id }, 'Added message to recent cache for retry receipts');
990
1419
  }
991
- }
992
- if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
993
- messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
994
- logger.debug({
995
- jid: msg.key.remoteJid,
996
- id: msg.key.id
997
- }, 'Added message to recent cache for retry receipts');
998
- }
999
- try {
1000
- await processingMutex.mutex(async () => {
1420
+ await messageMutex.mutex(async () => {
1001
1421
  await decrypt();
1002
1422
  // message failed to decrypt
1003
1423
  if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
1004
1424
  if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
1425
+ acked = true;
1005
1426
  return sendMessageAck(node, NACK_REASONS.ParsingError);
1006
1427
  }
1007
- const errorMessage = msg?.messageStubParameters?.[0] || '';
1008
- const isPreKeyError = errorMessage.includes('PreKey');
1009
- logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
1010
- // Handle both pre-key and normal retries in single mutex
1011
- await retryMutex.mutex(async () => {
1012
- try {
1013
- if (!ws.isOpen) {
1014
- logger.debug({ node }, 'Connection closed, skipping retry');
1015
- return;
1016
- }
1017
- // Handle pre-key errors with upload and delay
1018
- if (isPreKeyError) {
1019
- logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
1020
- try {
1021
- logger.debug('Uploading pre-keys for error recovery');
1022
- await uploadPreKeys(5);
1023
- logger.debug('Waiting for server to process new pre-keys');
1024
- await delay(1000);
1025
- }
1026
- catch (uploadErr) {
1027
- logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
1028
- }
1428
+ if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
1429
+ // Message arrived without encryption (e.g. CTWA ads messages).
1430
+ // Check if this is eligible for placeholder resend (matching WA Web filters).
1431
+ const unavailableNode = getBinaryNodeChild(node, 'unavailable');
1432
+ const unavailableType = unavailableNode?.attrs?.type;
1433
+ if (unavailableType === 'bot_unavailable_fanout' ||
1434
+ unavailableType === 'hosted_unavailable_fanout' ||
1435
+ unavailableType === 'view_once_unavailable_fanout') {
1436
+ logger.debug({ msgId: msg.key.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type');
1437
+ acked = true;
1438
+ return sendMessageAck(node);
1439
+ }
1440
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
1441
+ if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) {
1442
+ logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message');
1443
+ acked = true;
1444
+ return sendMessageAck(node);
1445
+ }
1446
+ // Request the real content from the phone via placeholder resend PDO.
1447
+ // Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg),
1448
+ // and store the requestId in stubParameters[1] so users can correlate
1449
+ // with the incoming PDO response event.
1450
+ const cleanKey = {
1451
+ remoteJid: msg.key.remoteJid,
1452
+ fromMe: msg.key.fromMe,
1453
+ id: msg.key.id,
1454
+ participant: msg.key.participant
1455
+ };
1456
+ // Cache the original message metadata so the PDO response handler
1457
+ // can preserve key fields (LID details etc.) that the phone may omit
1458
+ const msgData = {
1459
+ key: msg.key,
1460
+ messageTimestamp: msg.messageTimestamp,
1461
+ pushName: msg.pushName,
1462
+ participant: msg.participant,
1463
+ verifiedBizName: msg.verifiedBizName
1464
+ };
1465
+ requestPlaceholderResend(cleanKey, msgData)
1466
+ .then(requestId => {
1467
+ if (requestId && requestId !== 'RESOLVED') {
1468
+ logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message');
1469
+ ev.emit('messages.update', [
1470
+ {
1471
+ key: msg.key,
1472
+ update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] }
1473
+ }
1474
+ ]);
1029
1475
  }
1030
- const encNode = getBinaryNodeChild(node, 'enc');
1031
- await sendRetryRequest(node, !encNode);
1032
- if (retryRequestDelayMs) {
1033
- await delay(retryRequestDelayMs);
1476
+ })
1477
+ .catch(err => {
1478
+ logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message');
1479
+ });
1480
+ acked = true;
1481
+ await sendMessageAck(node);
1482
+ // Don't return — fall through to upsertMessage so the stub is emitted
1483
+ }
1484
+ else {
1485
+ // Skip retry for expired status messages (>24h old)
1486
+ if (isJidStatusBroadcast(msg.key.remoteJid)) {
1487
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
1488
+ if (messageAge > STATUS_EXPIRY_SECONDS) {
1489
+ logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, 'skipping retry for expired status message');
1490
+ acked = true;
1491
+ return sendMessageAck(node);
1034
1492
  }
1035
1493
  }
1036
- catch (err) {
1037
- logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
1038
- // Still attempt retry even if pre-key upload failed
1494
+ const errorMessage = msg?.messageStubParameters?.[0] || '';
1495
+ const isPreKeyError = errorMessage.includes('PreKey');
1496
+ logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
1497
+ // Handle both pre-key and normal retries in single mutex
1498
+ await retryMutex.mutex(async () => {
1039
1499
  try {
1500
+ if (!ws.isOpen) {
1501
+ logger.debug({ node }, 'Connection closed, skipping retry');
1502
+ return;
1503
+ }
1504
+ // Handle pre-key errors with upload and delay
1505
+ if (isPreKeyError) {
1506
+ logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
1507
+ try {
1508
+ logger.debug('Uploading pre-keys for error recovery');
1509
+ await uploadPreKeys(5);
1510
+ logger.debug('Waiting for server to process new pre-keys');
1511
+ await delay(1000);
1512
+ }
1513
+ catch (uploadErr) {
1514
+ logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
1515
+ }
1516
+ }
1040
1517
  const encNode = getBinaryNodeChild(node, 'enc');
1041
1518
  await sendRetryRequest(node, !encNode);
1519
+ if (retryRequestDelayMs) {
1520
+ await delay(retryRequestDelayMs);
1521
+ }
1042
1522
  }
1043
- catch (retryErr) {
1044
- logger.error({ retryErr }, 'Failed to send retry after error handling');
1523
+ catch (err) {
1524
+ logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
1525
+ // Still attempt retry even if pre-key upload failed
1526
+ try {
1527
+ const encNode = getBinaryNodeChild(node, 'enc');
1528
+ await sendRetryRequest(node, !encNode);
1529
+ }
1530
+ catch (retryErr) {
1531
+ logger.error({ retryErr }, 'Failed to send retry after error handling');
1532
+ }
1045
1533
  }
1046
- }
1047
- await sendMessageAck(node, NACK_REASONS.UnhandledError);
1048
- });
1534
+ acked = true;
1535
+ await sendMessageAck(node, NACK_REASONS.UnhandledError);
1536
+ });
1537
+ }
1049
1538
  }
1050
1539
  else {
1051
- await sendMessageAck(node);
1540
+ if (messageRetryManager && msg.key.id) {
1541
+ messageRetryManager.cancelPendingPhoneRequest(msg.key.id);
1542
+ }
1052
1543
  const isNewsletter = isJidNewsletter(msg.key.remoteJid);
1053
1544
  if (!isNewsletter) {
1054
1545
  // no type in the receipt => message delivered
@@ -1069,15 +1560,18 @@ export const makeMessagesRecvSocket = (config) => {
1069
1560
  else if (!sendActiveReceipts) {
1070
1561
  type = 'inactive';
1071
1562
  }
1563
+ acked = true;
1072
1564
  await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type);
1073
1565
  // send ack for history message
1074
1566
  const isAnyHistoryMsg = getHistoryMsg(msg.message);
1075
1567
  if (isAnyHistoryMsg) {
1076
1568
  const jid = jidNormalizedUser(msg.key.remoteJid);
1077
- await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync');
1569
+ await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync'); // TODO: investigate
1078
1570
  }
1079
1571
  }
1080
1572
  else {
1573
+ acked = true;
1574
+ await sendMessageAck(node);
1081
1575
  logger.debug({ key: msg.key }, 'processed newsletter message without receipts');
1082
1576
  }
1083
1577
  }
@@ -1087,6 +1581,9 @@ export const makeMessagesRecvSocket = (config) => {
1087
1581
  }
1088
1582
  catch (error) {
1089
1583
  logger.error({ error, node: binaryNodeToString(node) }, 'error in handling message');
1584
+ if (!acked) {
1585
+ await sendMessageAck(node, NACK_REASONS.UnhandledError).catch(ackErr => logger.error({ ackErr }, 'failed to ack message after error'));
1586
+ }
1090
1587
  }
1091
1588
  };
1092
1589
  const handleCall = async (node) => {
@@ -1101,6 +1598,7 @@ export const makeMessagesRecvSocket = (config) => {
1101
1598
  const call = {
1102
1599
  chatId: attrs.from,
1103
1600
  from,
1601
+ callerPn: infoChild.attrs['caller_pn'],
1104
1602
  id: callId,
1105
1603
  date: new Date(+attrs.t * 1000),
1106
1604
  offline: !!attrs.offline,
@@ -1117,6 +1615,7 @@ export const makeMessagesRecvSocket = (config) => {
1117
1615
  if (existingCall) {
1118
1616
  call.isVideo = existingCall.isVideo;
1119
1617
  call.isGroup = existingCall.isGroup;
1618
+ call.callerPn = call.callerPn || existingCall.callerPn;
1120
1619
  }
1121
1620
  // delete data once call has ended
1122
1621
  if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
@@ -1127,23 +1626,22 @@ export const makeMessagesRecvSocket = (config) => {
1127
1626
  };
1128
1627
  const handleBadAck = async ({ attrs }) => {
1129
1628
  const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id };
1130
- // WARNING: REFRAIN FROM ENABLING THIS FOR NOW. IT WILL CAUSE A LOOP
1131
- // // current hypothesis is that if pash is sent in the ack
1132
- // // it means -- the message hasn't reached all devices yet
1133
- // // we'll retry sending the message here
1134
- // if(attrs.phash) {
1135
- // logger.info({ attrs }, 'received phash in ack, resending message...')
1136
- // const msg = await getMessage(key)
1137
- // if(msg) {
1138
- // await relayMessage(key.remoteJid!, msg, { messageId: key.id!, useUserDevicesCache: false })
1139
- // } else {
1140
- // logger.warn({ attrs }, 'could not send message again, as it was not found')
1141
- // }
1142
- // }
1143
1629
  // error in acknowledgement,
1144
1630
  // device could not display the message
1145
1631
  if (attrs.error) {
1146
- logger.warn({ attrs }, 'received error in ack');
1632
+ if (attrs.error === SERVER_ERROR_CODES.MissingTcToken) {
1633
+ // 463 = account restricted + no tctoken for this contact.
1634
+ // WA Web prevents this client-side (disables compose bar).
1635
+ // No retry — retrying worsens the restriction by counting
1636
+ // as another "reach out" to an unknown contact.
1637
+ logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted or missing tctoken for contact');
1638
+ }
1639
+ else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
1640
+ logger.warn({ msgId: attrs.id, from: attrs.from }, 'smax-invalid (479): stanza rejected by server — likely stale device session or malformed addressing');
1641
+ }
1642
+ else {
1643
+ logger.warn({ attrs }, 'received error in ack');
1644
+ }
1147
1645
  ev.emit('messages.update', [
1148
1646
  {
1149
1647
  key,
@@ -1153,19 +1651,6 @@ export const makeMessagesRecvSocket = (config) => {
1153
1651
  }
1154
1652
  }
1155
1653
  ]);
1156
- // resend the message with device_fanout=false, use at your own risk
1157
- // if (attrs.error === '475') {
1158
- // const msg = await getMessage(key)
1159
- // if (msg) {
1160
- // await relayMessage(key.remoteJid!, msg, {
1161
- // messageId: key.id!,
1162
- // useUserDevicesCache: false,
1163
- // additionalAttributes: {
1164
- // device_fanout: 'false'
1165
- // }
1166
- // })
1167
- // }
1168
- // }
1169
1654
  }
1170
1655
  };
1171
1656
  /// processes a node with the given function
@@ -1178,6 +1663,10 @@ export const makeMessagesRecvSocket = (config) => {
1178
1663
  return exec(node, false).catch(err => onUnexpectedError(err, identifier));
1179
1664
  }
1180
1665
  };
1666
+ /** Yields control to the event loop to prevent blocking */
1667
+ const yieldToEventLoop = () => {
1668
+ return new Promise(resolve => setImmediate(resolve));
1669
+ };
1181
1670
  const makeOfflineNodeProcessor = () => {
1182
1671
  const nodeProcessorMap = new Map([
1183
1672
  ['message', handleMessage],
@@ -1187,6 +1676,8 @@ export const makeMessagesRecvSocket = (config) => {
1187
1676
  ]);
1188
1677
  const nodes = [];
1189
1678
  let isProcessing = false;
1679
+ // Number of nodes to process before yielding to event loop
1680
+ const BATCH_SIZE = 10;
1190
1681
  const enqueue = (type, node) => {
1191
1682
  nodes.push({ type, node });
1192
1683
  if (isProcessing) {
@@ -1194,6 +1685,7 @@ export const makeMessagesRecvSocket = (config) => {
1194
1685
  }
1195
1686
  isProcessing = true;
1196
1687
  const promise = async () => {
1688
+ let processedInBatch = 0;
1197
1689
  while (nodes.length && ws.isOpen) {
1198
1690
  const { type, node } = nodes.shift();
1199
1691
  const nodeProcessor = nodeProcessorMap.get(type);
@@ -1202,6 +1694,13 @@ export const makeMessagesRecvSocket = (config) => {
1202
1694
  continue;
1203
1695
  }
1204
1696
  await nodeProcessor(node);
1697
+ processedInBatch++;
1698
+ // Yield to event loop after processing a batch
1699
+ // This prevents blocking the event loop for too long when there are many offline nodes
1700
+ if (processedInBatch >= BATCH_SIZE) {
1701
+ processedInBatch = 0;
1702
+ await yieldToEventLoop();
1703
+ }
1205
1704
  }
1206
1705
  isProcessing = false;
1207
1706
  };
@@ -1266,17 +1765,112 @@ export const makeMessagesRecvSocket = (config) => {
1266
1765
  await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
1267
1766
  }
1268
1767
  });
1269
- ev.on('connection.update', ({ isOnline }) => {
1768
+ /** timestamp of last tctoken prune run — throttles to once per 24h */
1769
+ let lastTcTokenPruneTs = 0;
1770
+ ev.on('connection.update', ({ isOnline, connection }) => {
1270
1771
  if (typeof isOnline !== 'undefined') {
1271
1772
  sendActiveReceipts = isOnline;
1272
1773
  logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`);
1273
1774
  }
1775
+ // Flush pending tctoken index save on disconnect to avoid writing after close
1776
+ if (connection === 'close' && tcTokenIndexTimer) {
1777
+ clearTimeout(tcTokenIndexTimer);
1778
+ tcTokenIndexTimer = undefined;
1779
+ try {
1780
+ void Promise.resolve(flushTcTokenIndex()).catch(() => { });
1781
+ }
1782
+ catch {
1783
+ /* ignore sync errors */
1784
+ }
1785
+ }
1786
+ // Prune expired tctokens when coming online, at most once per 24 hours
1787
+ if (isOnline) {
1788
+ const now = Date.now();
1789
+ const DAY_MS = 24 * 60 * 60 * 1000;
1790
+ if (now - lastTcTokenPruneTs >= DAY_MS) {
1791
+ lastTcTokenPruneTs = now;
1792
+ void pruneExpiredTcTokens();
1793
+ }
1794
+ }
1274
1795
  });
1796
+ async function pruneExpiredTcTokens() {
1797
+ try {
1798
+ await tcTokenIndexLoaded;
1799
+ const persisted = await readTcTokenIndex(authState.keys);
1800
+ const allJids = new Set(tcTokenKnownJids);
1801
+ for (const jid of persisted)
1802
+ allJids.add(jid);
1803
+ if (!allJids.size)
1804
+ return;
1805
+ const jids = [...allJids];
1806
+ const allTokens = await authState.keys.get('tctoken', jids);
1807
+ const writes = {};
1808
+ const survivors = new Set();
1809
+ let mutated = 0;
1810
+ for (const jid of jids) {
1811
+ const entry = allTokens[jid];
1812
+ if (!entry) {
1813
+ mutated++;
1814
+ continue;
1815
+ }
1816
+ const hasPeerToken = !!entry.token?.length;
1817
+ const peerTokenExpired = hasPeerToken && isTcTokenExpired(entry.timestamp);
1818
+ const hasSenderTs = entry.senderTimestamp !== undefined;
1819
+ const senderTsExpired = hasSenderTs && isTcTokenExpired(entry.senderTimestamp);
1820
+ const keepPeerToken = hasPeerToken && !peerTokenExpired;
1821
+ const keepSenderTs = hasSenderTs && !senderTsExpired;
1822
+ if (!keepPeerToken && !keepSenderTs) {
1823
+ writes[jid] = null;
1824
+ mutated++;
1825
+ }
1826
+ else if (peerTokenExpired && keepSenderTs) {
1827
+ writes[jid] = { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp };
1828
+ survivors.add(jid);
1829
+ mutated++;
1830
+ }
1831
+ else {
1832
+ survivors.add(jid);
1833
+ }
1834
+ }
1835
+ if (mutated === 0)
1836
+ return;
1837
+ await authState.keys.set({
1838
+ tctoken: {
1839
+ ...writes,
1840
+ [TC_TOKEN_INDEX_KEY]: {
1841
+ token: Buffer.from(JSON.stringify([...survivors]))
1842
+ }
1843
+ }
1844
+ });
1845
+ tcTokenKnownJids.clear();
1846
+ for (const jid of survivors)
1847
+ tcTokenKnownJids.add(jid);
1848
+ logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens');
1849
+ }
1850
+ catch (err) {
1851
+ logger.warn({ err: err?.message }, 'failed to prune expired tctokens');
1852
+ }
1853
+ }
1275
1854
  return {
1276
1855
  ...sock,
1277
1856
  sendMessageAck,
1278
1857
  sendRetryRequest,
1858
+ offerCall,
1859
+ initiateCall,
1860
+ cancelCall,
1279
1861
  rejectCall,
1862
+ acceptCall,
1863
+ preacceptCall,
1864
+ terminateCall,
1865
+ sendRelayLatency,
1866
+ sendTransport,
1867
+ sendCallDuration,
1868
+ muteCall,
1869
+ sendHeartbeat,
1870
+ sendEncRekey,
1871
+ sendVideoState,
1872
+ queryCallLink,
1873
+ joinCallLink,
1280
1874
  fetchMessageHistory,
1281
1875
  requestPlaceholderResend,
1282
1876
  messageRetryManager