@alannxd/baileys 6.0.3 → 6.0.5

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