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