@pontalabs/baileys 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (447) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +267 -839
  3. package/WAProto/GenerateStatics.sh +3 -4
  4. package/WAProto/WAProto.proto +1215 -511
  5. package/WAProto/fix-imports.js +85 -0
  6. package/WAProto/index.d.ts +14017 -55
  7. package/WAProto/index.js +97689 -29
  8. package/engine-requirements.js +10 -0
  9. package/lib/Defaults/index.d.ts +72 -74
  10. package/lib/Defaults/index.d.ts.map +1 -0
  11. package/lib/Defaults/index.js +94 -108
  12. package/lib/Defaults/index.js.map +1 -0
  13. package/lib/Signal/Group/ciphertext-message.d.ts +8 -7
  14. package/lib/Signal/Group/ciphertext-message.d.ts.map +1 -0
  15. package/lib/Signal/Group/ciphertext-message.js +9 -16
  16. package/lib/Signal/Group/ciphertext-message.js.map +1 -0
  17. package/lib/Signal/Group/group-session-builder.d.ts +11 -13
  18. package/lib/Signal/Group/group-session-builder.d.ts.map +1 -0
  19. package/lib/Signal/Group/group-session-builder.js +19 -61
  20. package/lib/Signal/Group/group-session-builder.js.map +1 -0
  21. package/lib/Signal/Group/group_cipher.d.ts +14 -16
  22. package/lib/Signal/Group/group_cipher.d.ts.map +1 -0
  23. package/lib/Signal/Group/group_cipher.js +41 -70
  24. package/lib/Signal/Group/group_cipher.js.map +1 -0
  25. package/lib/Signal/Group/index.d.ts +12 -11
  26. package/lib/Signal/Group/index.d.ts.map +1 -0
  27. package/lib/Signal/Group/index.js +12 -61
  28. package/lib/Signal/Group/index.js.map +1 -0
  29. package/lib/Signal/Group/keyhelper.d.ts +9 -14
  30. package/lib/Signal/Group/keyhelper.d.ts.map +1 -0
  31. package/lib/Signal/Group/keyhelper.js +10 -58
  32. package/lib/Signal/Group/keyhelper.js.map +1 -0
  33. package/lib/Signal/Group/sender-chain-key.d.ts +13 -13
  34. package/lib/Signal/Group/sender-chain-key.d.ts.map +1 -0
  35. package/lib/Signal/Group/sender-chain-key.js +13 -34
  36. package/lib/Signal/Group/sender-chain-key.js.map +1 -0
  37. package/lib/Signal/Group/sender-key-distribution-message.d.ts +16 -16
  38. package/lib/Signal/Group/sender-key-distribution-message.d.ts.map +1 -0
  39. package/lib/Signal/Group/sender-key-distribution-message.js +28 -36
  40. package/lib/Signal/Group/sender-key-distribution-message.js.map +1 -0
  41. package/lib/Signal/Group/sender-key-message.d.ts +18 -18
  42. package/lib/Signal/Group/sender-key-message.d.ts.map +1 -0
  43. package/lib/Signal/Group/sender-key-message.js +38 -45
  44. package/lib/Signal/Group/sender-key-message.js.map +1 -0
  45. package/lib/Signal/Group/sender-key-name.d.ts +14 -15
  46. package/lib/Signal/Group/sender-key-name.d.ts.map +1 -0
  47. package/lib/Signal/Group/sender-key-name.js +20 -31
  48. package/lib/Signal/Group/sender-key-name.js.map +1 -0
  49. package/lib/Signal/Group/sender-key-record.d.ts +24 -25
  50. package/lib/Signal/Group/sender-key-record.d.ts.map +1 -0
  51. package/lib/Signal/Group/sender-key-record.js +18 -35
  52. package/lib/Signal/Group/sender-key-record.js.map +1 -0
  53. package/lib/Signal/Group/sender-key-state.d.ts +29 -34
  54. package/lib/Signal/Group/sender-key-state.d.ts.map +1 -0
  55. package/lib/Signal/Group/sender-key-state.js +37 -100
  56. package/lib/Signal/Group/sender-key-state.js.map +1 -0
  57. package/lib/Signal/Group/sender-message-key.d.ts +11 -10
  58. package/lib/Signal/Group/sender-message-key.d.ts.map +1 -0
  59. package/lib/Signal/Group/sender-message-key.js +15 -22
  60. package/lib/Signal/Group/sender-message-key.js.map +1 -0
  61. package/lib/Signal/libsignal.d.ts +5 -8
  62. package/lib/Signal/libsignal.d.ts.map +1 -0
  63. package/lib/Signal/libsignal.js +279 -121
  64. package/lib/Signal/libsignal.js.map +1 -0
  65. package/lib/Signal/lid-mapping.d.ts +23 -28
  66. package/lib/Signal/lid-mapping.d.ts.map +1 -0
  67. package/lib/Signal/lid-mapping.js +277 -184
  68. package/lib/Signal/lid-mapping.js.map +1 -0
  69. package/lib/Socket/Client/index.d.ts +3 -2
  70. package/lib/Socket/Client/index.d.ts.map +1 -0
  71. package/lib/Socket/Client/index.js +3 -22
  72. package/lib/Socket/Client/index.js.map +1 -0
  73. package/lib/Socket/Client/types.d.ts +15 -15
  74. package/lib/Socket/Client/types.d.ts.map +1 -0
  75. package/lib/Socket/Client/types.js +8 -15
  76. package/lib/Socket/Client/types.js.map +1 -0
  77. package/lib/Socket/Client/websocket.d.ts +12 -12
  78. package/lib/Socket/Client/websocket.d.ts.map +1 -0
  79. package/lib/Socket/Client/websocket.js +29 -37
  80. package/lib/Socket/Client/websocket.js.map +1 -0
  81. package/lib/Socket/business.d.ts +208 -178
  82. package/lib/Socket/business.d.ts.map +1 -0
  83. package/lib/Socket/business.js +91 -127
  84. package/lib/Socket/business.js.map +1 -0
  85. package/lib/Socket/chats.d.ts +120 -93
  86. package/lib/Socket/chats.d.ts.map +1 -0
  87. package/lib/Socket/chats.js +727 -652
  88. package/lib/Socket/chats.js.map +1 -0
  89. package/lib/Socket/communities.d.ts +273 -0
  90. package/lib/Socket/communities.d.ts.map +1 -0
  91. package/lib/Socket/communities.js +431 -0
  92. package/lib/Socket/communities.js.map +1 -0
  93. package/lib/Socket/dugong.js +261 -0
  94. package/lib/Socket/dugong.js.bak +204 -0
  95. package/lib/Socket/groups.d.ts +143 -111
  96. package/lib/Socket/groups.d.ts.map +1 -0
  97. package/lib/Socket/groups.js +165 -177
  98. package/lib/Socket/groups.js.map +1 -0
  99. package/lib/Socket/index.d.ts +252 -183
  100. package/lib/Socket/index.d.ts.map +1 -0
  101. package/lib/Socket/index.js +38 -18
  102. package/lib/Socket/index.js.bak +44 -0
  103. package/lib/Socket/index.js.map +1 -0
  104. package/lib/Socket/messages-recv.d.ts +196 -167
  105. package/lib/Socket/messages-recv.d.ts.map +1 -0
  106. package/lib/Socket/messages-recv.js +1443 -1563
  107. package/lib/Socket/messages-recv.js.map +1 -0
  108. package/lib/Socket/messages-send.d.ts +192 -158
  109. package/lib/Socket/messages-send.d.ts.map +1 -0
  110. package/lib/Socket/messages-send.js +1102 -1208
  111. package/lib/Socket/messages-send.js.bak +1400 -0
  112. package/lib/Socket/messages-send.js.map +1 -0
  113. package/lib/Socket/mex.d.ts +3 -0
  114. package/lib/Socket/mex.d.ts.map +1 -0
  115. package/lib/Socket/mex.js +42 -0
  116. package/lib/Socket/mex.js.map +1 -0
  117. package/lib/Socket/newsletter.d.ts +163 -138
  118. package/lib/Socket/newsletter.d.ts.map +1 -0
  119. package/lib/Socket/newsletter.js +153 -249
  120. package/lib/Socket/newsletter.js.map +1 -0
  121. package/lib/Socket/socket.d.ts +50 -36
  122. package/lib/Socket/socket.d.ts.map +1 -0
  123. package/lib/Socket/socket.js +749 -820
  124. package/lib/Socket/socket.js.map +1 -0
  125. package/lib/Store/index.d.ts +4 -4
  126. package/lib/Store/index.d.ts.map +1 -0
  127. package/lib/Store/index.js +4 -24
  128. package/lib/Store/index.js.map +1 -0
  129. package/lib/Store/make-in-memory-store.d.ts +44 -104
  130. package/lib/Store/make-in-memory-store.d.ts.map +1 -0
  131. package/lib/Store/make-in-memory-store.js +199 -213
  132. package/lib/Store/make-in-memory-store.js.map +1 -0
  133. package/lib/Store/make-ordered-dictionary.d.ts +13 -12
  134. package/lib/Store/make-ordered-dictionary.d.ts.map +1 -0
  135. package/lib/Store/make-ordered-dictionary.js +38 -45
  136. package/lib/Store/make-ordered-dictionary.js.map +1 -0
  137. package/lib/Store/object-repository.d.ts +11 -10
  138. package/lib/Store/object-repository.d.ts.map +1 -0
  139. package/lib/Store/object-repository.js +9 -16
  140. package/lib/Store/object-repository.js.map +1 -0
  141. package/lib/Types/Auth.d.ts +93 -97
  142. package/lib/Types/Auth.d.ts.map +1 -0
  143. package/lib/Types/Auth.js +2 -3
  144. package/lib/Types/Auth.js.map +1 -0
  145. package/lib/Types/Bussines.d.ts +25 -0
  146. package/lib/Types/Bussines.d.ts.map +1 -0
  147. package/lib/Types/Bussines.js +2 -3
  148. package/lib/Types/Bussines.js.map +1 -0
  149. package/lib/Types/Call.d.ts +14 -13
  150. package/lib/Types/Call.d.ts.map +1 -0
  151. package/lib/Types/Call.js +2 -3
  152. package/lib/Types/Call.js.map +1 -0
  153. package/lib/Types/Chat.d.ts +78 -97
  154. package/lib/Types/Chat.d.ts.map +1 -0
  155. package/lib/Types/Chat.js +8 -9
  156. package/lib/Types/Chat.js.map +1 -0
  157. package/lib/Types/Contact.d.ts +12 -9
  158. package/lib/Types/Contact.d.ts.map +1 -0
  159. package/lib/Types/Contact.js +2 -3
  160. package/lib/Types/Contact.js.map +1 -0
  161. package/lib/Types/Events.d.ts +206 -176
  162. package/lib/Types/Events.d.ts.map +1 -0
  163. package/lib/Types/Events.js +2 -3
  164. package/lib/Types/Events.js.map +1 -0
  165. package/lib/Types/GroupMetadata.d.ts +53 -48
  166. package/lib/Types/GroupMetadata.d.ts.map +1 -0
  167. package/lib/Types/GroupMetadata.js +2 -3
  168. package/lib/Types/GroupMetadata.js.map +1 -0
  169. package/lib/Types/Label.d.ts +13 -14
  170. package/lib/Types/Label.d.ts.map +1 -0
  171. package/lib/Types/Label.js +24 -30
  172. package/lib/Types/Label.js.map +1 -0
  173. package/lib/Types/LabelAssociation.d.ts +15 -20
  174. package/lib/Types/LabelAssociation.d.ts.map +1 -0
  175. package/lib/Types/LabelAssociation.js +6 -12
  176. package/lib/Types/LabelAssociation.js.map +1 -0
  177. package/lib/Types/Message.d.ts +248 -412
  178. package/lib/Types/Message.d.ts.map +1 -0
  179. package/lib/Types/Message.js +17 -19
  180. package/lib/Types/Message.js.map +1 -0
  181. package/lib/Types/Mex.d.ts +141 -0
  182. package/lib/Types/Mex.d.ts.map +1 -0
  183. package/lib/Types/Mex.js +37 -0
  184. package/lib/Types/Mex.js.map +1 -0
  185. package/lib/Types/Product.d.ts +58 -71
  186. package/lib/Types/Product.d.ts.map +1 -0
  187. package/lib/Types/Product.js +2 -3
  188. package/lib/Types/Product.js.map +1 -0
  189. package/lib/Types/RichType.d.ts +3 -0
  190. package/lib/Types/RichType.d.ts.map +1 -0
  191. package/lib/Types/RichType.js +23 -0
  192. package/lib/Types/RichType.js.bak +23 -0
  193. package/lib/Types/RichType.js.map +1 -0
  194. package/lib/Types/Signal.d.ts +71 -82
  195. package/lib/Types/Signal.d.ts.map +1 -0
  196. package/lib/Types/Signal.js +2 -3
  197. package/lib/Types/Signal.js.map +1 -0
  198. package/lib/Types/Socket.d.ts +76 -81
  199. package/lib/Types/Socket.d.ts.map +1 -0
  200. package/lib/Types/Socket.js +3 -3
  201. package/lib/Types/Socket.js.map +1 -0
  202. package/lib/Types/State.d.ts +75 -19
  203. package/lib/Types/State.d.ts.map +1 -0
  204. package/lib/Types/State.js +56 -14
  205. package/lib/Types/State.js.map +1 -0
  206. package/lib/Types/USync.d.ts +8 -8
  207. package/lib/Types/USync.d.ts.map +1 -0
  208. package/lib/Types/USync.js +2 -3
  209. package/lib/Types/USync.js.map +1 -0
  210. package/lib/Types/index.d.ts +47 -62
  211. package/lib/Types/index.d.ts.map +1 -0
  212. package/lib/Types/index.js +26 -50
  213. package/lib/Types/index.js.map +1 -0
  214. package/lib/Utils/auth-utils.d.ts +13 -10
  215. package/lib/Utils/auth-utils.d.ts.map +1 -0
  216. package/lib/Utils/auth-utils.js +213 -454
  217. package/lib/Utils/auth-utils.js.map +1 -0
  218. package/lib/Utils/browser-utils.d.ts +4 -0
  219. package/lib/Utils/browser-utils.d.ts.map +1 -0
  220. package/lib/Utils/browser-utils.js +28 -0
  221. package/lib/Utils/browser-utils.js.map +1 -0
  222. package/lib/Utils/business.d.ts +14 -20
  223. package/lib/Utils/business.d.ts.map +1 -0
  224. package/lib/Utils/business.js +110 -134
  225. package/lib/Utils/business.js.map +1 -0
  226. package/lib/Utils/chat-utils.d.ts +88 -70
  227. package/lib/Utils/chat-utils.d.ts.map +1 -0
  228. package/lib/Utils/chat-utils.js +466 -403
  229. package/lib/Utils/chat-utils.js.map +1 -0
  230. package/lib/Utils/companion-reg-client-utils.d.ts +17 -0
  231. package/lib/Utils/companion-reg-client-utils.d.ts.map +1 -0
  232. package/lib/Utils/companion-reg-client-utils.js +35 -0
  233. package/lib/Utils/companion-reg-client-utils.js.map +1 -0
  234. package/lib/Utils/crypto.d.ts +27 -46
  235. package/lib/Utils/crypto.d.ts.map +1 -0
  236. package/lib/Utils/crypto.js +117 -188
  237. package/lib/Utils/crypto.js.map +1 -0
  238. package/lib/Utils/decode-wa-message.d.ts +58 -45
  239. package/lib/Utils/decode-wa-message.d.ts.map +1 -0
  240. package/lib/Utils/decode-wa-message.js +213 -303
  241. package/lib/Utils/decode-wa-message.js.map +1 -0
  242. package/lib/Utils/event-buffer.d.ts +14 -17
  243. package/lib/Utils/event-buffer.d.ts.map +1 -0
  244. package/lib/Utils/event-buffer.js +321 -296
  245. package/lib/Utils/event-buffer.js.map +1 -0
  246. package/lib/Utils/generics.d.ts +66 -92
  247. package/lib/Utils/generics.d.ts.map +1 -0
  248. package/lib/Utils/generics.js +270 -436
  249. package/lib/Utils/generics.js.map +1 -0
  250. package/lib/Utils/history.d.ts +24 -23
  251. package/lib/Utils/history.d.ts.map +1 -0
  252. package/lib/Utils/history.js +106 -76
  253. package/lib/Utils/history.js.map +1 -0
  254. package/lib/Utils/identity-change-handler.d.ts +44 -0
  255. package/lib/Utils/identity-change-handler.d.ts.map +1 -0
  256. package/lib/Utils/identity-change-handler.js +50 -0
  257. package/lib/Utils/identity-change-handler.js.map +1 -0
  258. package/lib/Utils/index.d.ts +24 -21
  259. package/lib/Utils/index.d.ts.map +1 -0
  260. package/lib/Utils/index.js +24 -41
  261. package/lib/Utils/index.js.map +1 -0
  262. package/lib/Utils/link-preview.d.ts +12 -14
  263. package/lib/Utils/link-preview.d.ts.map +1 -0
  264. package/lib/Utils/link-preview.js +40 -75
  265. package/lib/Utils/link-preview.js.map +1 -0
  266. package/lib/Utils/logger.d.ts +10 -11
  267. package/lib/Utils/logger.d.ts.map +1 -0
  268. package/lib/Utils/logger.js +3 -7
  269. package/lib/Utils/logger.js.map +1 -0
  270. package/lib/Utils/lt-hash.d.ts +8 -14
  271. package/lib/Utils/lt-hash.d.ts.map +1 -0
  272. package/lib/Utils/lt-hash.js +3 -53
  273. package/lib/Utils/lt-hash.js.map +1 -0
  274. package/lib/Utils/make-mutex.d.ts +7 -7
  275. package/lib/Utils/make-mutex.d.ts.map +1 -0
  276. package/lib/Utils/make-mutex.js +28 -44
  277. package/lib/Utils/make-mutex.js.map +1 -0
  278. package/lib/Utils/message-retry-manager.d.ts +115 -88
  279. package/lib/Utils/message-retry-manager.d.ts.map +1 -0
  280. package/lib/Utils/message-retry-manager.js +265 -160
  281. package/lib/Utils/message-retry-manager.js.map +1 -0
  282. package/lib/Utils/messages-media.d.ts +105 -107
  283. package/lib/Utils/messages-media.d.ts.map +1 -0
  284. package/lib/Utils/messages-media.js +548 -619
  285. package/lib/Utils/messages-media.js.bak +786 -0
  286. package/lib/Utils/messages-media.js.map +1 -0
  287. package/lib/Utils/messages.d.ts +52 -66
  288. package/lib/Utils/messages.d.ts.map +1 -0
  289. package/lib/Utils/messages.js +1730 -1558
  290. package/lib/Utils/messages.js.backup +1768 -0
  291. package/lib/Utils/messages.js.bak +1809 -0
  292. package/lib/Utils/messages.js.bak2 +893 -0
  293. package/lib/Utils/messages.js.bak3 +1183 -0
  294. package/lib/Utils/messages.js.map +1 -0
  295. package/lib/Utils/noise-handler.d.ts +18 -18
  296. package/lib/Utils/noise-handler.d.ts.map +1 -0
  297. package/lib/Utils/noise-handler.js +176 -130
  298. package/lib/Utils/noise-handler.js.map +1 -0
  299. package/lib/Utils/offline-node-processor.d.ts +17 -0
  300. package/lib/Utils/offline-node-processor.d.ts.map +1 -0
  301. package/lib/Utils/offline-node-processor.js +40 -0
  302. package/lib/Utils/offline-node-processor.js.map +1 -0
  303. package/lib/Utils/pre-key-manager.d.ts +28 -0
  304. package/lib/Utils/pre-key-manager.d.ts.map +1 -0
  305. package/lib/Utils/pre-key-manager.js +106 -0
  306. package/lib/Utils/pre-key-manager.js.map +1 -0
  307. package/lib/Utils/process-message.d.ts +43 -32
  308. package/lib/Utils/process-message.d.ts.map +1 -0
  309. package/lib/Utils/process-message.js +484 -282
  310. package/lib/Utils/process-message.js.map +1 -0
  311. package/lib/Utils/reporting-utils.d.ts +11 -0
  312. package/lib/Utils/reporting-utils.d.ts.map +1 -0
  313. package/lib/Utils/reporting-utils.js +258 -0
  314. package/lib/Utils/reporting-utils.js.map +1 -0
  315. package/lib/Utils/rich-message-utils.d.ts +49 -0
  316. package/lib/Utils/rich-message-utils.d.ts.map +1 -0
  317. package/lib/Utils/rich-message-utils.js +1220 -724
  318. package/lib/Utils/rich-message-utils.js.backup +706 -0
  319. package/lib/Utils/rich-message-utils.js.bak +246 -0
  320. package/lib/Utils/rich-message-utils.js.map +1 -0
  321. package/lib/Utils/rich-message-utils.js.original +1332 -0
  322. package/lib/Utils/signal.d.ts +37 -32
  323. package/lib/Utils/signal.d.ts.map +1 -0
  324. package/lib/Utils/signal.js +140 -105
  325. package/lib/Utils/signal.js.map +1 -0
  326. package/lib/Utils/stanza-ack.d.ts +11 -0
  327. package/lib/Utils/stanza-ack.d.ts.map +1 -0
  328. package/lib/Utils/stanza-ack.js +38 -0
  329. package/lib/Utils/stanza-ack.js.map +1 -0
  330. package/lib/Utils/sync-action-utils.d.ts +19 -0
  331. package/lib/Utils/sync-action-utils.d.ts.map +1 -0
  332. package/lib/Utils/sync-action-utils.js +49 -0
  333. package/lib/Utils/sync-action-utils.js.map +1 -0
  334. package/lib/Utils/tc-token-utils.d.ts +37 -0
  335. package/lib/Utils/tc-token-utils.d.ts.map +1 -0
  336. package/lib/Utils/tc-token-utils.js +163 -0
  337. package/lib/Utils/tc-token-utils.js.map +1 -0
  338. package/lib/Utils/use-multi-file-auth-state.d.ts +5 -5
  339. package/lib/Utils/use-multi-file-auth-state.d.ts.map +1 -0
  340. package/lib/Utils/use-multi-file-auth-state.js +70 -120
  341. package/lib/Utils/use-multi-file-auth-state.js.map +1 -0
  342. package/lib/Utils/use-single-file-auth-state.d.ts +11 -13
  343. package/lib/Utils/use-single-file-auth-state.d.ts.map +1 -0
  344. package/lib/Utils/use-single-file-auth-state.js +92 -66
  345. package/lib/Utils/use-single-file-auth-state.js.map +1 -0
  346. package/lib/Utils/use-sqlite-auth-state.d.ts +11 -0
  347. package/lib/Utils/use-sqlite-auth-state.d.ts.map +1 -0
  348. package/lib/Utils/use-sqlite-auth-state.js +109 -0
  349. package/lib/Utils/use-sqlite-auth-state.js.map +1 -0
  350. package/lib/Utils/validate-connection.d.ts +11 -13
  351. package/lib/Utils/validate-connection.d.ts.map +1 -0
  352. package/lib/Utils/validate-connection.js +128 -113
  353. package/lib/Utils/validate-connection.js.map +1 -0
  354. package/lib/WABinary/constants.d.ts +25 -27
  355. package/lib/WABinary/constants.d.ts.map +1 -0
  356. package/lib/WABinary/constants.js +1451 -1292
  357. package/lib/WABinary/constants.js.bak +1373 -0
  358. package/lib/WABinary/constants.js.map +1 -0
  359. package/lib/WABinary/decode.d.ts +7 -9
  360. package/lib/WABinary/decode.d.ts.map +1 -0
  361. package/lib/WABinary/decode.js +163 -189
  362. package/lib/WABinary/decode.js.map +1 -0
  363. package/lib/WABinary/encode.d.ts +3 -3
  364. package/lib/WABinary/encode.d.ts.map +1 -0
  365. package/lib/WABinary/encode.js +110 -155
  366. package/lib/WABinary/encode.js.map +1 -0
  367. package/lib/WABinary/generic-utils.d.ts +14 -27
  368. package/lib/WABinary/generic-utils.d.ts.map +1 -0
  369. package/lib/WABinary/generic-utils.js +177 -105
  370. package/lib/WABinary/generic-utils.js.map +1 -0
  371. package/lib/WABinary/index.d.ts +6 -5
  372. package/lib/WABinary/index.d.ts.map +1 -0
  373. package/lib/WABinary/index.js +6 -25
  374. package/lib/WABinary/index.js.map +1 -0
  375. package/lib/WABinary/jid-utils.d.ts +41 -51
  376. package/lib/WABinary/jid-utils.d.ts.map +1 -0
  377. package/lib/WABinary/jid-utils.js +83 -120
  378. package/lib/WABinary/jid-utils.js.map +1 -0
  379. package/lib/WABinary/types.d.ts +10 -13
  380. package/lib/WABinary/types.d.ts.map +1 -0
  381. package/lib/WABinary/types.js +2 -3
  382. package/lib/WABinary/types.js.map +1 -0
  383. package/lib/WAM/BinaryInfo.d.ts +8 -15
  384. package/lib/WAM/BinaryInfo.d.ts.map +1 -0
  385. package/lib/WAM/BinaryInfo.js +7 -14
  386. package/lib/WAM/BinaryInfo.js.map +1 -0
  387. package/lib/WAM/constants.d.ts +30 -37
  388. package/lib/WAM/constants.d.ts.map +1 -0
  389. package/lib/WAM/constants.js +19193 -11711
  390. package/lib/WAM/constants.js.map +1 -0
  391. package/lib/WAM/encode.d.ts +3 -3
  392. package/lib/WAM/encode.d.ts.map +1 -0
  393. package/lib/WAM/encode.js +95 -109
  394. package/lib/WAM/encode.js.map +1 -0
  395. package/lib/WAM/index.d.ts +4 -3
  396. package/lib/WAM/index.d.ts.map +1 -0
  397. package/lib/WAM/index.js +4 -23
  398. package/lib/WAM/index.js.map +1 -0
  399. package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts +9 -9
  400. package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts.map +1 -0
  401. package/lib/WAUSync/Protocols/USyncContactProtocol.js +37 -21
  402. package/lib/WAUSync/Protocols/USyncContactProtocol.js.map +1 -0
  403. package/lib/WAUSync/Protocols/USyncDeviceProtocol.d.ts +19 -22
  404. package/lib/WAUSync/Protocols/USyncDeviceProtocol.d.ts.map +1 -0
  405. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +18 -26
  406. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js.map +1 -0
  407. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts +11 -12
  408. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts.map +1 -0
  409. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +12 -20
  410. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js.map +1 -0
  411. package/lib/WAUSync/Protocols/USyncStatusProtocol.d.ts +11 -12
  412. package/lib/WAUSync/Protocols/USyncStatusProtocol.d.ts.map +1 -0
  413. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +16 -24
  414. package/lib/WAUSync/Protocols/USyncStatusProtocol.js.map +1 -0
  415. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts +10 -0
  416. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts.map +1 -0
  417. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
  418. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js.map +1 -0
  419. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.d.ts +26 -0
  420. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.d.ts.map +1 -0
  421. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +51 -0
  422. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js.map +1 -0
  423. package/lib/WAUSync/Protocols/UsyncLIDProtocol.d.ts +10 -0
  424. package/lib/WAUSync/Protocols/UsyncLIDProtocol.d.ts.map +1 -0
  425. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +29 -0
  426. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js.map +1 -0
  427. package/lib/WAUSync/Protocols/index.d.ts +6 -6
  428. package/lib/WAUSync/Protocols/index.d.ts.map +1 -0
  429. package/lib/WAUSync/Protocols/index.js +6 -26
  430. package/lib/WAUSync/Protocols/index.js.map +1 -0
  431. package/lib/WAUSync/USyncQuery.d.ts +27 -28
  432. package/lib/WAUSync/USyncQuery.d.ts.map +1 -0
  433. package/lib/WAUSync/USyncQuery.js +68 -62
  434. package/lib/WAUSync/USyncQuery.js.map +1 -0
  435. package/lib/WAUSync/USyncUser.d.ts +15 -10
  436. package/lib/WAUSync/USyncUser.d.ts.map +1 -0
  437. package/lib/WAUSync/USyncUser.js +20 -19
  438. package/lib/WAUSync/USyncUser.js.map +1 -0
  439. package/lib/WAUSync/index.d.ts +4 -3
  440. package/lib/WAUSync/index.d.ts.map +1 -0
  441. package/lib/WAUSync/index.js +4 -23
  442. package/lib/WAUSync/index.js.map +1 -0
  443. package/lib/index.d.ts +11 -12
  444. package/lib/index.d.ts.map +1 -0
  445. package/lib/index.js +12 -39
  446. package/lib/index.js.map +1 -0
  447. package/package.json +86 -42
@@ -1,388 +1,465 @@
1
- "use strict"
2
-
3
- var __importDefault = (this && this.__importDefault) || function(mod) {
4
- return (mod && mod.__esModule) ? mod : {
5
- "default": mod
6
- }
1
+ import NodeCache from '@cacheable/node-cache';
2
+ import { Boom } from '@hapi/boom';
3
+ import { randomBytes } from 'crypto';
4
+ import Long from 'long';
5
+ import { proto } from '../../WAProto/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 { ACCOUNT_RESTRICTED_TEXT, aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, extractE2ESessionFromRetryReceipt, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
9
+ import { makeMutex } from '../Utils/make-mutex.js';
10
+ import { makeOfflineNodeProcessor } from '../Utils/offline-node-processor.js';
11
+ import { buildAckStanza } from '../Utils/stanza-ack.js';
12
+ import { buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveIssuanceJid, resolveTcTokenJid, storeTcTokensFromIqResult, TC_TOKEN_INDEX_KEY } from '../Utils/tc-token-utils.js';
13
+ import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, getBinaryNodeChildUInt, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
14
+ import { extractGroupMetadata } from './groups.js';
15
+ import { makeMessagesSocket } from './messages-send.js';
16
+ const ENFORCEMENT_TYPE_VALUES = new Set(Object.values(ReachoutTimelockEnforcementType));
17
+ function isValidEnforcementType(value) {
18
+ return typeof value === 'string' && ENFORCEMENT_TYPE_VALUES.has(value);
7
19
  }
8
-
9
- Object.defineProperty(exports, "__esModule", {
10
- value: true
11
- })
12
-
13
- const node_cache_1 = __importDefault(require("@cacheable/node-cache"))
14
- const boom_1 = require("@hapi/boom")
15
- const crypto_1 = require("crypto")
16
- const WAProto_1 = require("../../WAProto")
17
- const Defaults_1 = require("../Defaults")
18
- const Types_1 = require("../Types")
19
- const Utils_1 = require("../Utils")
20
- const WABinary_1 = require("../WABinary")
21
- const groups_1 = require("./groups")
22
- const make_mutex_1 = require("../Utils/make-mutex")
23
- const messages_send_1 = require("./messages-send")
24
-
25
- const makeMessagesRecvSocket = (config) => {
26
- const {
27
- logger,
28
- retryRequestDelayMs,
29
- maxMsgRetryCount,
30
- getMessage,
31
- shouldIgnoreJid,
32
- enableAutoSessionRecreation
33
- } = config
34
- const sock = messages_send_1.makeMessagesSocket(config)
35
- const {
36
- ev,
37
- authState,
38
- ws,
39
- processingMutex,
40
- signalRepository,
41
- query,
42
- upsertMessage,
43
- resyncAppState,
44
- onUnexpectedError,
45
- assertSessions,
46
- sendNode,
47
- relayMessage,
48
- sendReceipt,
49
- uploadPreKeys,
50
- groupMetadata,
51
- getUSyncDevices,
52
- createParticipantNodes,
53
- messageRetryManager,
54
- sendPeerDataOperationMessage
55
- } = sock
56
-
57
- const retryMutex = make_mutex_1.makeMutex()
58
- // Mutex terpisah dari processingMutex (dipakai chats.js/messages-send.js) supaya
59
- // handleMessage/handleReceipt/handleNotification tidak saling antre di satu antrean
60
- // yang sama. Kalau satu pesan lambat diproses (nunggu plugin/group metadata), receipt
61
- // & notification lain tetap bisa jalan tanpa nunggu giliran.
62
- const messageMutex = make_mutex_1.makeMutex()
63
- const receiptMutex = make_mutex_1.makeMutex()
64
- const notificationMutex = make_mutex_1.makeMutex()
65
-
66
- const groupDataCache = new Map()
67
- global.groupMetadataCache = async (jid) => {
68
- if (!groupDataCache.has(jid)) {
69
- groupDataCache.set(jid, groupMetadata(jid))
70
- }
71
- return await groupDataCache.get(jid)
72
- }
73
-
74
- const msgRetryCache = config.msgRetryCounterCache || new node_cache_1.default({
75
- stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY,
76
- useClones: false
77
- })
78
-
79
- const callOfferCache = config.callOfferCache || new node_cache_1.default({
80
- stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.CALL_OFFER,
81
- useClones: false
82
- })
83
-
84
- const placeholderResendCache = config.placeholderResendCache || new node_cache_1.default({
85
- stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY,
86
- useClones: false
87
- })
88
-
89
- let sendActiveReceipts = false
90
-
91
- const sendMessageAck = async ({
92
- tag,
93
- attrs,
94
- content
95
- }, errorCode) => {
96
- const stanza = {
97
- tag: 'ack',
98
- attrs: {
99
- id: attrs.id,
100
- to: attrs.from,
101
- class: tag
102
- }
20
+ export const makeMessagesRecvSocket = (config) => {
21
+ const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
22
+ const sock = makeMessagesSocket(config);
23
+ const { userDevicesCache, devicesMutex, ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager, registerSocketEndHandler, issuePrivacyTokens, fetchAccountReachoutTimelock, placeholderResendCache } = sock;
24
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
25
+ /** this mutex ensures that each retryRequest will wait for the previous one to finish */
26
+ const retryMutex = makeMutex();
27
+ const msgRetryCache = config.msgRetryCounterCache ||
28
+ new NodeCache({
29
+ stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
30
+ useClones: false
31
+ });
32
+ const callOfferCache = config.callOfferCache ||
33
+ new NodeCache({
34
+ stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
35
+ useClones: false
36
+ });
37
+ // Debounce identity-change session refreshes per JID to avoid bursts
38
+ const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false });
39
+ let sendActiveReceipts = false;
40
+ const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
41
+ if (!authState.creds.me?.id) {
42
+ throw new Boom('Not authenticated');
103
43
  }
104
-
105
- if (!!errorCode) {
106
- stanza.attrs.error = errorCode.toString()
107
- }
108
-
109
- if (!!attrs.participant) {
110
- stanza.attrs.participant = attrs.participant
111
- }
112
-
113
- if (!!attrs.recipient) {
114
- stanza.attrs.recipient = attrs.recipient
115
- }
116
-
117
- if (!!attrs.type && (tag !== 'message' || WABinary_1.getBinaryNodeChild({
118
- tag,
119
- attrs,
120
- content
121
- }, 'unavailable') || errorCode !== 0)) {
122
- stanza.attrs.type = attrs.type
123
- }
124
-
125
- if (tag === 'message' && WABinary_1.getBinaryNodeChild({
126
- tag,
127
- attrs,
128
- content
129
- }, 'unavailable')) {
130
- stanza.attrs.from = authState.creds.me.id
131
- }
132
-
133
- logger.debug({
134
- recv: {
135
- tag,
136
- attrs
137
- },
138
- sent: stanza.attrs
139
- }, 'sent ack')
140
- await sendNode(stanza)
141
- }
142
-
143
- const offerCall = async (toJid, isVideo = false) => {
144
- const callId = crypto_1.randomBytes(16).toString('hex').toUpperCase().substring(0, 64)
145
- const offerContent = []
146
- offerContent.push({
147
- tag: 'audio',
148
- attrs: {
149
- enc: 'opus',
150
- rate: '16000'
151
- },
152
- content: undefined
153
- })
154
- offerContent.push({
155
- tag: 'audio',
156
- attrs: {
157
- enc: 'opus',
158
- rate: '8000'
44
+ const pdoMessage = {
45
+ historySyncOnDemandRequest: {
46
+ chatJid: oldestMsgKey.remoteJid,
47
+ oldestMsgFromMe: oldestMsgKey.fromMe,
48
+ oldestMsgId: oldestMsgKey.id,
49
+ oldestMsgTimestampMs: oldestMsgTimestamp,
50
+ onDemandMsgCount: count
159
51
  },
160
- content: undefined
161
- })
162
-
163
- if (isVideo) {
164
- offerContent.push({
165
- tag: 'video',
166
- attrs: {
167
- enc: 'vp8',
168
- dec: 'vp8',
169
- orientation: '0',
170
- 'screen_width': '1920',
171
- 'screen_height': '1080',
172
- 'device_orientation': '0'
173
- },
174
- content: undefined
175
- })
52
+ peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND
53
+ };
54
+ return sendPeerDataOperationMessage(pdoMessage);
55
+ };
56
+ const requestPlaceholderResend = async (messageKey, msgData) => {
57
+ if (!authState.creds.me?.id) {
58
+ throw new Boom('Not authenticated');
176
59
  }
177
- offerContent.push({
178
- tag: 'net',
179
- attrs: {
180
- medium: '3'
181
- },
182
- content: undefined
183
- })
184
- offerContent.push({
185
- tag: 'capability',
186
- attrs: {
187
- ver: '1'
188
- },
189
- content: new Uint8Array([1, 4, 255, 131, 207, 4])
190
- })
191
- offerContent.push({
192
- tag: 'encopt',
193
- attrs: {
194
- keygen: '2'
195
- },
196
- content: undefined
197
- })
198
-
199
- const encKey = crypto_1.randomBytes(32)
200
- const devices = (await getUSyncDevices([toJid], true, false)).map(({
201
- user,
202
- device
203
- }) => WABinary_1.jidEncode(user, 's.whatsapp.net', device))
204
- await assertSessions(devices, true)
205
-
206
- const {
207
- nodes: destinations,
208
- shouldIncludeDeviceIdentity
209
- } = await createParticipantNodes(devices, {
210
- call: {
211
- callKey: new Uint8Array(encKey)
60
+ if (await placeholderResendCache.get(messageKey?.id)) {
61
+ logger.debug({ messageKey }, 'already requested resend');
62
+ return;
63
+ }
64
+ else {
65
+ // Store original message data so PDO response handler can preserve
66
+ // metadata (LID details, timestamps, etc.) that the phone may omit
67
+ await placeholderResendCache.set(messageKey?.id, msgData || true);
68
+ }
69
+ await delay(2000);
70
+ if (!(await placeholderResendCache.get(messageKey?.id))) {
71
+ logger.debug({ messageKey }, 'message received while resend requested');
72
+ return 'RESOLVED';
73
+ }
74
+ const pdoMessage = {
75
+ placeholderMessageResendRequest: [
76
+ {
77
+ messageKey
78
+ }
79
+ ],
80
+ peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
81
+ };
82
+ setTimeout(async () => {
83
+ if (await placeholderResendCache.get(messageKey?.id)) {
84
+ logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline');
85
+ await placeholderResendCache.del(messageKey?.id);
212
86
  }
213
- }, {
214
- count: '0'
215
- })
216
- offerContent.push({
217
- tag: 'destination',
218
- attrs: {},
219
- content: destinations
220
- })
221
-
222
- if (shouldIncludeDeviceIdentity) {
223
- offerContent.push({
224
- tag: 'device-identity',
225
- attrs: {},
226
- content: Utils_1.encodeSignedDeviceIdentity(authState.creds.account, true)
227
- })
228
- }
229
-
230
- const stanza = ({
231
- tag: 'call',
232
- attrs: {
233
- id: Utils_1.generateMessageID(),
234
- to: toJid,
235
- },
236
- content: [{
237
- tag: 'offer',
238
- attrs: {
239
- 'call-id': callId,
240
- 'call-creator': authState.creds.me.id,
241
- },
242
- content: offerContent,
243
- }],
244
- })
245
-
246
- await query(stanza)
247
-
248
- return {
249
- id: callId,
250
- to: toJid
87
+ }, 8000);
88
+ return sendPeerDataOperationMessage(pdoMessage);
89
+ };
90
+ const handleMexNotification = async (node) => {
91
+ const updateNode = getBinaryNodeChild(node, 'update');
92
+ if (updateNode) {
93
+ const opName = updateNode.attrs?.op_name;
94
+ if (!opName) {
95
+ logger.warn({ node: binaryNodeToString(node) }, 'mex notification missing op_name, fallback to legacy');
96
+ await handleLegacyMexNewsletterNotification(node);
97
+ return;
98
+ }
99
+ let mexResponse;
100
+ try {
101
+ mexResponse = JSON.parse(updateNode.content.toString());
102
+ }
103
+ catch (error) {
104
+ logger.error({ err: error, opName }, 'failed to parse mex notification JSON');
105
+ return;
106
+ }
107
+ if (mexResponse.errors?.length) {
108
+ logger.warn({ errors: mexResponse.errors, opName }, 'mex notification has GQL errors');
109
+ return;
110
+ }
111
+ const data = mexResponse.data;
112
+ if (!data) {
113
+ logger.warn({ opName }, 'mex notification has null data');
114
+ return;
115
+ }
116
+ logger.debug({ opName }, 'processing mex notification');
117
+ switch (opName) {
118
+ case 'NotificationUserReachoutTimelockUpdate':
119
+ handleReachoutTimelockNotification(data);
120
+ break;
121
+ case 'MessageCappingInfoNotification':
122
+ handleMessageCappingNotification(data);
123
+ break;
124
+ // newsletter ops still use the legacy <mex> child structure
125
+ case 'NotificationNewsletterUpdate':
126
+ case 'NotificationLinkedProfilesUpdates':
127
+ case 'NotificationNewsletterAdminPromote':
128
+ case 'NotificationNewsletterAdminDemote':
129
+ case 'NotificationNewsletterUserSettingChange':
130
+ case 'NotificationNewsletterJoin':
131
+ case 'NotificationNewsletterLeave':
132
+ case 'NotificationNewsletterStateChange':
133
+ case 'NotificationNewsletterAdminMetadataUpdate':
134
+ case 'NotificationNewsletterOwnerUpdate':
135
+ case 'NotificationNewsletterAdminInviteRevoke':
136
+ case 'NotificationNewsletterWamoSubStatusChange':
137
+ case 'NotificationNewsletterBlockUser':
138
+ case 'NotificationNewsletterPaidPartnership':
139
+ case 'NotificationNewsletterMilestone':
140
+ case 'NewsletterResponseStateUpdate':
141
+ await handleLegacyMexNewsletterNotification(node);
142
+ break;
143
+ default:
144
+ logger.debug({ opName }, 'unhandled mex notification');
145
+ break;
146
+ }
147
+ return;
251
148
  }
252
- }
253
-
149
+ await handleLegacyMexNewsletterNotification(node);
150
+ };
151
+ const handleReachoutTimelockNotification = (data) => {
152
+ const payload = data.xwa2_notify_account_reachout_timelock;
153
+ if (!payload) {
154
+ logger.warn('reachout timelock notification missing payload');
155
+ return;
156
+ }
157
+ if (!payload.is_active) {
158
+ logger.info('reachout timelock restriction lifted');
159
+ ev.emit('connection.update', {
160
+ reachoutTimeLock: {
161
+ isActive: false,
162
+ enforcementType: ReachoutTimelockEnforcementType.DEFAULT
163
+ }
164
+ });
165
+ return;
166
+ }
167
+ // WA Web defaults to now+60s when the server omits the expiry
168
+ const timeEnforcementEnds = payload.time_enforcement_ends
169
+ ? new Date(parseInt(payload.time_enforcement_ends, 10) * 1000)
170
+ : new Date(Date.now() + 60000);
171
+ const enforcementType = isValidEnforcementType(payload.enforcement_type)
172
+ ? payload.enforcement_type
173
+ : ReachoutTimelockEnforcementType.DEFAULT;
174
+ logger.info({ enforcementType, timeEnforcementEnds }, 'reachout timelock restriction set');
175
+ ev.emit('connection.update', {
176
+ reachoutTimeLock: {
177
+ isActive: true,
178
+ timeEnforcementEnds,
179
+ enforcementType
180
+ }
181
+ });
182
+ };
183
+ const handleMessageCappingNotification = (data) => {
184
+ const payload = data.xwa2_notify_new_chat_messages_capping_info_update;
185
+ if (!payload) {
186
+ logger.warn('message capping notification missing payload');
187
+ return;
188
+ }
189
+ logger.info({ payload }, 'received message capping update');
190
+ ev.emit('message-capping.update', payload);
191
+ };
192
+ const handleLegacyMexNewsletterNotification = async (node) => {
193
+ const mexNode = getBinaryNodeChild(node, 'mex');
194
+ const updateNode = mexNode?.content ? null : getBinaryNodeChild(node, 'update') || getAllBinaryNodeChildren(node)[0];
195
+ const payloadNode = mexNode?.content ? mexNode : updateNode;
196
+ if (!payloadNode?.content) {
197
+ logger.warn({ node: binaryNodeToString(node) }, 'invalid mex newsletter notification');
198
+ return;
199
+ }
200
+ let data;
201
+ try {
202
+ const payloadContent = payloadNode.content;
203
+ if (Array.isArray(payloadContent)) {
204
+ logger.warn({ payloadNode }, 'invalid mex newsletter notification payload format');
205
+ return;
206
+ }
207
+ const contentBuf = typeof payloadContent === 'string' ? Buffer.from(payloadContent, 'binary') : Buffer.from(payloadContent);
208
+ data = JSON.parse(contentBuf.toString());
209
+ }
210
+ catch (error) {
211
+ logger.error({ err: error, node: binaryNodeToString(node) }, 'failed to parse mex newsletter notification');
212
+ return;
213
+ }
214
+ const operation = data?.operation ?? payloadNode?.attrs?.op_name;
215
+ let updates = data?.updates;
216
+ if (!updates) {
217
+ const linkedProfiles = data?.data?.xwa2_notify_linked_profiles;
218
+ if (linkedProfiles) {
219
+ updates = [linkedProfiles];
220
+ }
221
+ }
222
+ if (!updates || !operation) {
223
+ logger.warn({ data }, 'invalid mex newsletter notification content');
224
+ return;
225
+ }
226
+ logger.info({ operation, updates }, 'got mex newsletter notification');
227
+ switch (operation) {
228
+ case 'NotificationNewsletterUpdate':
229
+ for (const update of updates) {
230
+ if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
231
+ ev.emit('newsletter-settings.update', {
232
+ id: update.jid,
233
+ update: update.settings
234
+ });
235
+ }
236
+ }
237
+ break;
238
+ case 'NotificationNewsletterAdminPromote':
239
+ for (const update of updates) {
240
+ if (update.jid && update.user) {
241
+ ev.emit('newsletter-participants.update', {
242
+ id: update.jid,
243
+ author: node.attrs.from,
244
+ user: update.user,
245
+ new_role: 'ADMIN',
246
+ action: 'promote'
247
+ });
248
+ }
249
+ }
250
+ break;
251
+ case 'NotificationLinkedProfilesUpdates':
252
+ for (const update of updates) {
253
+ const lid = update?.jid;
254
+ const addedProfiles = Array.isArray(update?.added_profiles) ? update.added_profiles : [];
255
+ const mappings = [];
256
+ for (const profile of addedProfiles) {
257
+ const pn = typeof profile === 'string' ? profile : (profile?.pn ?? profile?.jid ?? null);
258
+ if (lid && pn) {
259
+ const mapping = { lid, pn };
260
+ ev.emit('lid-mapping.update', mapping);
261
+ mappings.push(mapping);
262
+ }
263
+ }
264
+ await signalRepository.lidMapping.storeLIDPNMappings(mappings);
265
+ }
266
+ break;
267
+ default:
268
+ logger.info({ operation, data }, 'unhandled mex newsletter notification');
269
+ break;
270
+ }
271
+ };
272
+ // Handles newsletter notifications
273
+ const handleNewsletterNotification = async (node) => {
274
+ const from = node.attrs.from;
275
+ const children = getAllBinaryNodeChildren(node);
276
+ const author = node.attrs.participant;
277
+ for (const child of children) {
278
+ logger.debug({ from, child }, 'got newsletter notification');
279
+ switch (child.tag) {
280
+ case 'reaction': {
281
+ const reactionUpdate = {
282
+ id: from,
283
+ server_id: child.attrs.message_id,
284
+ reaction: {
285
+ code: getBinaryNodeChildString(child, 'reaction'),
286
+ count: 1
287
+ }
288
+ };
289
+ ev.emit('newsletter.reaction', reactionUpdate);
290
+ break;
291
+ }
292
+ case 'view': {
293
+ const viewUpdate = {
294
+ id: from,
295
+ server_id: child.attrs.message_id,
296
+ count: parseInt(child.content?.toString() || '0', 10)
297
+ };
298
+ ev.emit('newsletter.view', viewUpdate);
299
+ break;
300
+ }
301
+ case 'participant': {
302
+ const participantUpdate = {
303
+ id: from,
304
+ author,
305
+ user: child.attrs.jid,
306
+ action: child.attrs.action,
307
+ new_role: child.attrs.role
308
+ };
309
+ ev.emit('newsletter-participants.update', participantUpdate);
310
+ break;
311
+ }
312
+ case 'update': {
313
+ const settingsNode = getBinaryNodeChild(child, 'settings');
314
+ if (settingsNode) {
315
+ const update = {};
316
+ const nameNode = getBinaryNodeChild(settingsNode, 'name');
317
+ if (nameNode?.content)
318
+ update.name = nameNode.content.toString();
319
+ const descriptionNode = getBinaryNodeChild(settingsNode, 'description');
320
+ if (descriptionNode?.content)
321
+ update.description = descriptionNode.content.toString();
322
+ ev.emit('newsletter-settings.update', {
323
+ id: from,
324
+ update
325
+ });
326
+ }
327
+ break;
328
+ }
329
+ case 'message': {
330
+ const plaintextNode = getBinaryNodeChild(child, 'plaintext');
331
+ if (plaintextNode?.content) {
332
+ try {
333
+ const contentBuf = typeof plaintextNode.content === 'string'
334
+ ? Buffer.from(plaintextNode.content, 'binary')
335
+ : Buffer.from(plaintextNode.content);
336
+ const messageProto = proto.Message.decode(contentBuf).toJSON();
337
+ const fullMessage = proto.WebMessageInfo.fromObject({
338
+ key: {
339
+ remoteJid: from,
340
+ id: child.attrs.message_id || child.attrs.server_id,
341
+ fromMe: false // TODO: is this really true though
342
+ },
343
+ message: messageProto,
344
+ messageTimestamp: +child.attrs.t
345
+ }).toJSON();
346
+ await upsertMessage(fullMessage, 'append');
347
+ logger.debug('Processed plaintext newsletter message');
348
+ }
349
+ catch (error) {
350
+ logger.error({ error }, 'Failed to decode plaintext newsletter message');
351
+ }
352
+ }
353
+ break;
354
+ }
355
+ default:
356
+ logger.warn({ node, child }, 'Unknown newsletter notification child');
357
+ break;
358
+ }
359
+ }
360
+ };
361
+ const sendMessageAck = async (node, errorCode) => {
362
+ const stanza = buildAckStanza(node, errorCode, authState.creds.me.id);
363
+ logger.debug({ recv: { tag: node.tag, attrs: node.attrs }, sent: stanza.attrs }, 'sent ack');
364
+ await sendNode(stanza);
365
+ };
254
366
  const rejectCall = async (callId, callFrom) => {
255
- const stanza = ({
367
+ const stanza = {
256
368
  tag: 'call',
257
369
  attrs: {
258
370
  from: authState.creds.me.id,
259
- to: callFrom,
371
+ to: callFrom
260
372
  },
261
- content: [{
262
- tag: 'reject',
263
- attrs: {
264
- 'call-id': callId,
265
- 'call-creator': callFrom,
266
- count: '0',
267
- },
268
- content: undefined,
269
- }],
270
- })
271
-
272
- await query(stanza)
273
- }
274
-
373
+ content: [
374
+ {
375
+ tag: 'reject',
376
+ attrs: {
377
+ 'call-id': callId,
378
+ 'call-creator': callFrom,
379
+ count: '0'
380
+ },
381
+ content: undefined
382
+ }
383
+ ]
384
+ };
385
+ await query(stanza);
386
+ };
275
387
  const sendRetryRequest = async (node, forceIncludeKeys = false) => {
276
- const {
277
- fullMessage
278
- } = Utils_1.decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '')
279
- const {
280
- key: msgKey
281
- } = fullMessage
282
- const msgId = msgKey.id
283
-
388
+ const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '');
389
+ const { key: msgKey } = fullMessage;
390
+ const msgId = msgKey.id;
284
391
  if (messageRetryManager) {
285
392
  // Check if we've exceeded max retries using the new system
286
393
  if (messageRetryManager.hasExceededMaxRetries(msgId)) {
287
- logger.debug({
288
- msgId
289
- }, 'reached retry limit with new retry manager, clearing')
290
- messageRetryManager.markRetryFailed(msgId)
291
- return
394
+ logger.debug({ msgId }, 'reached retry limit with new retry manager, clearing');
395
+ messageRetryManager.markRetryFailed(msgId);
396
+ return;
292
397
  }
293
-
294
398
  // Increment retry count using new system
295
- const retryCount = messageRetryManager.incrementRetryCount(msgId)
296
-
399
+ const retryCount = messageRetryManager.incrementRetryCount(msgId);
297
400
  // Use the new retry count for the rest of the logic
298
- const key = `${msgId}:${msgKey?.participant}`
299
- msgRetryCache.set(key, retryCount)
300
- } else {
401
+ const key = `${msgId}:${msgKey?.participant}`;
402
+ await msgRetryCache.set(key, retryCount);
403
+ }
404
+ else {
301
405
  // Fallback to old system
302
- const key = `${msgId}:${msgKey?.participant}`
303
- let retryCount = (await msgRetryCache.get(key)) || 0
304
-
406
+ const key = `${msgId}:${msgKey?.participant}`;
407
+ let retryCount = (await msgRetryCache.get(key)) || 0;
305
408
  if (retryCount >= maxMsgRetryCount) {
306
- logger.debug({
307
- retryCount,
308
- msgId
309
- }, 'reached retry limit, clearing')
310
- msgRetryCache.del(key)
311
- return
409
+ logger.debug({ retryCount, msgId }, 'reached retry limit, clearing');
410
+ await msgRetryCache.del(key);
411
+ return;
312
412
  }
313
-
314
- retryCount += 1
315
- await msgRetryCache.set(key, retryCount)
316
- }
317
-
318
- const key = `${msgId}:${msgKey?.participant}`
319
- const retryCount = (await msgRetryCache.get(key)) || 1
320
- const {
321
- account,
322
- signedPreKey,
323
- signedIdentityKey: identityKey
324
- } = authState.creds
325
- const fromJid = node.attrs.from
326
-
413
+ retryCount += 1;
414
+ await msgRetryCache.set(key, retryCount);
415
+ }
416
+ const key = `${msgId}:${msgKey?.participant}`;
417
+ const retryCount = (await msgRetryCache.get(key)) || 1;
418
+ const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds;
419
+ const fromJid = node.attrs.from;
327
420
  // Check if we should recreate the session
328
- let shouldRecreateSession = false
329
- let recreateReason = ''
330
-
331
- if (enableAutoSessionRecreation && messageRetryManager) {
421
+ let shouldRecreateSession = false;
422
+ let recreateReason = '';
423
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
332
424
  try {
333
425
  // Check if we have a session with this JID
334
- const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
335
- const hasSession = await signalRepository.validateSession(fromJid)
336
- const result = messageRetryManager.shouldRecreateSession(fromJid, retryCount, hasSession.exists)
337
-
338
- shouldRecreateSession = result.recreate
339
- recreateReason = result.reason
340
-
426
+ const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid);
427
+ const hasSession = await signalRepository.validateSession(fromJid);
428
+ const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists);
429
+ shouldRecreateSession = result.recreate;
430
+ recreateReason = result.reason;
341
431
  if (shouldRecreateSession) {
342
- logger.info({
343
- fromJid,
344
- retryCount,
345
- reason: recreateReason
346
- }, 'recreating session for retry')
432
+ logger.debug({ fromJid, retryCount, reason: recreateReason }, 'recreating session for retry');
347
433
  // Delete existing session to force recreation
348
- await authState.keys.set({
349
- session: {
350
- [sessionId]: null
351
- }
352
- })
353
- forceIncludeKeys = true
434
+ await authState.keys.set({ session: { [sessionId]: null } });
435
+ forceIncludeKeys = true;
354
436
  }
355
- } catch (error) {
356
- logger.warn({
357
- error,
358
- fromJid
359
- }, 'failed to check session recreation')
437
+ }
438
+ catch (error) {
439
+ logger.warn({ error, fromJid }, 'failed to check session recreation');
360
440
  }
361
441
  }
362
-
363
442
  if (retryCount <= 2) {
364
443
  // Use new retry manager for phone requests if available
365
444
  if (messageRetryManager) {
366
445
  // Schedule phone request with delay (like whatsmeow)
367
446
  messageRetryManager.schedulePhoneRequest(msgId, async () => {
368
447
  try {
369
- const msgId = await requestPlaceholderResend(msgKey)
370
- logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId} (scheduled)`)
371
- } catch (error) {
372
- logger.warn({
373
- error,
374
- msgId
375
- }, 'failed to send scheduled phone request');
448
+ const requestId = await requestPlaceholderResend(msgKey);
449
+ logger.debug(`sendRetryRequest: requested placeholder resend (${requestId}) for message ${msgId} (scheduled)`);
376
450
  }
377
- })
378
- } else {
451
+ catch (error) {
452
+ logger.warn({ error, msgId }, 'failed to send scheduled phone request');
453
+ }
454
+ });
455
+ }
456
+ else {
379
457
  // Fallback to immediate request
380
- const msgId = await requestPlaceholderResend(msgKey)
381
- logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`)
458
+ const msgId = await requestPlaceholderResend(msgKey);
459
+ logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`);
382
460
  }
383
461
  }
384
-
385
- const deviceIdentity = Utils_1.encodeSignedDeviceIdentity(account, true)
462
+ const deviceIdentity = encodeSignedDeviceIdentity(account, true);
386
463
  await authState.keys.transaction(async () => {
387
464
  const receipt = {
388
465
  tag: 'receipt',
@@ -391,1311 +468,1031 @@ const makeMessagesRecvSocket = (config) => {
391
468
  type: 'retry',
392
469
  to: node.attrs.from
393
470
  },
394
- content: [{
471
+ content: [
472
+ {
395
473
  tag: 'retry',
396
474
  attrs: {
397
475
  count: retryCount.toString(),
398
476
  id: node.attrs.id,
399
477
  t: node.attrs.t,
400
- v: '1'
478
+ v: '1',
479
+ // ADD ERROR FIELD
480
+ error: '0'
401
481
  }
402
482
  },
403
483
  {
404
484
  tag: 'registration',
405
485
  attrs: {},
406
- content: Utils_1.encodeBigEndian(authState.creds.registrationId)
486
+ content: encodeBigEndian(authState.creds.registrationId)
407
487
  }
408
488
  ]
409
- }
410
-
489
+ };
411
490
  if (node.attrs.recipient) {
412
- receipt.attrs.recipient = node.attrs.recipient
491
+ receipt.attrs.recipient = node.attrs.recipient;
413
492
  }
414
-
415
493
  if (node.attrs.participant) {
416
- receipt.attrs.participant = node.attrs.participant
494
+ receipt.attrs.participant = node.attrs.participant;
417
495
  }
418
-
419
496
  if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
420
- const {
421
- update,
422
- preKeys
423
- } = await Utils_1.getNextPreKeys(authState, 1)
424
- const [keyId] = Object.keys(preKeys)
425
- const key = preKeys[+keyId]
426
- const content = receipt.content
427
-
497
+ const { update, preKeys } = await getNextPreKeys(authState, 1);
498
+ const [keyId] = Object.keys(preKeys);
499
+ const key = preKeys[+keyId];
500
+ const content = receipt.content;
428
501
  content.push({
429
502
  tag: 'keys',
430
503
  attrs: {},
431
- content: [{
432
- tag: 'type',
433
- attrs: {},
434
- content: Buffer.from(Defaults_1.KEY_BUNDLE_TYPE)
435
- },
436
- {
437
- tag: 'identity',
438
- attrs: {},
439
- content: identityKey.public
440
- },
441
- Utils_1.xmppPreKey(key, +keyId),
442
- Utils_1.xmppSignedPreKey(signedPreKey),
443
- {
444
- tag: 'device-identity',
445
- attrs: {},
446
- content: deviceIdentity
447
- }
504
+ content: [
505
+ { tag: 'type', attrs: {}, content: Buffer.from(KEY_BUNDLE_TYPE) },
506
+ { tag: 'identity', attrs: {}, content: identityKey.public },
507
+ xmppPreKey(key, +keyId),
508
+ xmppSignedPreKey(signedPreKey),
509
+ { tag: 'device-identity', attrs: {}, content: deviceIdentity }
448
510
  ]
449
511
  });
450
- ev.emit('creds.update', update)
512
+ ev.emit('creds.update', update);
451
513
  }
452
- await sendNode(receipt)
453
- logger.info({
454
- msgAttrs: node.attrs,
455
- retryCount
456
- }, 'sent retry receipt')
457
- }, authState?.creds?.me?.id || 'sendRetryRequest')
458
- }
459
-
514
+ await sendNode(receipt);
515
+ logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt');
516
+ }, authState?.creds?.me?.id || 'sendRetryRequest');
517
+ };
518
+ // Mirrors WAWeb/Handle/PreKeyLow.js: skip a re-issued notification with the same stanza id.
519
+ const inFlightPreKeyLow = new Set();
520
+ /**
521
+ * Fire-and-forget tctoken re-issuance after a peer's device identity changed.
522
+ * Mirrors WAWebSendTcTokenWhenDeviceIdentityChange — runs in parallel with
523
+ * the session refresh (not after it).
524
+ */
525
+ const reissueTcTokenAfterIdentityChange = (from) => {
526
+ void (async () => {
527
+ const normalizedJid = jidNormalizedUser(from);
528
+ const tcJid = await resolveTcTokenJid(normalizedJid, getLIDForPN);
529
+ const tcTokenData = await authState.keys.get('tctoken', [tcJid]);
530
+ const senderTs = tcTokenData?.[tcJid]?.senderTimestamp;
531
+ if (senderTs === null || senderTs === undefined || isTcTokenExpired(senderTs)) {
532
+ return;
533
+ }
534
+ logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken');
535
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
536
+ const issueJid = await resolveIssuanceJid(normalizedJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID);
537
+ const result = await issuePrivacyTokens([issueJid], senderTs);
538
+ await storeTcTokensFromIqResult({
539
+ result,
540
+ fallbackJid: tcJid,
541
+ keys: authState.keys,
542
+ getLIDForPN,
543
+ onNewJidStored: trackTcTokenJid
544
+ });
545
+ })().catch(err => {
546
+ logger.debug({ jid: from, err: err?.message }, 'failed to re-issue tctoken after identity change');
547
+ });
548
+ };
460
549
  const handleEncryptNotification = async (node) => {
461
- const from = node.attrs.from
462
- if (from === WABinary_1.S_WHATSAPP_NET) {
463
- const countChild = WABinary_1.getBinaryNodeChild(node, 'count')
464
- const count = +countChild.attrs.value
465
- const shouldUploadMorePreKeys = count < Defaults_1.MIN_PREKEY_COUNT
466
- logger.debug({
467
- count,
468
- shouldUploadMorePreKeys
469
- }, 'recv pre-key count')
470
-
550
+ const from = node.attrs.from;
551
+ if (from === S_WHATSAPP_NET) {
552
+ const stanzaId = node.attrs.id;
553
+ if (stanzaId && inFlightPreKeyLow.has(stanzaId)) {
554
+ return;
555
+ }
556
+ const countChild = getBinaryNodeChild(node, 'count');
557
+ const count = +countChild.attrs.value;
558
+ const shouldUploadMorePreKeys = count < MIN_PREKEY_COUNT;
559
+ logger.debug({ count, shouldUploadMorePreKeys }, 'recv pre-key count');
471
560
  if (shouldUploadMorePreKeys) {
472
- await uploadPreKeys()
561
+ if (stanzaId)
562
+ inFlightPreKeyLow.add(stanzaId);
563
+ try {
564
+ await uploadPreKeys();
565
+ }
566
+ finally {
567
+ if (stanzaId)
568
+ inFlightPreKeyLow.delete(stanzaId);
569
+ }
473
570
  }
474
- } else {
475
- const identityNode = WABinary_1.getBinaryNodeChild(node, 'identity')
476
- if (identityNode) {
477
- logger.info({
478
- jid: from
479
- }, 'identity changed')
480
- // not handling right now
481
- // signal will override new identity anyway
482
- } else {
483
- logger.info({
484
- node
485
- }, 'unknown encrypt notification')
571
+ }
572
+ else {
573
+ const result = await handleIdentityChange(node, {
574
+ meId: authState.creds.me?.id,
575
+ meLid: authState.creds.me?.lid,
576
+ validateSession: signalRepository.validateSession,
577
+ assertSessions,
578
+ debounceCache: identityAssertDebounce,
579
+ logger,
580
+ onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange
581
+ });
582
+ if (result.action === 'no_identity_node') {
583
+ logger.info({ node }, 'unknown encrypt notification');
486
584
  }
487
585
  }
488
- }
489
-
490
- const handleGroupNotification = (participant, child, msg, mode) => {
491
- let participantJid = mode === 'lid' ? WABinary_1.getBinaryNodeChild(child, 'participant')?.attrs?.phone_number : WABinary_1.getBinaryNodeChild(child, 'participant')?.attrs?.jid || participant
492
-
493
- // TODO: Add participant LID
494
- switch (child.tag) {
586
+ };
587
+ const handleGroupNotification = (fullNode, child, msg) => {
588
+ // TODO: Support PN/LID (Here is only LID now)
589
+ const actingParticipantLid = fullNode.attrs.participant;
590
+ const actingParticipantPn = fullNode.attrs.participant_pn;
591
+ const actingParticipantUsername = fullNode.attrs.participant_username;
592
+ const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid;
593
+ const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn;
594
+ switch (child?.tag) {
495
595
  case 'create':
496
- const metadata = groups_1.extractGroupMetadata(child)
497
- msg.messageStubType = Types_1.WAMessageStubType.GROUP_CREATE
498
- msg.messageStubParameters = [metadata.subject]
499
- msg.key = {
500
- participant: metadata.owner
501
- }
502
- ev.emit('chats.upsert', [{
503
- id: metadata.id,
504
- name: metadata.subject,
505
- conversationTimestamp: metadata.creation,
506
- }])
507
- ev.emit('groups.upsert', [{
508
- ...metadata,
509
- author: participant
510
- }])
511
- break
512
- case 'delete':
513
- msg.messageStubType = Types_1.WAMessageStubType.COMMUNITY_PARENT_GROUP_DELETED
514
- msg.messageStubParameters = [participantJid, 'delete']
515
- break
596
+ const metadata = extractGroupMetadata(child);
597
+ msg.messageStubType = WAMessageStubType.GROUP_CREATE;
598
+ msg.messageStubParameters = [metadata.subject];
599
+ msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn };
600
+ ev.emit('chats.upsert', [
601
+ {
602
+ id: metadata.id,
603
+ name: metadata.subject,
604
+ conversationTimestamp: metadata.creation
605
+ }
606
+ ]);
607
+ ev.emit('groups.upsert', [
608
+ {
609
+ ...metadata,
610
+ author: actingParticipantLid,
611
+ authorPn: actingParticipantPn,
612
+ authorUsername: actingParticipantUsername
613
+ }
614
+ ]);
615
+ break;
516
616
  case 'ephemeral':
517
617
  case 'not_ephemeral':
518
618
  msg.message = {
519
619
  protocolMessage: {
520
- type: WAProto_1.proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
620
+ type: proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
521
621
  ephemeralExpiration: +(child.attrs.expiration || 0)
522
622
  }
523
- }
524
- break
623
+ };
624
+ break;
525
625
  case 'modify':
526
- const oldNumber = mode === 'lid' ? WABinary_1.getBinaryNodeChildren(child, 'participant').map(p => p.attrs.phone_number) : WABinary_1.getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid)
527
- msg.messageStubParameters = oldNumber || []
528
- msg.messageStubType = Types_1.WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER
529
- break
626
+ const oldNumber = getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid);
627
+ msg.messageStubParameters = oldNumber || [];
628
+ msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER;
629
+ break;
530
630
  case 'promote':
531
631
  case 'demote':
532
632
  case 'remove':
533
633
  case 'add':
534
634
  case 'leave':
535
- let stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`
536
- if (child.attrs?.reason === 'linked_group_join') {
537
- stubType = GROUP_PARTICIPANT_LINKED_GROUP_JOIN
538
- }
539
- msg.messageStubType = Types_1.WAMessageStubType[stubType]
540
- const participants = mode === 'lid' ? WABinary_1.getBinaryNodeChildren(child, 'participant').map(p => p.attrs.phone_number) : WABinary_1.getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid)
635
+ const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`;
636
+ msg.messageStubType = WAMessageStubType[stubType];
637
+ const participants = getBinaryNodeChildren(child, 'participant').map(({ attrs }) => {
638
+ // TODO: Store LID MAPPINGS
639
+ return {
640
+ id: attrs.jid,
641
+ phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
642
+ lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
643
+ username: attrs.participant_username || attrs.username || undefined,
644
+ admin: (attrs.type || null)
645
+ };
646
+ });
541
647
  if (participants.length === 1 &&
542
648
  // if recv. "remove" message and sender removed themselves
543
649
  // mark as left
544
- WABinary_1.areJidsSameUser(participants[0], participant) &&
650
+ (areJidsSameUser(participants[0].id, actingParticipantLid) ||
651
+ areJidsSameUser(participants[0].id, actingParticipantPn)) &&
545
652
  child.tag === 'remove') {
546
- msg.messageStubType = Types_1.WAMessageStubType.GROUP_PARTICIPANT_LEAVE
653
+ msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE;
547
654
  }
548
- msg.messageStubParameters = participants
549
- break
655
+ msg.messageStubParameters = participants.map(a => JSON.stringify(a));
656
+ break;
550
657
  case 'subject':
551
- msg.messageStubType = Types_1.WAMessageStubType.GROUP_CHANGE_SUBJECT
552
- msg.messageStubParameters = [participantJid, child.attrs.subject]
553
- break
658
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_SUBJECT;
659
+ msg.messageStubParameters = [child.attrs.subject];
660
+ break;
554
661
  case 'description':
555
- const description = WABinary_1.getBinaryNodeChild(child, 'body')?.content?.toString()
556
- msg.messageStubType = Types_1.WAMessageStubType.GROUP_CHANGE_DESCRIPTION
557
- msg.messageStubParameters = description ? [description] : undefined
558
- break
662
+ const description = getBinaryNodeChild(child, 'body')?.content?.toString();
663
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_DESCRIPTION;
664
+ msg.messageStubParameters = description ? [description] : undefined;
665
+ break;
559
666
  case 'announcement':
560
667
  case 'not_announcement':
561
- msg.messageStubType = Types_1.WAMessageStubType.GROUP_CHANGE_ANNOUNCE
562
- msg.messageStubParameters = [(child.tag === 'announcement') ? 'on' : 'off']
563
- break
668
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_ANNOUNCE;
669
+ msg.messageStubParameters = [child.tag === 'announcement' ? 'on' : 'off'];
670
+ break;
564
671
  case 'locked':
565
672
  case 'unlocked':
566
- msg.messageStubType = Types_1.WAMessageStubType.GROUP_CHANGE_RESTRICT
567
- msg.messageStubParameters = [(child.tag === 'locked') ? 'on' : 'off']
568
- break
673
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_RESTRICT;
674
+ msg.messageStubParameters = [child.tag === 'locked' ? 'on' : 'off'];
675
+ break;
569
676
  case 'invite':
570
- msg.messageStubType = Types_1.WAMessageStubType.GROUP_CHANGE_INVITE_LINK
571
- msg.messageStubParameters = [child.attrs.code]
572
- break
677
+ msg.messageStubType = WAMessageStubType.GROUP_CHANGE_INVITE_LINK;
678
+ msg.messageStubParameters = [child.attrs.code];
679
+ break;
573
680
  case 'member_add_mode':
574
- const addMode = child.content
681
+ const addMode = child.content;
575
682
  if (addMode) {
576
- msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBER_ADD_MODE
577
- msg.messageStubParameters = [addMode.toString()]
683
+ msg.messageStubType = WAMessageStubType.GROUP_MEMBER_ADD_MODE;
684
+ msg.messageStubParameters = [addMode.toString()];
578
685
  }
579
- break
686
+ break;
580
687
  case 'membership_approval_mode':
581
- const approvalMode = WABinary_1.getBinaryNodeChild(child, 'group_join')
688
+ const approvalMode = getBinaryNodeChild(child, 'group_join');
582
689
  if (approvalMode) {
583
- msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE
584
- msg.messageStubParameters = [approvalMode.attrs.state]
690
+ msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE;
691
+ msg.messageStubParameters = [approvalMode.attrs.state];
585
692
  }
586
- break
693
+ break;
587
694
  case 'created_membership_requests':
588
- participantJid = mode === 'lid' ? WABinary_1.getBinaryNodeChild(child, 'requested_user')?.attrs?.phone_number : WABinary_1.getBinaryNodeChild(child, 'requested_user')?.attrs?.jid || participant
589
-
590
- msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
591
- msg.messageStubParameters = [participantJid, 'created', child.attrs.request_method]
592
- break
695
+ msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD;
696
+ msg.messageStubParameters = [
697
+ JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
698
+ 'created',
699
+ child.attrs.request_method
700
+ ];
701
+ break;
593
702
  case 'revoked_membership_requests':
594
- participantJid = mode === 'lid' ? WABinary_1.getBinaryNodeChild(child, 'requested_user')?.attrs?.phone_number : WABinary_1.getBinaryNodeChild(child, 'requested_user')?.attrs?.jid || participant
595
-
596
- const isDenied = WABinary_1.areJidsSameUser(participantJid, participant)
597
- msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
598
- msg.messageStubParameters = [participantJid, isDenied ? 'revoked' : 'rejected']
599
- break
600
- case 'link':
601
- case 'unlink':
602
- const type = child.attrs?.unlink_type || child.attrs?.link_type
603
- const stubMap = {
604
- parent_group: Types_1.WAMessageStubType[`COMMUNITY_${child.tag.toUpperCase()}_PARENT_GROUP`],
605
- sibling_group: Types_1.WAMessageStubType[`COMMUNITY_${child.tag.toUpperCase()}_SIBLING_GROUP`],
606
- sub_group: Types_1.WAMessageStubType[`COMMUNITY_${child.tag.toUpperCase()}_SUB_GROUP`]
607
- }
608
- const groups = WABinary_1.getBinaryNodeChildren(child, 'group')
609
- .map(g => g.attrs?.jid || g.attrs?.subject || '')
610
- .filter(x => x)
611
- msg.messageStubType = stubMap?.[type] || Types_1.WAMessageStubType[`COMMUNITY_${child.tag.toUpperCase()}_PARENT_GROUP`]
612
- msg.messageStubParameters = [participantJid, child.tag, groups]
613
- break
614
- case 'linked_group_promote':
615
- case 'linked_group_demote':
616
- const stubtype = `COMMUNITY_PARTICIPANT_${child.tag.split('_')[2].toUpperCase()}`
617
- const participantS = mode === 'lid' ? WABinary_1.getBinaryNodeChildren(child, 'participant').map(p => p.attrs.phone_number) : WABinary_1.getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid)
618
- msg.messageStubType = Types_1.WAMessageStubType[stubtype]
619
- msg.messageStubParameters = participantS
620
- break
621
- case 'created_sub_group_suggestion':
622
- msg.messageStubType = Types_1.WAMessageStubType.SUGGESTED_SUBGROUP_ANNOUNCE
623
- msg.messageStubParameters = [participantJid, 'add']
624
- break
625
- case 'revoked_sub_group_suggestions':
626
- const res = WABinary_1.getBinaryNodeChildren(child, 'sub_group_suggestions')
627
- const reason = res.attrs?.reason
628
- if (reason === 'approved') msg.messageStubType = Types_1.WAMessageStubType.GROUP_CREATE
629
- else msg.messageStubType = Types_1.WAMessageStubType.GENERIC_NOTIFICATION
630
- msg.messageStubParameters = [participantJid, reason]
631
- break
632
- default:
633
- logger.warn(child.tag, 'Unhandled group node')
634
- break
703
+ const isDenied = areJidsSameUser(affectedParticipantLid, actingParticipantLid);
704
+ // TODO: LIDMAPPING SUPPORT
705
+ msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD;
706
+ msg.messageStubParameters = [
707
+ JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
708
+ isDenied ? 'revoked' : 'rejected'
709
+ ];
710
+ break;
635
711
  }
636
- }
637
-
638
- const handleNewsletterNotification = (id, node) => {
639
- const messages = WABinary_1.getBinaryNodeChild(node, 'messages')
640
- const message = WABinary_1.getBinaryNodeChild(node, 'message')
641
- const serverId = node.attrs.server_id
642
-
643
- const reactionsList = WABinary_1.getBinaryNodeChild(node, 'reactions')
644
- const viewsList = WABinary_1.getBinaryNodeChild(node, 'views_count')
645
-
646
- if (reactionsList) {
647
- const reactions = WABinary_1.getBinaryNodeChild(reactionsList, 'reaction')
648
-
649
- if (reactions.length === 0) {
650
- ev.emit('newsletter.reaction', {
651
- id,
652
- newsletter_server_id: serverId,
653
- reaction: {
654
- removed: true
655
- }
656
- })
657
- }
658
-
659
- reactions.forEach(item => {
660
- ev.emit('newsletter.reaction', {
661
- id,
662
- newsletter_server_id: serverId,
663
- reaction: {
664
- code: item.attrs?.code,
665
- count: +item.attrs.count
666
- }
667
- })
668
- })
669
- }
670
-
671
- if (viewsList.length) {
672
- viewsList.forEach(item => {
673
- ev.emit('newsletter.view', {
674
- id,
675
- newsletter_server_id: serverId,
676
- count: +item.attrs.count
677
- })
678
- })
712
+ };
713
+ const handleDevicesNotification = async (node) => {
714
+ const [child] = getAllBinaryNodeChildren(node);
715
+ const from = jidNormalizedUser(node.attrs.from);
716
+ if (!child) {
717
+ logger.debug({ from }, 'devices notification missing child, skipping');
718
+ return;
679
719
  }
680
- }
681
-
682
- const handleMexNotification = (id, node) => {
683
- const operation = node?.attrs?.op_name
684
- const content = JSON.parse(node?.content)
685
-
686
- let contentPath
687
- let action
688
-
689
- if (operation === Types_1.MexOperations.UPDATE) {
690
- contentPath = content.data[Types_1.XWAPaths.METADATA_UPDATE]
691
-
692
- ev.emit('newsletter-settings.update', {
693
- id,
694
- update: contentPath.thread_metadata.settings
695
- })
696
- } else if (operation === Types_1.MexUpdatesOperations.GROUP_LIMIT_SHARING) {
697
- contentPath = content.data[Types_1.XWAPathsMexUpdates.GROUP_SHARING_CHANGE]
698
-
699
- ev.emit('limit-sharing.update', {
700
- id,
701
- author: contentPath.updated_by?.pn ? contentPath.updated_by.pn : contentPath.updated_by.id,
702
- action: `${contentPath.properties.limit_sharing.limit_sharing_enabled ? 'on' : 'off'}`,
703
- trigger: contentPath.properties.limit_sharing.limit_sharing_trigger,
704
- update_time: contentPath.update_time
705
- })
706
- } else if (operation === Types_1.MexUpdatesOperations.OWNER_COMMUNITY) {
707
- contentPath = content.data[Types_1.XWAPathsMexUpdates.COMMUNITY_OWNER_CHANGE]
708
-
709
- ev.emit('community-owner.update', {
710
- id,
711
- author: contentPath.updated_by?.pn ? contentPath.updated_by.pn : contentPath.updated_by.id,
712
- user: contentPath.role_updates[0].user?.pn ? contentPath.role_updates[0].user.pn : contentPath.role_updates[0].user.jid,
713
- new_role: contentPath.role_updates[0].new_role,
714
- update_time: contentPath.update_time
715
- })
716
- } else {
717
-
718
- if (operation === Types_1.MexOperations.PROMOTE) {
719
- action = 'promote'
720
- contentPath = content.data[Types_1.XWAPaths.PROMOTE]
721
- } else {
722
- action = 'demote'
723
- contentPath = content.data[Types_1.XWAPaths.DEMOTE]
720
+ const tag = child.tag;
721
+ const deviceHash = child.attrs.device_hash;
722
+ const devices = getBinaryNodeChildren(child, 'device');
723
+ if (areJidsSameUser(from, authState.creds.me.id) || areJidsSameUser(from, authState.creds.me.lid)) {
724
+ const deviceJids = devices.map(d => d.attrs.jid);
725
+ logger.info({ deviceJids }, 'got my own devices');
726
+ }
727
+ if (!devices.length) {
728
+ logger.debug({ from, tag }, 'no devices in notification, skipping');
729
+ return;
730
+ }
731
+ const decoded = [];
732
+ for (const d of devices) {
733
+ const jid = d.attrs.jid;
734
+ if (!jid)
735
+ continue;
736
+ const parts = jidDecode(jid);
737
+ if (!parts) {
738
+ logger.debug({ jid }, 'failed to decode device jid, skipping');
739
+ continue;
724
740
  }
725
-
726
- ev.emit('newsletter-participants.update', {
727
- id,
728
- author: contentPath.actor.pn,
729
- user: contentPath.user.pn,
730
- new_role: contentPath.user_new_role,
731
- action
732
- })
741
+ decoded.push({ jid, user: parts.user, server: parts.server, device: parts.device });
733
742
  }
734
- }
735
-
743
+ if (!decoded.length)
744
+ return;
745
+ await devicesMutex.mutex(async () => {
746
+ const byUser = new Map();
747
+ for (const d of decoded) {
748
+ const list = byUser.get(d.user) || [];
749
+ list.push(d);
750
+ byUser.set(d.user, list);
751
+ }
752
+ for (const [user, entries] of byUser) {
753
+ if (tag === 'update') {
754
+ logger.debug({ user }, `${user}'s device list updated, dropping cached devices`);
755
+ await userDevicesCache?.del(user);
756
+ continue;
757
+ }
758
+ if (tag === 'remove') {
759
+ await signalRepository.deleteSession(entries.map(e => e.jid));
760
+ }
761
+ const existingCache = (await userDevicesCache?.get(user)) || [];
762
+ if (!existingCache.length) {
763
+ // No baseline yet; skip applying the delta so getUSyncDevices can
764
+ // later fetch the full device list. Caching just the notification
765
+ // entries would make a partial list look authoritative.
766
+ logger.debug({ user, tag }, 'device list not cached, deferring to USync refresh');
767
+ continue;
768
+ }
769
+ const affected = new Set(entries.map(e => e.device));
770
+ let updatedDevices;
771
+ switch (tag) {
772
+ case 'add':
773
+ logger.info({ deviceHash, count: entries.length }, 'devices added');
774
+ updatedDevices = [
775
+ ...existingCache.filter(d => !affected.has(d.device)),
776
+ ...entries.map(e => ({ user: e.user, server: e.server, device: e.device }))
777
+ ];
778
+ break;
779
+ case 'remove':
780
+ logger.info({ deviceHash, count: entries.length }, 'devices removed');
781
+ updatedDevices = existingCache.filter(d => !affected.has(d.device));
782
+ break;
783
+ default:
784
+ logger.debug({ tag }, 'Unknown device list change tag');
785
+ continue;
786
+ }
787
+ if (updatedDevices.length === 0) {
788
+ await userDevicesCache?.del(user);
789
+ }
790
+ else {
791
+ await userDevicesCache?.set(user, updatedDevices);
792
+ }
793
+ }
794
+ });
795
+ };
736
796
  const processNotification = async (node) => {
737
- const result = {}
738
- const [child] = WABinary_1.getAllBinaryNodeChildren(node)
739
- const nodeType = node.attrs.type
740
- const from = WABinary_1.jidNormalizedUser(node.attrs.from)
741
-
797
+ const result = {};
798
+ const [child] = getAllBinaryNodeChildren(node);
799
+ const nodeType = node.attrs.type;
800
+ const from = jidNormalizedUser(node.attrs.from);
742
801
  switch (nodeType) {
743
- case 'privacy_token':
744
- const tokenList = WABinary_1.getBinaryNodeChildren(child, 'token')
745
- for (const {
746
- attrs,
747
- content
748
- }
749
- of tokenList) {
750
- const jid = attrs.jid
751
- ev.emit('chats.update', [{
752
- id: jid,
753
- tcToken: content
754
- }])
755
- logger.debug({
756
- jid
757
- }, 'got privacy token update')
758
- }
759
- break
760
- case 'w:gp2':
761
- const mode = node.attrs.addressing_mode
762
- handleGroupNotification(mode === 'lid' ? node.attrs.participant_pn : node.attrs.participant, child, result, mode)
763
- break
764
802
  case 'newsletter':
765
- handleNewsletterNotification(node.attrs.from, child)
766
- break
803
+ await handleNewsletterNotification(node);
804
+ break;
767
805
  case 'mex':
768
- handleMexNotification(node.attrs.from, child)
769
- break
806
+ await handleMexNotification(node);
807
+ break;
808
+ case 'w:gp2':
809
+ // TODO: HANDLE PARTICIPANT_PN
810
+ handleGroupNotification(node, child, result);
811
+ break;
770
812
  case 'mediaretry':
771
- const event = Utils_1.decodeMediaRetryNode(node)
772
- ev.emit('messages.media-update', [event])
773
- break
813
+ const event = decodeMediaRetryNode(node);
814
+ ev.emit('messages.media-update', [event]);
815
+ break;
774
816
  case 'encrypt':
775
- await handleEncryptNotification(node)
776
- break
817
+ await handleEncryptNotification(node);
818
+ break;
777
819
  case 'devices':
778
- const devices = WABinary_1.getBinaryNodeChildren(child, 'device')
779
- if (WABinary_1.areJidsSameUser(child.attrs.jid, authState.creds.me.id) ||
780
- WABinary_1.areJidsSameUser(child.attrs.lid, authState.creds.me.lid)) {
781
- const deviceData = devices.map(d => ({
782
- id: d.attrs.jid,
783
- lid: d.attrs.lid
784
- }))
785
- logger.info({
786
- deviceData
787
- }, 'my own devices changed')
820
+ try {
821
+ await handleDevicesNotification(node);
822
+ }
823
+ catch (error) {
824
+ logger.error({ error, node }, 'failed to handle devices notification');
788
825
  }
789
- //TODO: drop a new event, add hashes
790
- break
826
+ break;
791
827
  case 'server_sync':
792
- const update = WABinary_1.getBinaryNodeChild(node, 'collection')
828
+ const update = getBinaryNodeChild(node, 'collection');
793
829
  if (update) {
794
- const name = update.attrs.name
795
- await resyncAppState([name], false)
830
+ const name = update.attrs.name;
831
+ await resyncAppState([name], false);
796
832
  }
797
- break
833
+ break;
798
834
  case 'picture':
799
- const setPicture = WABinary_1.getBinaryNodeChild(node, 'set')
800
- const delPicture = WABinary_1.getBinaryNodeChild(node, 'delete')
801
- ev.emit('contacts.update', [{
802
- id: WABinary_1.jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
803
- imgUrl: setPicture ? 'changed' : 'removed'
804
- }])
805
- if (WABinary_1.isJidGroup(from)) {
806
- const node = setPicture || delPicture
807
- result.messageStubType = Types_1.WAMessageStubType.GROUP_CHANGE_ICON
835
+ const setPicture = getBinaryNodeChild(node, 'set');
836
+ const delPicture = getBinaryNodeChild(node, 'delete');
837
+ // TODO: WAJIDHASH stuff proper support inhouse
838
+ ev.emit('contacts.update', [
839
+ {
840
+ id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
841
+ imgUrl: setPicture ? 'changed' : 'removed'
842
+ }
843
+ ]);
844
+ if (isJidGroup(from)) {
845
+ const node = setPicture || delPicture;
846
+ result.messageStubType = WAMessageStubType.GROUP_CHANGE_ICON;
808
847
  if (setPicture) {
809
- result.messageStubParameters = [setPicture.attrs.id]
848
+ result.messageStubParameters = [setPicture.attrs.id];
810
849
  }
811
- result.participant = node?.attrs?.author
850
+ result.participant = node?.attrs.author;
812
851
  result.key = {
813
- ...result.key || {},
814
- participant: setPicture?.attrs?.author
815
- }
852
+ ...(result.key || {}),
853
+ participant: setPicture?.attrs.author
854
+ };
816
855
  }
817
- break
856
+ break;
818
857
  case 'account_sync':
819
858
  if (child.tag === 'disappearing_mode') {
820
- const newDuration = +child.attrs.duration
821
- const timestamp = +child.attrs.t
822
- logger.info({
823
- newDuration
824
- }, 'updated account disappearing mode')
859
+ const newDuration = +child.attrs.duration;
860
+ const timestamp = +child.attrs.t;
861
+ logger.info({ newDuration }, 'updated account disappearing mode');
825
862
  ev.emit('creds.update', {
826
863
  accountSettings: {
827
864
  ...authState.creds.accountSettings,
828
865
  defaultDisappearingMode: {
829
866
  ephemeralExpiration: newDuration,
830
- ephemeralSettingTimestamp: timestamp,
831
- },
832
- }
833
- })
834
- } else if (child.tag === 'blocklist') {
835
- const blocklists = WABinary_1.getBinaryNodeChildren(child, 'item')
836
- for (const {
837
- attrs
867
+ ephemeralSettingTimestamp: timestamp
868
+ }
838
869
  }
839
- of blocklists) {
840
- const blocklist = [attrs.jid]
841
- const type = (attrs.action === 'block') ? 'add' : 'remove'
842
- ev.emit('blocklist.update', {
843
- blocklist,
844
- type
845
- })
870
+ });
871
+ }
872
+ else if (child.tag === 'blocklist') {
873
+ const blocklists = getBinaryNodeChildren(child, 'item');
874
+ for (const { attrs } of blocklists) {
875
+ const blocklist = [attrs.jid];
876
+ const type = attrs.action === 'block' ? 'add' : 'remove';
877
+ ev.emit('blocklist.update', { blocklist, type });
846
878
  }
847
879
  }
848
- break
880
+ break;
849
881
  case 'link_code_companion_reg':
850
- const linkCodeCompanionReg = WABinary_1.getBinaryNodeChild(node, 'link_code_companion_reg')
851
- const ref = toRequiredBuffer(WABinary_1.getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_ref'))
852
- const primaryIdentityPublicKey = toRequiredBuffer(WABinary_1.getBinaryNodeChildBuffer(linkCodeCompanionReg, 'primary_identity_pub'))
853
- const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(WABinary_1.getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_wrapped_primary_ephemeral_pub'))
854
- const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped)
855
- const companionSharedKey = Utils_1.Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey)
856
- const random = crypto_1.randomBytes(32)
857
- const linkCodeSalt = crypto_1.randomBytes(32)
858
-
859
- const linkCodePairingExpanded = await Utils_1.hkdf(companionSharedKey, 32, {
882
+ const linkCodeCompanionReg = getBinaryNodeChild(node, 'link_code_companion_reg');
883
+ const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_ref'));
884
+ const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'primary_identity_pub'));
885
+ const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_wrapped_primary_ephemeral_pub'));
886
+ const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped);
887
+ const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey);
888
+ const random = randomBytes(32);
889
+ const linkCodeSalt = randomBytes(32);
890
+ const linkCodePairingExpanded = hkdf(companionSharedKey, 32, {
860
891
  salt: linkCodeSalt,
861
892
  info: 'link_code_pairing_key_bundle_encryption_key'
862
- })
863
-
864
- const encryptPayload = Buffer.concat([Buffer.from(authState.creds.signedIdentityKey.public), primaryIdentityPublicKey, random])
865
- const encryptIv = crypto_1.randomBytes(12)
866
- const encrypted = Utils_1.aesEncryptGCM(encryptPayload, linkCodePairingExpanded, encryptIv, Buffer.alloc(0))
867
- const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted])
868
- const identitySharedKey = Utils_1.Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey)
869
- const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random])
870
-
871
- authState.creds.advSecretKey = (await Utils_1.hkdf(identityPayload, 32, {
872
- info: 'adv_secret'
873
- })).toString('base64')
874
-
893
+ });
894
+ const encryptPayload = Buffer.concat([
895
+ Buffer.from(authState.creds.signedIdentityKey.public),
896
+ primaryIdentityPublicKey,
897
+ random
898
+ ]);
899
+ const encryptIv = randomBytes(12);
900
+ const encrypted = aesEncryptGCM(encryptPayload, linkCodePairingExpanded, encryptIv, Buffer.alloc(0));
901
+ const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted]);
902
+ const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey);
903
+ const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random]);
904
+ authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64');
875
905
  await query({
876
906
  tag: 'iq',
877
907
  attrs: {
878
- to: WABinary_1.S_WHATSAPP_NET,
908
+ to: S_WHATSAPP_NET,
879
909
  type: 'set',
880
910
  id: sock.generateMessageTag(),
881
911
  xmlns: 'md'
882
912
  },
883
- content: [{
884
- tag: 'link_code_companion_reg',
885
- attrs: {
886
- jid: authState.creds.me.id,
887
- stage: 'companion_finish',
888
- },
889
- content: [{
890
- tag: 'link_code_pairing_wrapped_key_bundle',
891
- attrs: {},
892
- content: encryptedPayload
893
- },
894
- {
895
- tag: 'companion_identity_public',
896
- attrs: {},
897
- content: authState.creds.signedIdentityKey.public
913
+ content: [
914
+ {
915
+ tag: 'link_code_companion_reg',
916
+ attrs: {
917
+ jid: authState.creds.me.id,
918
+ stage: 'companion_finish'
898
919
  },
899
- {
900
- tag: 'link_code_pairing_ref',
901
- attrs: {},
902
- content: ref
903
- }
904
- ]
905
- }]
906
- })
907
-
908
- authState.creds.registered = true
909
- ev.emit('creds.update', authState.creds)
920
+ content: [
921
+ {
922
+ tag: 'link_code_pairing_wrapped_key_bundle',
923
+ attrs: {},
924
+ content: encryptedPayload
925
+ },
926
+ {
927
+ tag: 'companion_identity_public',
928
+ attrs: {},
929
+ content: authState.creds.signedIdentityKey.public
930
+ },
931
+ {
932
+ tag: 'link_code_pairing_ref',
933
+ attrs: {},
934
+ content: ref
935
+ }
936
+ ]
937
+ }
938
+ ]
939
+ });
940
+ authState.creds.registered = true;
941
+ ev.emit('creds.update', authState.creds);
942
+ break;
943
+ case 'privacy_token':
944
+ await handlePrivacyTokenNotification(node);
945
+ break;
910
946
  }
911
-
912
947
  if (Object.keys(result).length) {
913
- return result
948
+ return result;
949
+ }
950
+ };
951
+ /**
952
+ * In-memory cache of storage JIDs with stored tctokens, seeded from the persisted index.
953
+ * Used to coalesce writes during a session; pruning always re-reads the persisted index
954
+ * to cover writes made by other layers (e.g. history sync).
955
+ */
956
+ const tcTokenKnownJids = new Set();
957
+ const tcTokenIndexLoaded = (async () => {
958
+ try {
959
+ const jids = await readTcTokenIndex(authState.keys);
960
+ for (const jid of jids)
961
+ tcTokenKnownJids.add(jid);
962
+ logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index');
963
+ }
964
+ catch (err) {
965
+ logger.warn({ err: err?.message }, 'failed to load tctoken index');
966
+ }
967
+ })();
968
+ let tcTokenIndexTimer;
969
+ async function flushTcTokenIndex() {
970
+ if (tcTokenIndexTimer) {
971
+ clearTimeout(tcTokenIndexTimer);
972
+ tcTokenIndexTimer = undefined;
914
973
  }
974
+ // Merge with whatever is already persisted so we don't clobber writes from other
975
+ // paths (history sync, concurrent sessions on the same store).
976
+ const write = await buildMergedTcTokenIndexWrite(authState.keys, tcTokenKnownJids);
977
+ return authState.keys.set({ tctoken: write });
915
978
  }
916
-
917
- async function decipherLinkPublicKey(data) {
918
- const buffer = toRequiredBuffer(data)
919
- const salt = buffer.slice(0, 32)
920
- const secretKey = await Utils_1.derivePairingCodeKey(authState.creds.pairingCode, salt)
921
- const iv = buffer.slice(32, 48)
922
- const payload = buffer.slice(48, 80)
923
- return Utils_1.aesDecryptCTR(payload, secretKey, iv)
979
+ function scheduleTcTokenIndexSave() {
980
+ if (tcTokenIndexTimer) {
981
+ clearTimeout(tcTokenIndexTimer);
982
+ }
983
+ tcTokenIndexTimer = setTimeout(() => {
984
+ tcTokenIndexTimer = undefined;
985
+ flushTcTokenIndex().catch(err => {
986
+ logger.warn({ err: err?.message }, 'failed to save tctoken index');
987
+ });
988
+ }, 5000);
924
989
  }
925
-
926
- function getTypeMessage(message) {
927
- const type = Object.keys(message)
928
- const restype = (!['senderKeyDistributionMessage', 'messageContextInfo'].includes(type[0]) && type[0]) || (type.length >= 3 && type[1] !== 'messageContextInfo' && type[1]) || type[type.length - 1] || Object.keys(message)[0]
929
- return restype
990
+ function trackTcTokenJid(jid) {
991
+ if (jid && jid !== TC_TOKEN_INDEX_KEY && !tcTokenKnownJids.has(jid)) {
992
+ tcTokenKnownJids.add(jid);
993
+ scheduleTcTokenIndexSave();
994
+ }
995
+ }
996
+ const handlePrivacyTokenNotification = async (node) => {
997
+ const tokensNode = getBinaryNodeChild(node, 'tokens');
998
+ if (!tokensNode)
999
+ return;
1000
+ const from = jidNormalizedUser(node.attrs.from);
1001
+ // WA Web uses: senderLid ?? toLid(from) for the storage key
1002
+ // The sender_lid attribute provides the LID directly when available
1003
+ const senderLid = node.attrs.sender_lid && isLidUser(jidNormalizedUser(node.attrs.sender_lid))
1004
+ ? jidNormalizedUser(node.attrs.sender_lid)
1005
+ : undefined;
1006
+ const fallbackJid = senderLid ?? (await resolveTcTokenJid(from, getLIDForPN));
1007
+ logger.debug({ from, storageJid: fallbackJid }, 'processing privacy token notification');
1008
+ await storeTcTokensFromIqResult({
1009
+ result: node,
1010
+ fallbackJid,
1011
+ keys: authState.keys,
1012
+ getLIDForPN,
1013
+ onNewJidStored: trackTcTokenJid
1014
+ });
1015
+ };
1016
+ async function decipherLinkPublicKey(data) {
1017
+ const buffer = toRequiredBuffer(data);
1018
+ const salt = buffer.slice(0, 32);
1019
+ const secretKey = await derivePairingCodeKey(authState.creds.pairingCode, salt);
1020
+ const iv = buffer.slice(32, 48);
1021
+ const payload = buffer.slice(48, 80);
1022
+ return aesDecryptCTR(payload, secretKey, iv);
930
1023
  }
931
-
932
1024
  function toRequiredBuffer(data) {
933
1025
  if (data === undefined) {
934
- throw new boom_1.Boom('Invalid buffer', {
935
- statusCode: 400
936
- })
1026
+ throw new Boom('Invalid buffer', { statusCode: 400 });
937
1027
  }
938
- return data instanceof Buffer ? data : Buffer.from(data)
1028
+ return data instanceof Buffer ? data : Buffer.from(data);
939
1029
  }
940
-
941
1030
  const willSendMessageAgain = async (id, participant) => {
942
- const key = `${id}:${participant}`
943
- const retryCount = (await msgRetryCache.get(key)) || 0
944
- return retryCount < maxMsgRetryCount
945
- }
946
-
1031
+ const key = `${id}:${participant}`;
1032
+ const retryCount = (await msgRetryCache.get(key)) || 0;
1033
+ return retryCount < maxMsgRetryCount;
1034
+ };
947
1035
  const updateSendMessageAgainCount = async (id, participant) => {
948
- const key = `${id}:${participant}`
949
- const newValue = ((await msgRetryCache.get(key)) || 0) + 1
950
- await msgRetryCache.set(key, newValue)
951
- }
952
-
953
- const sendMessagesAgain = async (key, ids, retryNode) => {
954
- const remoteJid = key.remoteJid
955
- const participant = key.participant || remoteJid
956
- const retryCount = +retryNode.attrs.count || 1
957
-
1036
+ const key = `${id}:${participant}`;
1037
+ const newValue = ((await msgRetryCache.get(key)) || 0) + 1;
1038
+ await msgRetryCache.set(key, newValue);
1039
+ };
1040
+ const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
1041
+ const remoteJid = key.remoteJid;
1042
+ const participant = key.participant || remoteJid;
1043
+ const retryCount = +retryNode.attrs.count || 1;
1044
+ const msgId = ids[0];
958
1045
  // Try to get messages from cache first, then fallback to getMessage
959
- const msgs = []
960
-
1046
+ const msgs = [];
961
1047
  for (const id of ids) {
962
- let msg
963
-
1048
+ let msg;
964
1049
  // Try to get from retry cache first if enabled
965
1050
  if (messageRetryManager) {
966
- const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id)
1051
+ const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id);
967
1052
  if (cachedMsg) {
968
- msg = cachedMsg.message
969
- logger.debug({
970
- jid: remoteJid,
971
- id
972
- }, 'found message in retry cache')
973
-
1053
+ msg = cachedMsg.message;
1054
+ logger.debug({ jid: remoteJid, id }, 'found message in retry cache');
974
1055
  // Mark retry as successful since we found the message
975
- messageRetryManager.markRetrySuccess(id)
1056
+ messageRetryManager.markRetrySuccess(id);
976
1057
  }
977
1058
  }
978
-
979
1059
  // Fallback to getMessage if not found in cache
980
1060
  if (!msg) {
981
- msg = await getMessage({
982
- ...key,
983
- id
984
- })
1061
+ msg = await getMessage({ ...key, id });
985
1062
  if (msg) {
986
- logger.debug({
987
- jid: remoteJid,
988
- id
989
- }, 'found message via getMessage')
990
-
1063
+ logger.debug({ jid: remoteJid, id }, 'found message via getMessage');
991
1064
  // Also mark as successful if found via getMessage
992
1065
  if (messageRetryManager) {
993
1066
  messageRetryManager.markRetrySuccess(id);
994
1067
  }
995
1068
  }
996
1069
  }
997
- msgs.push(msg)
1070
+ msgs.push(msg);
998
1071
  }
999
-
1000
1072
  // if it's the primary jid sending the request
1001
1073
  // just re-send the message to everyone
1002
1074
  // prevents the first message decryption failure
1003
- const sendToAll = !WABinary_1.jidDecode(participant)?.device
1004
-
1005
- // Check if we should recreate session for this retry
1006
- let shouldRecreateSession = false
1007
- let recreateReason = ''
1008
-
1009
- if (enableAutoSessionRecreation && messageRetryManager) {
1075
+ const sendToAll = !jidDecode(participant)?.device;
1076
+ const sessionId = signalRepository.jidToSignalProtocolAddress(participant);
1077
+ let injectedFromBundle = false;
1078
+ const bundle = extractE2ESessionFromRetryReceipt(receiptNode);
1079
+ if (bundle) {
1010
1080
  try {
1011
- const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
1012
- const hasSession = await signalRepository.validateSession(participant)
1013
- const result = messageRetryManager.shouldRecreateSession(participant, retryCount, hasSession.exists)
1014
-
1015
- shouldRecreateSession = result.recreate
1016
- recreateReason = result.reason
1017
-
1018
- if (shouldRecreateSession) {
1019
- logger.info({
1020
- participant,
1021
- retryCount,
1022
- reason: recreateReason
1023
- }, 'recreating session for outgoing retry')
1024
- await authState.keys.set({
1025
- session: {
1026
- [sessionId]: null
1027
- }
1028
- });
1081
+ await signalRepository.injectE2ESession({ jid: participant, session: bundle });
1082
+ injectedFromBundle = true;
1083
+ logger.debug({ participant, retryCount }, 'injected session from retry receipt key bundle');
1084
+ }
1085
+ catch (error) {
1086
+ logger.warn({ error, participant }, 'failed to inject session from retry receipt');
1087
+ }
1088
+ }
1089
+ if (!injectedFromBundle) {
1090
+ const receivedRegId = getBinaryNodeChildUInt(receiptNode, 'registration', 4);
1091
+ if (typeof receivedRegId === 'number' && Number.isInteger(receivedRegId)) {
1092
+ const info = await signalRepository.getSessionInfo(participant);
1093
+ if (info && info.registrationId !== 0 && info.registrationId !== receivedRegId) {
1094
+ logger.info({ participant, stored: info.registrationId, received: receivedRegId }, 'reg id mismatch on retry without bundle, deleting session');
1095
+ await authState.keys.set({ session: { [sessionId]: null } });
1029
1096
  }
1030
- } catch (error) {
1031
- logger.warn({
1032
- error,
1033
- participant
1034
- }, 'failed to check session recreation for outgoing retry')
1035
1097
  }
1036
1098
  }
1037
-
1038
- await assertSessions([participant], shouldRecreateSession)
1039
-
1040
- if (WABinary_1.isJidGroup(remoteJid)) {
1041
- await authState.keys.set({
1042
- 'sender-key-memory': {
1043
- [remoteJid]: null
1099
+ const BASE_KEY_CHECK_RETRY = 2;
1100
+ if (msgId && messageRetryManager) {
1101
+ const info = await signalRepository.getSessionInfo(participant);
1102
+ if (info) {
1103
+ if (retryCount === BASE_KEY_CHECK_RETRY) {
1104
+ messageRetryManager.saveBaseKey(sessionId, msgId, info.baseKey);
1044
1105
  }
1045
- });
1106
+ else if (retryCount > BASE_KEY_CHECK_RETRY) {
1107
+ if (messageRetryManager.hasSameBaseKey(sessionId, msgId, info.baseKey)) {
1108
+ logger.warn({ participant, retryCount }, 'base key collision on retry, forcing fresh session');
1109
+ await authState.keys.set({ session: { [sessionId]: null } });
1110
+ }
1111
+ messageRetryManager.deleteBaseKey(sessionId, msgId);
1112
+ }
1113
+ }
1046
1114
  }
1047
- logger.debug({
1048
- participant,
1049
- sendToAll,
1050
- shouldRecreateSession,
1051
- recreateReason
1052
- }, 'forced new session for retry recp')
1053
-
1115
+ let shouldRecreateSession = false;
1116
+ let recreateReason = '';
1117
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
1118
+ try {
1119
+ const hasSession = await signalRepository.validateSession(participant);
1120
+ const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists);
1121
+ shouldRecreateSession = result.recreate;
1122
+ recreateReason = result.reason;
1123
+ if (shouldRecreateSession) {
1124
+ logger.debug({ participant, retryCount, reason: recreateReason }, 'recreating session for outgoing retry');
1125
+ await authState.keys.set({ session: { [sessionId]: null } });
1126
+ }
1127
+ }
1128
+ catch (error) {
1129
+ logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry');
1130
+ }
1131
+ }
1132
+ if (!injectedFromBundle) {
1133
+ await assertSessions([participant], true);
1134
+ }
1135
+ if (isJidGroup(remoteJid)) {
1136
+ await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } });
1137
+ }
1138
+ logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason, injectedFromBundle }, 'prepared session for retry resend');
1054
1139
  for (const [i, msg] of msgs.entries()) {
1055
1140
  if (!ids[i])
1056
- continue
1057
-
1141
+ continue;
1058
1142
  if (msg && (await willSendMessageAgain(ids[i], participant))) {
1059
- updateSendMessageAgainCount(ids[i], participant)
1060
- const msgRelayOpts = {
1061
- messageId: ids[i]
1062
- }
1063
-
1143
+ await updateSendMessageAgainCount(ids[i], participant);
1144
+ const msgRelayOpts = { messageId: ids[i] };
1064
1145
  if (sendToAll) {
1065
- msgRelayOpts.useUserDevicesCache = false
1066
- } else {
1146
+ msgRelayOpts.useUserDevicesCache = false;
1147
+ }
1148
+ else {
1067
1149
  msgRelayOpts.participant = {
1068
1150
  jid: participant,
1069
1151
  count: +retryNode.attrs.count
1070
- }
1152
+ };
1071
1153
  }
1072
- await relayMessage(key.remoteJid, msg, msgRelayOpts)
1073
- } else {
1074
- logger.debug({
1075
- jid: key.remoteJid,
1076
- id: ids[i]
1077
- }, 'recv retry request, but message not available')
1154
+ await relayMessage(key.remoteJid, msg, msgRelayOpts);
1155
+ }
1156
+ else {
1157
+ logger.debug({ jid: key.remoteJid, id: ids[i] }, 'recv retry request, but message not available');
1078
1158
  }
1079
1159
  }
1080
- }
1081
-
1160
+ };
1082
1161
  const handleReceipt = async (node) => {
1083
- const {
1084
- attrs,
1085
- content
1086
- } = node
1087
- const isLid = attrs.from.includes('lid')
1088
- const isNodeFromMe = WABinary_1.areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id)
1089
- const remoteJid = !isNodeFromMe || WABinary_1.isJidGroup(attrs.from) ? attrs.from : attrs.recipient
1090
- const fromMe = !attrs.recipient || ((attrs.type === 'retry' || attrs.type === 'sender') && isNodeFromMe)
1091
-
1162
+ const { attrs, content } = node;
1163
+ const isLid = attrs.from.includes('lid');
1164
+ const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id);
1165
+ const remoteJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient;
1166
+ const fromMe = !attrs.recipient || ((attrs.type === 'retry' || attrs.type === 'sender') && isNodeFromMe);
1092
1167
  const key = {
1093
1168
  remoteJid,
1094
1169
  id: '',
1095
1170
  fromMe,
1096
1171
  participant: attrs.participant
1097
- }
1098
-
1099
- if (shouldIgnoreJid(remoteJid) && remoteJid !== '@s.whatsapp.net') {
1100
- logger.debug({
1101
- remoteJid
1102
- }, 'ignoring receipt from jid')
1103
- await sendMessageAck(node)
1104
- return
1105
- }
1106
-
1107
- const ids = [attrs.id]
1172
+ };
1173
+ const ids = [attrs.id];
1108
1174
  if (Array.isArray(content)) {
1109
- const items = WABinary_1.getBinaryNodeChildren(content[0], 'item')
1110
- ids.push(...items.map(i => i.attrs.id))
1175
+ const items = getBinaryNodeChildren(content[0], 'item');
1176
+ ids.push(...items.map(i => i.attrs.id));
1111
1177
  }
1112
-
1113
1178
  try {
1114
1179
  await Promise.all([
1115
1180
  receiptMutex.mutex(async () => {
1116
- const status = Utils_1.getStatusFromReceiptType(attrs.type)
1117
-
1118
- if (typeof status !== 'undefined' && (
1119
- // basically, we only want to know when a message from us has been delivered to/read by the other person
1120
- // or another device of ours has read some messages
1121
- status >= WAProto_1.proto.WebMessageInfo.Status.SERVER_ACK ||
1122
- !isNodeFromMe)) {
1123
- if (WABinary_1.isJidGroup(remoteJid) || WABinary_1.isJidStatusBroadcast(remoteJid)) {
1181
+ const status = getStatusFromReceiptType(attrs.type);
1182
+ if (typeof status !== 'undefined' &&
1183
+ // basically, we only want to know when a message from us has been delivered to/read by the other person
1184
+ // or another device of ours has read some messages
1185
+ (status >= proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)) {
1186
+ if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
1124
1187
  if (attrs.participant) {
1125
- const updateKey = status === WAProto_1.proto.WebMessageInfo.Status.DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp'
1188
+ const updateKey = status === proto.WebMessageInfo.Status.DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp';
1126
1189
  ev.emit('message-receipt.update', ids.map(id => ({
1127
- key: {
1128
- ...key,
1129
- id
1130
- },
1190
+ key: { ...key, id },
1131
1191
  receipt: {
1132
- userJid: WABinary_1.jidNormalizedUser(attrs.participant),
1192
+ userJid: jidNormalizedUser(attrs.participant),
1133
1193
  [updateKey]: +attrs.t
1134
1194
  }
1135
- })))
1195
+ })));
1136
1196
  }
1137
- } else {
1197
+ }
1198
+ else {
1138
1199
  ev.emit('messages.update', ids.map(id => ({
1139
- key: {
1140
- ...key,
1141
- id
1142
- },
1143
- update: {
1144
- status
1145
- }
1146
- })))
1200
+ key: { ...key, id },
1201
+ update: { status, messageTimestamp: toNumber(+(attrs.t ?? 0)) }
1202
+ })));
1147
1203
  }
1148
1204
  }
1149
-
1150
1205
  if (attrs.type === 'retry') {
1151
1206
  // correctly set who is asking for the retry
1152
- key.participant = key.participant || attrs.from
1153
- const retryNode = WABinary_1.getBinaryNodeChild(node, 'retry')
1154
-
1207
+ key.participant = key.participant || attrs.from;
1208
+ const retryNode = getBinaryNodeChild(node, 'retry');
1155
1209
  if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
1156
1210
  if (key.fromMe) {
1157
1211
  try {
1158
- updateSendMessageAgainCount(ids[0], key.participant)
1159
- logger.debug({
1160
- attrs,
1161
- key
1162
- }, 'recv retry request')
1163
- await sendMessagesAgain(key, ids, retryNode)
1164
- } catch (error) {
1165
- logger.error({
1166
- key,
1167
- ids,
1168
- trace: error instanceof Error ? error.stack : 'Unknown error'
1169
- }, 'error in sending message again')
1212
+ await updateSendMessageAgainCount(ids[0], key.participant);
1213
+ logger.debug({ attrs, key }, 'recv retry request');
1214
+ await sendMessagesAgain(key, ids, retryNode, node);
1215
+ }
1216
+ catch (error) {
1217
+ logger.error({ key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' }, 'error in sending message again');
1170
1218
  }
1171
- } else {
1172
- logger.info({
1173
- attrs,
1174
- key
1175
- }, 'recv retry for not fromMe message')
1176
1219
  }
1177
- } else {
1178
- logger.info({
1179
- attrs,
1180
- key
1181
- }, 'will not send message again, as sent too many times')
1220
+ else {
1221
+ logger.info({ attrs, key }, 'recv retry for not fromMe message');
1222
+ }
1223
+ }
1224
+ else {
1225
+ logger.info({ attrs, key }, 'will not send message again, as sent too many times');
1182
1226
  }
1183
1227
  }
1184
1228
  })
1185
- ])
1186
- } finally {
1187
- await sendMessageAck(node)
1229
+ ]);
1188
1230
  }
1189
- }
1190
-
1231
+ finally {
1232
+ await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack receipt'));
1233
+ }
1234
+ };
1191
1235
  const handleNotification = async (node) => {
1192
- const remoteJid = node.attrs.from
1193
-
1194
- if (shouldIgnoreJid(remoteJid) && remoteJid !== '@s.whatsapp.net') {
1195
- logger.debug({
1196
- remoteJid,
1197
- id: node.attrs.id
1198
- }, 'ignored notification')
1199
- await sendMessageAck(node)
1200
- return
1201
- }
1202
-
1236
+ const remoteJid = node.attrs.from;
1203
1237
  try {
1204
1238
  await Promise.all([
1205
1239
  notificationMutex.mutex(async () => {
1206
- const msg = await processNotification(node)
1207
-
1240
+ const msg = await processNotification(node);
1208
1241
  if (msg) {
1209
- const fromMe = WABinary_1.areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id)
1242
+ const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id);
1243
+ const { senderAlt: participantAlt, addressingMode } = extractAddressingContext(node);
1210
1244
  msg.key = {
1211
1245
  remoteJid,
1212
1246
  fromMe,
1213
1247
  participant: node.attrs.participant,
1248
+ participantAlt,
1249
+ participantUsername: node.attrs.participant_username,
1250
+ addressingMode,
1214
1251
  id: node.attrs.id,
1215
1252
  ...(msg.key || {})
1216
- }
1217
- msg.participant = msg.participant ? msg.participant : node.attrs.participant
1218
- msg.messageTimestamp = +node.attrs.t
1219
- const fullMsg = WAProto_1.proto.WebMessageInfo.fromObject(msg)
1220
- await upsertMessage(fullMsg, 'append')
1253
+ };
1254
+ msg.participant ?? (msg.participant = node.attrs.participant);
1255
+ msg.messageTimestamp = +node.attrs.t;
1256
+ const fullMsg = proto.WebMessageInfo.fromObject(msg);
1257
+ await upsertMessage(fullMsg, 'append');
1221
1258
  }
1222
1259
  })
1223
- ])
1224
- } finally {
1225
- await sendMessageAck(node)
1260
+ ]);
1226
1261
  }
1227
- }
1228
-
1262
+ finally {
1263
+ await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack notification'));
1264
+ }
1265
+ };
1229
1266
  const handleMessage = async (node) => {
1230
- if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== '@s.whatsapp.net') {
1231
- logger.debug({
1232
- key: node.attrs.key
1233
- }, 'ignored message')
1234
- await sendMessageAck(node)
1235
- return
1236
- }
1237
-
1238
- let response
1239
-
1240
- const encNode = WABinary_1.getBinaryNodeChild(node, 'enc')
1241
- if (encNode && encNode.attrs.type === 'msmsg') {
1242
- logger.debug({
1243
- key: node.attrs.key
1244
- }, 'ignored msmsg')
1245
- await sendMessageAck(node, Utils_1.NACK_REASONS.MissingMessageSecret)
1246
- return
1247
- }
1248
-
1249
- if (WABinary_1.getBinaryNodeChild(node, 'unavailable') && !encNode) {
1250
- await sendMessageAck(node)
1251
- let key
1252
- try {
1253
- key = Utils_1.decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '').fullMessage.key
1254
- } catch (error) {
1255
- logger.error({ error, node }, 'failed to decode unavailable message node, skipping placeholder resend')
1256
- return
1257
- }
1258
- const unavailableNode = WABinary_1.getBinaryNodeChild(node, 'unavailable')
1259
- const unavailableType = unavailableNode?.attrs?.type
1260
- if (unavailableType === 'bot_unavailable_fanout' ||
1261
- unavailableType === 'hosted_unavailable_fanout' ||
1262
- unavailableType === 'view_once_unavailable_fanout') {
1263
- logger.debug({ msgId: node.attrs.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type')
1264
- return
1265
- }
1266
- const messageAge = Utils_1.unixTimestampSeconds() - Utils_1.toNumber(node.attrs.t)
1267
- if (messageAge > Defaults_1.PLACEHOLDER_MAX_AGE_SECONDS) {
1268
- logger.debug({ msgId: node.attrs.id, messageAge }, 'skipping placeholder resend for message too old')
1269
- return
1270
- }
1271
- response = await requestPlaceholderResend(key);
1272
- if (response === 'RESOLVED') {
1273
- return
1274
- }
1275
- logger.debug('received unavailable message, acked and requested resend from phone');
1276
- } else {
1277
- if (placeholderResendCache.get(node.attrs.id)) {
1278
- await placeholderResendCache.del(node.attrs.id)
1279
- }
1267
+ const encNode = getBinaryNodeChild(node, 'enc');
1268
+ // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
1269
+ if (encNode?.attrs.type === 'msmsg') {
1270
+ logger.debug({ key: node.attrs.key }, 'ignored msmsg');
1271
+ await sendMessageAck(node, NACK_REASONS.MissingMessageSecret);
1272
+ return;
1280
1273
  }
1281
-
1282
- let decodeResult
1274
+ let acked = false;
1283
1275
  try {
1284
- // decryptMessageNode bisa throw sebelum sempat masuk try/catch utama di bawah
1285
- // (contoh: bug upstream baileys "No participant in group message" pada node
1286
- // yang malformed/gak lengkap). Kalau gak ditangkep di sini, exception ini lolos
1287
- // sebagai unhandled rejection dari handleMessage - baik pas diproses langsung
1288
- // (live message) maupun lewat offline queue.
1289
- decodeResult = Utils_1.decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger)
1290
- } catch (error) {
1291
- logger.error({ error, node }, 'failed to decrypt/decode message node, dropping')
1292
- try {
1293
- await sendMessageAck(node, Utils_1.NACK_REASONS.ParsingError)
1294
- } catch (ackError) {
1295
- logger.error({ ackError }, 'failed to ack undecodable message node')
1276
+ const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
1277
+ const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
1278
+ // store new mappings we didn't have before
1279
+ if (!!alt) {
1280
+ const altServer = jidDecode(alt)?.server;
1281
+ const primaryJid = msg.key.participant || msg.key.remoteJid;
1282
+ if (altServer === 'lid') {
1283
+ if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
1284
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]);
1285
+ await signalRepository.migrateSession(primaryJid, alt);
1286
+ }
1287
+ }
1288
+ else {
1289
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
1290
+ await signalRepository.migrateSession(alt, primaryJid);
1291
+ }
1296
1292
  }
1297
- return
1298
- }
1299
-
1300
- const {
1301
- fullMessage: msg,
1302
- category,
1303
- author,
1304
- decrypt
1305
- } = decodeResult
1306
- if (response && msg?.messageStubParameters?.[0] === Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT) {
1307
- msg.messageStubParameters = [Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT, response]
1308
- }
1309
-
1310
- if (msg.message?.protocolMessage?.type === WAProto_1.proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER && node.attrs.sender_pn) {
1311
- const lid = WABinary_1.jidNormalizedUser(node.attrs.from), pn = WABinary_1.jidNormalizedUser(node.attrs.sender_pn)
1312
- ev.emit('lid-mapping.update', {
1313
- lid,
1314
- pn
1315
- })
1316
- await signalRepository.lidMapping.storeLIDPNMappings([{
1317
- lid,
1318
- pn
1319
- }])
1320
- }
1321
-
1322
- try {
1323
- await Promise.all([
1324
- messageMutex.mutex(async () => {
1325
- await decrypt()
1326
- if (msg.key?.remoteJid && msg.key?.id && msg.message && messageRetryManager) {
1327
- messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message)
1328
- logger.debug({
1329
- jid: msg.key.remoteJid,
1330
- id: msg.key.id
1331
- }, 'Added message to recent cache for retry receipts')
1293
+ await messageMutex.mutex(async () => {
1294
+ await decrypt();
1295
+ if (msg.key?.remoteJid && msg.key?.id && msg.message && messageRetryManager) {
1296
+ messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
1297
+ }
1298
+ // message failed to decrypt
1299
+ if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
1300
+ if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
1301
+ acked = true;
1302
+ return sendMessageAck(node, NACK_REASONS.ParsingError);
1332
1303
  }
1333
- if (msg.messageStubType === WAProto_1.proto.WebMessageInfo.StubType.CIPHERTEXT) {
1334
- if (msg?.messageStubParameters?.[0] === Utils_1.MISSING_KEYS_ERROR_TEXT) {
1335
- return sendMessageAck(node, Utils_1.NACK_REASONS.ParsingError)
1304
+ if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
1305
+ // Message arrived without encryption (e.g. CTWA ads messages).
1306
+ // Check if this is eligible for placeholder resend (matching WA Web filters).
1307
+ const unavailableNode = getBinaryNodeChild(node, 'unavailable');
1308
+ const unavailableType = unavailableNode?.attrs?.type;
1309
+ if (unavailableType === 'bot_unavailable_fanout' ||
1310
+ unavailableType === 'hosted_unavailable_fanout' ||
1311
+ unavailableType === 'view_once_unavailable_fanout') {
1312
+ logger.debug({ msgId: msg.key.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type');
1313
+ acked = true;
1314
+ return sendMessageAck(node);
1336
1315
  }
1337
-
1338
- if (WABinary_1.isJidStatusBroadcast(msg.key.remoteJid)) {
1339
- const messageAge = Utils_1.unixTimestampSeconds() - Utils_1.toNumber(msg.messageTimestamp)
1340
- if (messageAge > Defaults_1.STATUS_EXPIRY_SECONDS) {
1341
- logger.debug({ msgId: msg.key.id, messageAge }, 'skipping retry for expired status message')
1342
- return sendMessageAck(node)
1343
- }
1316
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
1317
+ if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) {
1318
+ logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message');
1319
+ acked = true;
1320
+ return sendMessageAck(node);
1344
1321
  }
1345
-
1346
- const errorMessage = msg?.messageStubParameters?.[0] || ''
1347
- const isPreKeyError = errorMessage.includes('PreKey')
1348
- logger.debug(`[handleMessage] Attempting retry request for failed decryption`)
1349
-
1350
- // .catch() wajib ada: call ini sengaja tidak di-await (biar retry
1351
- // request gak ngeblok proses pesan lain), tapi kalau dibiarkan tanpa
1352
- // .catch() dan mutex-nya reject, itu jadi unhandled promise rejection
1353
- // yang bisa nge-crash seluruh proses Node (default Node behavior sejak
1354
- // v15). Ini yang paling mungkin jadi penyebab bot "tiba-tiba disconnect
1355
- // dan gak recover" setelah jalan beberapa jam.
1356
- retryMutex.mutex(async () => {
1357
- try {
1358
- if (!ws.isOpen) {
1359
- logger.debug({
1360
- node
1361
- }, 'Connection closed, skipping retry')
1362
- return
1363
- }
1364
-
1365
- if (WABinary_1.getBinaryNodeChild(node, 'unavailable')) {
1366
- logger.debug('Message unavailable, skipping retry')
1367
- return
1368
- }
1369
-
1370
- if (isPreKeyError) {
1371
- logger.info({
1372
- error: errorMessage
1373
- }, 'PreKey error detected, uploading and retrying')
1374
- try {
1375
- logger.debug('Uploading pre-keys for error recovery')
1376
- await uploadPreKeys(5)
1377
- logger.debug('Waiting for server to process new pre-keys')
1378
- await Utils_1.delay(1000)
1379
- } catch (uploadErr) {
1380
- logger.error({
1381
- uploadErr
1382
- }, 'Pre-key upload failed, proceeding with retry anyway')
1322
+ // Request the real content from the phone via placeholder resend PDO.
1323
+ // Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg),
1324
+ // and store the requestId in stubParameters[1] so users can correlate
1325
+ // with the incoming PDO response event.
1326
+ const cleanKey = {
1327
+ remoteJid: msg.key.remoteJid,
1328
+ fromMe: msg.key.fromMe,
1329
+ id: msg.key.id,
1330
+ participant: msg.key.participant
1331
+ };
1332
+ // Cache the original message metadata so the PDO response handler
1333
+ // can preserve key fields (LID details etc.) that the phone may omit
1334
+ const msgData = {
1335
+ key: msg.key,
1336
+ messageTimestamp: msg.messageTimestamp,
1337
+ pushName: msg.pushName,
1338
+ participant: msg.participant,
1339
+ verifiedBizName: msg.verifiedBizName
1340
+ };
1341
+ requestPlaceholderResend(cleanKey, msgData)
1342
+ .then(requestId => {
1343
+ if (requestId && requestId !== 'RESOLVED') {
1344
+ logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message');
1345
+ ev.emit('messages.update', [
1346
+ {
1347
+ key: msg.key,
1348
+ update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] }
1383
1349
  }
1384
- }
1385
-
1386
- const encNode = WABinary_1.getBinaryNodeChild(node, 'enc')
1387
- await sendRetryRequest(node, !encNode)
1388
- if (retryRequestDelayMs) {
1389
- await Utils_1.delay(retryRequestDelayMs)
1390
- }
1391
- } catch (err) {
1392
- logger.error({
1393
- err,
1394
- isPreKeyError
1395
- }, 'Failed to handle retry, attempting basic retry')
1396
- try {
1397
- const encNode = WABinary_1.getBinaryNodeChild(node, 'enc')
1398
- await sendRetryRequest(node, !encNode)
1399
- } catch (retryErr) {
1400
- logger.error({
1401
- retryErr
1402
- }, 'Failed to send retry after error handling')
1403
- }
1350
+ ]);
1404
1351
  }
1405
- }).catch((mutexErr) => {
1406
- logger.error({ mutexErr }, 'retryMutex task failed unexpectedly')
1407
1352
  })
1408
- } else {
1409
- let type = undefined
1410
- // FIX: Jangan fallback ke me.id saat LID tidak bisa di-resolve.
1411
- // Fallback ke me.id menyebabkan semua pesan LID private chat
1412
- // punya participant = JID bot sendiri, sehingga owner check lolos
1413
- // untuk semua user yang menggunakan LID addressing.
1414
- if (msg.key.participant && msg.key.participant.endsWith('@lid')) {
1415
- if (node.attrs.participant_pn) {
1416
- // Gunakan nomor telepon asli jika tersedia
1417
- msg.key.participant = WABinary_1.jidNormalizedUser(node.attrs.participant_pn);
1418
- } else {
1419
- // Coba resolve dari lidMapping, jika gagal pakai remoteJid
1420
- // (lebih aman daripada me.id yang menyebabkan false owner match)
1421
- try {
1422
- const resolvedPn = await signalRepository.lidMapping.getPNForLID(msg.key.participant);
1423
- msg.key.participant = resolvedPn
1424
- ? WABinary_1.jidNormalizedUser(resolvedPn)
1425
- : WABinary_1.jidNormalizedUser(msg.key.remoteJid);
1426
- } catch (_) {
1427
- msg.key.participant = WABinary_1.jidNormalizedUser(msg.key.remoteJid);
1428
- }
1353
+ .catch(err => {
1354
+ logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message');
1355
+ });
1356
+ acked = true;
1357
+ await sendMessageAck(node);
1358
+ // Don't return fall through to upsertMessage so the stub is emitted
1359
+ }
1360
+ else {
1361
+ // Skip retry for expired status messages (>24h old)
1362
+ if (isJidStatusBroadcast(msg.key.remoteJid)) {
1363
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
1364
+ if (messageAge > STATUS_EXPIRY_SECONDS) {
1365
+ logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, 'skipping retry for expired status message');
1366
+ acked = true;
1367
+ return sendMessageAck(node);
1429
1368
  }
1430
1369
  }
1431
- if (WABinary_1.isJidGroup(msg.key.remoteJid)) {
1432
- const mtype = getTypeMessage(msg.message)
1433
- const message = msg.message?.[mtype]
1434
- const contextInfo = message?.contextInfo
1435
- const participant = contextInfo?.participant
1436
- const mentionedJid = contextInfo?.mentionedJid
1437
- if (typeof participant === "string" && participant.endsWith('@lid')) {
1438
- if (msg.message?.[mtype]?.contextInfo) {
1439
- const metadata = await global.groupMetadataCache(msg.key.remoteJid)
1440
- const sender_lid = msg.message[mtype].contextInfo.participant
1441
- const found = metadata.participants.find(p => p.lid === sender_lid)
1442
- if (found) {
1443
- msg.message[mtype].contextInfo.participant = found.id
1444
- } else {
1445
- const lid = WABinary_1.jidNormalizedUser(sender_lid)
1446
- const sender_jid = await signalRepository.lidMapping.getPNForLID(sender_lid)
1447
- if (sender_jid) {
1448
- const pn = WABinary_1.jidNormalizedUser(sender_jid)
1449
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }])
1450
- msg.message[mtype].contextInfo.participant = pn
1451
- }
1452
- }
1370
+ logger.debug('[handleMessage] Attempting retry request for failed decryption');
1371
+ // WAWeb only retry-receipts here; server emits PreKeyLow if prekeys run low.
1372
+ await retryMutex.mutex(async () => {
1373
+ try {
1374
+ if (!ws.isOpen) {
1375
+ logger.debug({ node }, 'Connection closed, skipping retry');
1376
+ return;
1453
1377
  }
1454
- }
1455
- if (Array.isArray(mentionedJid) && mentionedJid.length > 0) {
1456
- if (msg.message?.[mtype]?.contextInfo) {
1457
- const metadata = await global.groupMetadataCache(msg.key.remoteJid)
1458
- const updatedMentionedJid = []
1459
- let needsUpdate = false
1460
- for (const mentionedLid of mentionedJid) {
1461
- if (typeof mentionedLid === "string" && mentionedLid.endsWith('@lid')) {
1462
- const found = metadata.participants.find(p => p.lid === mentionedLid)
1463
- if (found) {
1464
- updatedMentionedJid.push(found.id)
1465
- if (msg.message[mtype]?.text) {
1466
- msg.message[mtype].text = msg.message[mtype].text.replace(new RegExp(`@${mentionedLid.split("@")[0]}`, "g"), `@${found.id.split("@")[0]}`);
1467
- } else if (msg.message[mtype]?.caption) {
1468
- msg.message[mtype].caption = msg.message[mtype].caption.replace(new RegExp(`@${mentionedLid.split("@")[0]}`, "g"), `@${found.id.split("@")[0]}`);
1469
- }
1470
- } else {
1471
- const lid = WABinary_1.jidNormalizedUser(mentionedLid)
1472
- const mentioned_jid = await signalRepository.lidMapping.getPNForLID(mentionedLid)
1473
- if (mentioned_jid) {
1474
- const pn = WABinary_1.jidNormalizedUser(mentioned_jid)
1475
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }])
1476
- updatedMentionedJid.push(pn)
1477
- if (msg.message[mtype]?.text) {
1478
- msg.message[mtype].text = msg.message[mtype].text.replace(new RegExp(`@${mentionedLid.split("@")[0]}`, "g"), `@${pn.split("@")[0]}`);
1479
- } else if (msg.message[mtype]?.caption) {
1480
- msg.message[mtype].caption = msg.message[mtype].caption.replace(new RegExp(`@${mentionedLid.split("@")[0]}`, "g"), `@${pn.split("@")[0]}`);
1481
- }
1482
- } else {
1483
- updatedMentionedJid.push(mentionedLid)
1484
- }
1485
- }
1486
- needsUpdate = true
1487
- } else {
1488
- updatedMentionedJid.push(mentionedLid)
1489
- }
1490
- }
1491
- if (needsUpdate) {
1492
- msg.message[mtype].contextInfo.mentionedJid = updatedMentionedJid
1493
- }
1378
+ const encNode = getBinaryNodeChild(node, 'enc');
1379
+ await sendRetryRequest(node, !encNode);
1380
+ if (retryRequestDelayMs) {
1381
+ await delay(retryRequestDelayMs);
1494
1382
  }
1495
1383
  }
1496
- }
1497
- if (!WABinary_1.isJidGroup(msg.key.remoteJid) && WABinary_1.isLidUser(msg.key.remoteJid)) {
1498
- // FIX: private chat - resolve remoteJid dari LID ke JID
1499
- const resolvedRemote = node.attrs.sender_pn || node.attrs.peer_recipient_pn;
1500
- if (resolvedRemote) {
1501
- msg.key.remoteJid = WABinary_1.jidNormalizedUser(resolvedRemote);
1502
- } else {
1503
- // Fallback: coba resolve dari lidMapping
1504
- try {
1505
- const resolvedPn = await signalRepository.lidMapping.getPNForLID(msg.key.remoteJid);
1506
- if (resolvedPn) {
1507
- msg.key.remoteJid = WABinary_1.jidNormalizedUser(resolvedPn);
1508
- }
1509
- } catch (_) {}
1384
+ catch (err) {
1385
+ logger.error({ err }, 'Failed to send retry');
1510
1386
  }
1387
+ acked = true;
1388
+ await sendMessageAck(node, NACK_REASONS.UnhandledError);
1389
+ });
1390
+ }
1391
+ }
1392
+ else {
1393
+ if (messageRetryManager && msg.key.id) {
1394
+ messageRetryManager.cancelPendingPhoneRequest(msg.key.id);
1395
+ }
1396
+ const isNewsletter = isJidNewsletter(msg.key.remoteJid);
1397
+ if (!isNewsletter) {
1398
+ // no type in the receipt => message delivered
1399
+ let type = undefined;
1400
+ let participant = msg.key.participant;
1401
+ if (category === 'peer') {
1402
+ // special peer message
1403
+ type = 'peer_msg';
1511
1404
  }
1512
- // FIX: private chat - jika author masih LID, resolve ke JID
1513
- if (!WABinary_1.isJidGroup(msg.key.remoteJid) && author && WABinary_1.isLidUser(author)) {
1514
- const resolvedAuthor = node.attrs.sender_pn
1515
- || node.attrs.participant_pn
1516
- || node.attrs.peer_recipient_pn;
1517
- if (resolvedAuthor) {
1518
- author = WABinary_1.jidNormalizedUser(resolvedAuthor);
1519
- } else {
1520
- try {
1521
- const resolvedPn = await signalRepository.lidMapping.getPNForLID(author);
1522
- if (resolvedPn) author = WABinary_1.jidNormalizedUser(resolvedPn);
1523
- } catch (_) {}
1405
+ else if (msg.key.fromMe) {
1406
+ // message was sent by us from a different device
1407
+ type = 'sender';
1408
+ // need to specially handle this case
1409
+ if (isLidUser(msg.key.remoteJid) || isLidUser(msg.key.remoteJidAlt)) {
1410
+ participant = author; // TODO: investigate sending receipts to LIDs and not PNs
1524
1411
  }
1525
1412
  }
1526
- let participant = msg.key.participant
1527
- if (category === 'peer') {
1528
- type = 'peer_msg'
1529
- } else if (msg.key.fromMe) {
1530
- type = 'sender'
1531
- if (WABinary_1.isLidUser(msg.key.remoteJid) || WABinary_1.isLidUser(msg.key.participant_lid)) {
1532
- participant = author
1533
- }
1534
- } else if (!sendActiveReceipts) {
1535
- type = 'inactive'
1413
+ else if (!sendActiveReceipts) {
1414
+ type = 'inactive';
1536
1415
  }
1537
- await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type)
1538
-
1539
- const isAnyHistoryMsg = Utils_1.getHistoryMsg(msg.message)
1416
+ acked = true;
1417
+ await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type);
1418
+ // send ack for history message
1419
+ const isAnyHistoryMsg = getHistoryMsg(msg.message);
1540
1420
  if (isAnyHistoryMsg) {
1541
- const jid = WABinary_1.jidNormalizedUser(msg.key.remoteJid)
1542
- await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync')
1421
+ const jid = jidNormalizedUser(msg.key.remoteJid);
1422
+ await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync'); // TODO: investigate
1543
1423
  }
1544
1424
  }
1545
- Utils_1.cleanMessage(msg, authState.creds.me.id)
1546
- await sendMessageAck(node)
1547
- await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify')
1548
- })
1549
- ])
1550
- } catch (error) {
1551
- logger.error({
1552
- error,
1553
- node
1554
- }, 'error in handling message')
1555
- // Kalau exception terjadi sebelum sempat ack (misal error di plugin/group
1556
- // metadata sebelum baris sendMessageAck), stanza ini wajib tetap di-ack.
1557
- // Tanpa ini, server WA bisa terus retry/redeliver pesan yang sama berulang-ulang.
1558
- try {
1559
- await sendMessageAck(node, Utils_1.NACK_REASONS.UnhandledError)
1560
- } catch (ackError) {
1561
- logger.error({ ackError }, 'failed to ack message after error')
1562
- }
1563
- }
1564
- }
1565
-
1566
- const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
1567
- if (!authState.creds.me?.id) {
1568
- throw new boom_1.Boom('Not authenticated')
1569
- }
1570
-
1571
- const pdoMessage = {
1572
- historySyncOnDemandRequest: {
1573
- chatJid: oldestMsgKey.remoteJid,
1574
- oldestMsgFromMe: oldestMsgKey.fromMe,
1575
- oldestMsgId: oldestMsgKey.id,
1576
- oldestMsgTimestampMs: oldestMsgTimestamp,
1577
- onDemandMsgCount: count
1578
- },
1579
- peerDataOperationRequestType: WAProto_1.proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND
1425
+ else {
1426
+ acked = true;
1427
+ await sendMessageAck(node);
1428
+ logger.debug({ key: msg.key }, 'processed newsletter message without receipts');
1429
+ }
1430
+ }
1431
+ cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid);
1432
+ await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify');
1433
+ });
1580
1434
  }
1581
-
1582
- return sendPeerDataOperationMessage(pdoMessage)
1583
- }
1584
-
1585
- const requestPlaceholderResend = async (messageKey) => {
1586
- if (!authState.creds.me?.id) {
1587
- throw new boom_1.Boom('Not authenticated')
1588
- }
1589
-
1590
- if (placeholderResendCache.get(messageKey?.id)) {
1591
- logger.debug({
1592
- messageKey
1593
- }, 'already requested resend')
1594
- return
1595
- } else {
1596
- placeholderResendCache.set(messageKey?.id, true)
1597
- }
1598
-
1599
- await Utils_1.delay(5000)
1600
-
1601
- if (!placeholderResendCache.get(messageKey?.id)) {
1602
- logger.debug({
1603
- messageKey
1604
- }, 'message received while resend requested')
1605
- return 'RESOLVED'
1606
- }
1607
-
1608
- const pdoMessage = {
1609
- placeholderMessageResendRequest: [{
1610
- messageKey
1611
- }],
1612
- peerDataOperationRequestType: WAProto_1.proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
1613
- }
1614
-
1615
- setTimeout(() => {
1616
- if (placeholderResendCache.get(messageKey?.id)) {
1617
- logger.debug({
1618
- messageKey
1619
- }, 'PDO message without response after 15 seconds. Phone possibly offline')
1620
- placeholderResendCache.del(messageKey?.id)
1435
+ catch (error) {
1436
+ logger.error({ error, node: binaryNodeToString(node) }, 'error in handling message');
1437
+ if (!acked) {
1438
+ await sendMessageAck(node, NACK_REASONS.UnhandledError).catch(ackErr => logger.error({ ackErr }, 'failed to ack message after error'));
1621
1439
  }
1622
- }, 15000)
1623
-
1624
- return sendPeerDataOperationMessage(pdoMessage)
1625
- }
1626
-
1440
+ }
1441
+ };
1627
1442
  const handleCall = async (node) => {
1628
- let status
1629
-
1630
- const {
1631
- attrs
1632
- } = node
1633
- const [infoChild] = WABinary_1.getAllBinaryNodeChildren(node)
1634
- const callId = infoChild.attrs['call-id']
1635
- const from = infoChild.attrs.from || infoChild.attrs['call-creator']
1636
- status = Utils_1.getCallStatusFromNode(infoChild)
1637
-
1638
- if (WABinary_1.isLidUser(from) && infoChild.tag === 'relaylatency') {
1639
- const verify = await callOfferCache.get(callId)
1640
-
1641
- if (!verify) {
1642
- status = 'offer'
1643
-
1644
- const callLid = {
1645
- chatId: attrs.from,
1646
- from,
1647
- id: callId,
1648
- date: new Date(+attrs.t * 1000),
1649
- offline: !!attrs.offline,
1650
- status
1443
+ try {
1444
+ const { attrs } = node;
1445
+ const [infoChild] = getAllBinaryNodeChildren(node);
1446
+ if (!infoChild) {
1447
+ throw new Boom('Missing call info in call node');
1448
+ }
1449
+ const status = getCallStatusFromNode(infoChild);
1450
+ const callId = infoChild.attrs['call-id'];
1451
+ const from = infoChild.attrs.from || infoChild.attrs['call-creator'];
1452
+ const call = {
1453
+ chatId: attrs.from,
1454
+ from,
1455
+ callerPn: infoChild.attrs['caller_pn'],
1456
+ id: callId,
1457
+ date: new Date(+attrs.t * 1000),
1458
+ offline: !!attrs.offline,
1459
+ status
1460
+ };
1461
+ if (status === 'relaylatency') {
1462
+ const latencyValue = infoChild.attrs.latency || infoChild.attrs['latency_ms'] || infoChild.attrs['latency-ms'];
1463
+ const latencyMs = latencyValue ? Number(latencyValue) : undefined;
1464
+ if (Number.isFinite(latencyMs)) {
1465
+ call.latencyMs = latencyMs;
1651
1466
  }
1652
- await callOfferCache.set(callId, callLid)
1653
1467
  }
1468
+ if (status === 'offer') {
1469
+ call.isVideo = !!getBinaryNodeChild(infoChild, 'video');
1470
+ call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'];
1471
+ call.groupJid = infoChild.attrs['group-jid'];
1472
+ await callOfferCache.set(call.id, call);
1473
+ }
1474
+ const existingCall = await callOfferCache.get(call.id);
1475
+ // use existing call info to populate this event
1476
+ if (existingCall) {
1477
+ call.isVideo = existingCall.isVideo;
1478
+ call.isGroup = existingCall.isGroup;
1479
+ call.callerPn = call.callerPn || existingCall.callerPn;
1480
+ }
1481
+ // delete data once call has ended
1482
+ if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
1483
+ await callOfferCache.del(call.id);
1484
+ }
1485
+ ev.emit('call', [call]);
1654
1486
  }
1655
-
1656
- const call = {
1657
- chatId: attrs.from,
1658
- from,
1659
- id: callId,
1660
- date: new Date(+attrs.t * 1000),
1661
- offline: !!attrs.offline,
1662
- status,
1663
- }
1664
-
1665
- if (status === 'offer') {
1666
- call.isVideo = !!WABinary_1.getBinaryNodeChild(infoChild, 'video')
1667
- call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid']
1668
- call.groupJid = infoChild.attrs['group-jid']
1669
- await callOfferCache.set(call.id, call)
1670
- }
1671
-
1672
- const existingCall = await callOfferCache.get(call.id)
1673
-
1674
- // use existing call info to populate this event
1675
- if (existingCall) {
1676
- call.isVideo = existingCall.isVideo
1677
- call.isGroup = existingCall.isGroup
1678
- }
1679
-
1680
- // delete data once call has ended
1681
- if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
1682
- await callOfferCache.del(call.id)
1683
- }
1684
-
1685
- ev.emit('call', [call])
1686
-
1687
- await sendMessageAck(node)
1688
- }
1689
-
1690
- const handleBadAck = async ({
1691
- attrs
1692
- }) => {
1693
- const key = {
1694
- remoteJid: attrs.from,
1695
- fromMe: true,
1696
- id: attrs.id,
1697
- newsletter_server_id: attrs?.server_id
1487
+ catch (error) {
1488
+ logger.error({ error, node: binaryNodeToString(node) }, 'error in handling call');
1698
1489
  }
1490
+ finally {
1491
+ await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack call'));
1492
+ }
1493
+ };
1494
+ const handleBadAck = async ({ attrs }) => {
1495
+ const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id };
1699
1496
  // WARNING: REFRAIN FROM ENABLING THIS FOR NOW. IT WILL CAUSE A LOOP
1700
1497
  // // current hypothesis is that if pash is sent in the ack
1701
1498
  // // it means -- the message hasn't reached all devices yet
@@ -1712,130 +1509,125 @@ const makeMessagesRecvSocket = (config) => {
1712
1509
  // error in acknowledgement,
1713
1510
  // device could not display the message
1714
1511
  if (attrs.error) {
1715
- logger.warn({
1716
- attrs
1717
- }, 'received error in ack')
1718
- ev.emit('messages.update', [{
1719
- key,
1720
- update: {
1721
- status: Types_1.WAMessageStatus.ERROR,
1722
- messageStubParameters: [
1723
- attrs.error
1724
- ]
1512
+ const isReachoutTimelocked = attrs.error === String(NACK_REASONS.SenderReachoutTimelocked);
1513
+ if (attrs.error === SERVER_ERROR_CODES.MessageAccountRestriction) {
1514
+ // 463 = 1:1 message missing privacy token (tctoken). Usually means the
1515
+ // account is restricted: WhatsApp blocks starting new chats but preserves
1516
+ // existing ones, since established chats already carry a tctoken.
1517
+ // WA Web prevents this client-side (disables the compose bar).
1518
+ // No retry — retrying counts as another "reach out" and worsens the restriction.
1519
+ logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted or missing tctoken for contact');
1520
+ const ackFrom = attrs.from;
1521
+ if (ackFrom && !inFlight463Recoveries.has(ackFrom)) {
1522
+ inFlight463Recoveries.add(ackFrom);
1523
+ void (async () => {
1524
+ try {
1525
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
1526
+ const tcStorageJid = await resolveTcTokenJid(ackFrom, getLIDForPN);
1527
+ const issueJid = await resolveIssuanceJid(ackFrom, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID);
1528
+ const result = await issuePrivacyTokens([issueJid], unixTimestampSeconds());
1529
+ await storeTcTokensFromIqResult({
1530
+ result,
1531
+ fallbackJid: tcStorageJid,
1532
+ keys: authState.keys,
1533
+ getLIDForPN,
1534
+ onNewJidStored: trackTcTokenJid
1535
+ });
1536
+ logger.debug({ from: ackFrom }, 'completed 463 token recovery issuance');
1537
+ }
1538
+ catch (err) {
1539
+ logger.debug({ from: ackFrom, err: err?.message }, 'failed 463 token recovery issuance');
1540
+ }
1541
+ finally {
1542
+ inFlight463Recoveries.delete(ackFrom);
1543
+ }
1544
+ })();
1725
1545
  }
1726
- }])
1546
+ }
1547
+ else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
1548
+ logger.warn({ msgId: attrs.id, from: attrs.from }, 'smax-invalid (479): stanza rejected by server — likely stale device session or malformed addressing');
1549
+ }
1550
+ else if (isReachoutTimelocked) {
1551
+ // user is temporarily restricted, fetch current restriction details
1552
+ await fetchAccountReachoutTimelock().catch(err => logger.warn({ err }, 'failed to fetch reachout timelock'));
1553
+ logger.warn({ attrs }, 'received error in ack');
1554
+ }
1555
+ else {
1556
+ logger.warn({ attrs }, 'received error in ack');
1557
+ }
1558
+ ev.emit('messages.update', [
1559
+ {
1560
+ key,
1561
+ update: {
1562
+ status: WAMessageStatus.ERROR,
1563
+ messageStubParameters: isReachoutTimelocked ? [attrs.error, ACCOUNT_RESTRICTED_TEXT] : [attrs.error]
1564
+ }
1565
+ }
1566
+ ]);
1727
1567
  }
1728
- }
1729
-
1568
+ };
1730
1569
  /// processes a node with the given function
1731
1570
  /// and adds the task to the existing buffer if we're buffering events
1732
1571
  const processNodeWithBuffer = async (node, identifier, exec) => {
1733
- ev.buffer()
1734
- await execTask()
1735
- ev.flush()
1736
-
1572
+ ev.buffer();
1573
+ await execTask();
1574
+ ev.flush();
1737
1575
  function execTask() {
1738
- return exec(node, false)
1739
- .catch(err => onUnexpectedError(err, identifier))
1576
+ return exec(node, false).catch(err => onUnexpectedError(err, identifier));
1740
1577
  }
1741
- }
1742
-
1743
- const makeOfflineNodeProcessor = () => {
1744
- const nodeProcessorMap = new Map([
1745
- ['message', handleMessage],
1746
- ['call', handleCall],
1747
- ['receipt', handleReceipt],
1748
- ['notification', handleNotification]
1749
- ])
1750
-
1751
- const nodes = []
1752
- let isProcessing = false
1753
-
1754
- const enqueue = (type, node) => {
1755
- nodes.push({
1756
- type,
1757
- node
1758
- })
1759
-
1760
- if (isProcessing) {
1761
- return
1762
- }
1763
-
1764
- isProcessing = true
1765
-
1766
- const promise = async () => {
1767
- // PENTING: sebelumnya kalau nodeProcessor(node) throw (misal bug upstream
1768
- // baileys "No participant in group message" pas decrypt node offline yang
1769
- // malformed), exception itu keluar dari while-loop ini SEBELUM sempat
1770
- // `isProcessing = false`. Akibatnya isProcessing kepentok true SELAMANYA,
1771
- // dan enqueue() di bawah selalu langsung `return` tanpa pernah mulai
1772
- // proses lagi -> semua pesan sesudahnya cuma numpuk di array `nodes` dan
1773
- // gak pernah diproses lagi sampai bot di-restart total. Ini kemungkinan
1774
- // besar penyebab "reconnect tapi gak pernah nerima pesan lagi".
1775
- try {
1776
- while (nodes.length && ws.isOpen) {
1777
- const {
1778
- type,
1779
- node
1780
- } = nodes.shift()
1781
- const nodeProcessor = nodeProcessorMap.get(type)
1782
- if (!nodeProcessor) {
1783
- onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node')
1784
- continue
1785
- }
1786
- try {
1787
- await nodeProcessor(node)
1788
- } catch (error) {
1789
- // Satu node gagal (misal pesan corrupt/participant kosong) TIDAK
1790
- // boleh ngeblok node-node lain di belakangnya dalam antrian.
1791
- onUnexpectedError(error, 'processing offline node')
1792
- }
1793
- }
1794
- } finally {
1795
- isProcessing = false
1796
- }
1797
- }
1798
- promise().catch(error => onUnexpectedError(error, 'processing offline nodes'))
1578
+ };
1579
+ const offlineNodeProcessor = makeOfflineNodeProcessor(new Map([
1580
+ ['message', handleMessage],
1581
+ ['call', handleCall],
1582
+ ['receipt', handleReceipt],
1583
+ ['notification', handleNotification]
1584
+ ]), {
1585
+ isWsOpen: () => ws.isOpen,
1586
+ onUnexpectedError,
1587
+ yieldToEventLoop: () => new Promise(resolve => setImmediate(resolve))
1588
+ });
1589
+ const processNode = async (type, node, identifier, exec) => {
1590
+ // Fast path: ack and drop ignored JIDs before entering the buffer/queue
1591
+ const from = node.attrs.from;
1592
+ let ignoreJid = from;
1593
+ if (type === 'receipt' && from) {
1594
+ const attrs = node.attrs;
1595
+ const isLid = attrs.from.includes('lid');
1596
+ const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id);
1597
+ ignoreJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient;
1799
1598
  }
1800
-
1801
- return {
1802
- enqueue
1599
+ if (ignoreJid && ignoreJid !== S_WHATSAPP_NET && shouldIgnoreJid(ignoreJid)) {
1600
+ await sendMessageAck(node, type === 'message' ? NACK_REASONS.UnhandledError : undefined);
1601
+ return;
1803
1602
  }
1804
- }
1805
-
1806
- const offlineNodeProcessor = makeOfflineNodeProcessor()
1807
-
1808
- const processNode = async (type, node, identifier, exec) => {
1809
- const isOffline = !!node.attrs.offline
1810
-
1603
+ const isOffline = !!node.attrs.offline;
1811
1604
  if (isOffline) {
1812
- offlineNodeProcessor.enqueue(type, node)
1813
- } else {
1814
- await processNodeWithBuffer(node, identifier, exec)
1605
+ offlineNodeProcessor.enqueue(type, node);
1815
1606
  }
1816
- }
1817
-
1607
+ else {
1608
+ await processNodeWithBuffer(node, identifier, exec);
1609
+ }
1610
+ };
1818
1611
  // recv a message
1819
- // Semua handler di-await supaya event 'CB:message'/'CB:call'/'CB:receipt'/'CB:notification'
1820
- // yang datang beruntun tidak diproses bersamaan tanpa urutan (race condition di state
1821
- // bersama seperti session & cache).
1822
1612
  ws.on('CB:message', async (node) => {
1823
- await processNode('message', node, 'processing message', handleMessage)
1824
- })
1613
+ await processNode('message', node, 'processing message', handleMessage);
1614
+ });
1825
1615
  ws.on('CB:call', async (node) => {
1826
- await processNode('call', node, 'handling call', handleCall)
1827
- })
1616
+ await processNode('call', node, 'handling call', handleCall);
1617
+ });
1828
1618
  ws.on('CB:receipt', async (node) => {
1829
- await processNode('receipt', node, 'handling receipt', handleReceipt)
1830
- })
1619
+ await processNode('receipt', node, 'handling receipt', handleReceipt);
1620
+ });
1831
1621
  ws.on('CB:notification', async (node) => {
1832
- await processNode('notification', node, 'handling notification', handleNotification)
1833
- })
1622
+ await processNode('notification', node, 'handling notification', handleNotification);
1623
+ });
1834
1624
  ws.on('CB:ack,class:message', (node) => {
1835
- handleBadAck(node)
1836
- .catch(error => onUnexpectedError(error, 'handling bad ack'))
1837
- })
1838
- ev.on('call', ([call]) => {
1625
+ handleBadAck(node).catch(error => onUnexpectedError(error, 'handling bad ack'));
1626
+ });
1627
+ ev.on('call', async ([call]) => {
1628
+ if (!call) {
1629
+ return;
1630
+ }
1839
1631
  // missed call + group call notification message generation
1840
1632
  if (call.status === 'timeout' || (call.status === 'offer' && call.isGroup)) {
1841
1633
  const msg = {
@@ -1844,49 +1636,137 @@ const makeMessagesRecvSocket = (config) => {
1844
1636
  id: call.id,
1845
1637
  fromMe: false
1846
1638
  },
1847
- messageTimestamp: Utils_1.unixTimestampSeconds(call.date),
1848
- }
1849
-
1639
+ messageTimestamp: unixTimestampSeconds(call.date)
1640
+ };
1850
1641
  if (call.status === 'timeout') {
1851
1642
  if (call.isGroup) {
1852
- msg.messageStubType = call.isVideo ? Types_1.WAMessageStubType.CALL_MISSED_GROUP_VIDEO : Types_1.WAMessageStubType.CALL_MISSED_GROUP_VOICE
1853
- } else {
1854
- msg.messageStubType = call.isVideo ? Types_1.WAMessageStubType.CALL_MISSED_VIDEO : Types_1.WAMessageStubType.CALL_MISSED_VOICE
1643
+ msg.messageStubType = call.isVideo
1644
+ ? WAMessageStubType.CALL_MISSED_GROUP_VIDEO
1645
+ : WAMessageStubType.CALL_MISSED_GROUP_VOICE;
1855
1646
  }
1856
- } else {
1857
- msg.message = {
1858
- call: {
1859
- callKey: Buffer.from(call.id)
1860
- }
1647
+ else {
1648
+ msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE;
1861
1649
  }
1862
1650
  }
1863
-
1864
- const protoMsg = WAProto_1.proto.WebMessageInfo.fromObject(msg)
1865
- upsertMessage(protoMsg, call.offline ? 'append' : 'notify')
1866
- }
1867
- })
1868
-
1869
- ev.on('connection.update', ({
1870
- isOnline
1871
- }) => {
1651
+ else {
1652
+ msg.message = { call: { callKey: Buffer.from(call.id) } };
1653
+ }
1654
+ const protoMsg = proto.WebMessageInfo.fromObject(msg);
1655
+ await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
1656
+ }
1657
+ });
1658
+ /** timestamp of last tctoken prune run — throttles to once per 24h */
1659
+ let lastTcTokenPruneTs = 0;
1660
+ /** dedupe in-flight 463 recovery token issuance by target JID */
1661
+ const inFlight463Recoveries = new Set();
1662
+ ev.on('connection.update', ({ isOnline, connection }) => {
1872
1663
  if (typeof isOnline !== 'undefined') {
1873
- sendActiveReceipts = isOnline
1874
- logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`)
1664
+ sendActiveReceipts = isOnline;
1665
+ logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`);
1666
+ }
1667
+ // Flush pending tctoken index save on disconnect to avoid writing after close
1668
+ if (connection === 'close' && tcTokenIndexTimer) {
1669
+ clearTimeout(tcTokenIndexTimer);
1670
+ tcTokenIndexTimer = undefined;
1671
+ // Best-effort flush — may fail if store is already closed
1672
+ try {
1673
+ void Promise.resolve(flushTcTokenIndex()).catch(() => { });
1674
+ }
1675
+ catch {
1676
+ /* ignore sync errors */
1677
+ }
1678
+ }
1679
+ // Prune expired tctokens when coming online, at most once per 24 hours
1680
+ // Matches WA Web's CLEAN_TC_TOKENS task
1681
+ // Note: don't gate on tcTokenKnownJids.size — the index may still be loading
1682
+ if (isOnline) {
1683
+ const now = Date.now();
1684
+ const DAY_MS = 24 * 60 * 60 * 1000;
1685
+ if (now - lastTcTokenPruneTs >= DAY_MS) {
1686
+ lastTcTokenPruneTs = now;
1687
+ void pruneExpiredTcTokens();
1688
+ }
1689
+ }
1690
+ });
1691
+ registerSocketEndHandler(() => {
1692
+ if (!config.msgRetryCounterCache && msgRetryCache.close) {
1693
+ msgRetryCache.close();
1694
+ }
1695
+ if (!config.callOfferCache && callOfferCache.close) {
1696
+ callOfferCache.close();
1697
+ }
1698
+ identityAssertDebounce.close();
1699
+ sendActiveReceipts = false;
1700
+ });
1701
+ async function pruneExpiredTcTokens() {
1702
+ try {
1703
+ await tcTokenIndexLoaded;
1704
+ // Union with the persisted index picks up JIDs added by other layers
1705
+ // (history sync) without needing inter-module wiring.
1706
+ const persisted = await readTcTokenIndex(authState.keys);
1707
+ const allJids = new Set(tcTokenKnownJids);
1708
+ for (const jid of persisted)
1709
+ allJids.add(jid);
1710
+ if (!allJids.size)
1711
+ return;
1712
+ const jids = [...allJids];
1713
+ const allTokens = await authState.keys.get('tctoken', jids);
1714
+ const writes = {};
1715
+ const survivors = new Set();
1716
+ let mutated = 0;
1717
+ for (const jid of jids) {
1718
+ const entry = allTokens[jid];
1719
+ if (!entry) {
1720
+ // Tracked but nothing in store — drop from index.
1721
+ mutated++;
1722
+ continue;
1723
+ }
1724
+ const hasPeerToken = !!entry.token?.length;
1725
+ const peerTokenExpired = hasPeerToken && isTcTokenExpired(entry.timestamp);
1726
+ const hasSenderTs = entry.senderTimestamp !== undefined;
1727
+ const senderTsExpired = hasSenderTs && isTcTokenExpired(entry.senderTimestamp);
1728
+ const keepPeerToken = hasPeerToken && !peerTokenExpired;
1729
+ const keepSenderTs = hasSenderTs && !senderTsExpired;
1730
+ if (!keepPeerToken && !keepSenderTs) {
1731
+ writes[jid] = null;
1732
+ mutated++;
1733
+ }
1734
+ else if (peerTokenExpired && keepSenderTs) {
1735
+ writes[jid] = { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp };
1736
+ survivors.add(jid);
1737
+ mutated++;
1738
+ }
1739
+ else {
1740
+ survivors.add(jid);
1741
+ }
1742
+ }
1743
+ if (mutated === 0)
1744
+ return;
1745
+ await authState.keys.set({
1746
+ tctoken: {
1747
+ ...writes,
1748
+ [TC_TOKEN_INDEX_KEY]: {
1749
+ token: Buffer.from(JSON.stringify([...survivors]))
1750
+ }
1751
+ }
1752
+ });
1753
+ tcTokenKnownJids.clear();
1754
+ for (const jid of survivors)
1755
+ tcTokenKnownJids.add(jid);
1756
+ logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens');
1757
+ }
1758
+ catch (err) {
1759
+ logger.warn({ err: err?.message }, 'failed to prune expired tctokens');
1875
1760
  }
1876
- })
1877
-
1761
+ }
1878
1762
  return {
1879
1763
  ...sock,
1880
1764
  sendMessageAck,
1881
1765
  sendRetryRequest,
1882
- offerCall,
1883
1766
  rejectCall,
1884
1767
  fetchMessageHistory,
1885
1768
  requestPlaceholderResend,
1886
1769
  messageRetryManager
1887
- }
1888
- }
1889
-
1890
- module.exports = {
1891
- makeMessagesRecvSocket
1892
- }
1770
+ };
1771
+ };
1772
+ //# sourceMappingURL=messages-recv.js.map