@crysnovax/baileys 2.5.5 → 2.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,26 +4,21 @@ import { randomBytes } from 'crypto';
4
4
  import Long from 'long';
5
5
  import { proto } from '../../WAProto/index.js';
6
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';
7
+ import { WAMessageStatus, WAMessageStubType } from '../Types/index.js';
8
+ import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
9
9
  import { makeMutex } from '../Utils/make-mutex.js';
10
10
  import { makeOfflineNodeProcessor } from '../Utils/offline-node-processor.js';
11
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';
12
+ import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
14
13
  import { extractGroupMetadata } from './groups.js';
15
14
  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
15
  export const makeMessagesRecvSocket = (config) => {
21
16
  const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
22
17
  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);
18
+ const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, generateMessageTag, messageRetryManager, registerSocketEndHandler } = sock;
25
19
  /** this mutex ensures that each retryRequest will wait for the previous one to finish */
26
20
  const retryMutex = makeMutex();
21
+ const devicesMutex = makeMutex();
27
22
  const msgRetryCache = config.msgRetryCounterCache ||
28
23
  new NodeCache({
29
24
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
@@ -34,6 +29,16 @@ export const makeMessagesRecvSocket = (config) => {
34
29
  stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
35
30
  useClones: false
36
31
  });
32
+ const placeholderResendCache = config.placeholderResendCache ||
33
+ new NodeCache({
34
+ stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
35
+ useClones: false
36
+ });
37
+ const userDevicesCache = config.userDevicesCache ??=
38
+ new NodeCache({
39
+ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
40
+ useClones: false
41
+ });
37
42
  // Debounce identity-change session refreshes per JID to avoid bursts
38
43
  const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false });
39
44
  let sendActiveReceipts = false;
@@ -87,140 +92,25 @@ export const makeMessagesRecvSocket = (config) => {
87
92
  }, 8000);
88
93
  return sendPeerDataOperationMessage(pdoMessage);
89
94
  };
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;
148
- }
149
- await handleLegacyMexNewsletterNotification(node);
150
- };
151
- const handleReachoutTimelockNotification = (data) => {
152
- const payload = data.xwa2_notify_account_reachout_timelock;
153
- if (!payload) {
154
- logger.warn('reachout timelock notification missing payload');
155
- return;
156
- }
157
- if (!payload.is_active) {
158
- logger.info('reachout timelock restriction lifted');
159
- ev.emit('connection.update', {
160
- reachoutTimeLock: {
161
- isActive: false,
162
- enforcementType: ReachoutTimelockEnforcementType.DEFAULT
163
- }
164
- });
165
- return;
166
- }
167
- // WA Web defaults to now+60s when the server omits the expiry
168
- const timeEnforcementEnds = payload.time_enforcement_ends
169
- ? new Date(parseInt(payload.time_enforcement_ends, 10) * 1000)
170
- : new Date(Date.now() + 60000);
171
- const enforcementType = isValidEnforcementType(payload.enforcement_type)
172
- ? payload.enforcement_type
173
- : ReachoutTimelockEnforcementType.DEFAULT;
174
- logger.info({ enforcementType, timeEnforcementEnds }, 'reachout timelock restriction set');
175
- ev.emit('connection.update', {
176
- reachoutTimeLock: {
177
- isActive: true,
178
- timeEnforcementEnds,
179
- enforcementType
180
- }
181
- });
182
- };
183
- const handleMessageCappingNotification = (data) => {
184
- const payload = data.xwa2_notify_new_chat_messages_capping_info_update;
185
- if (!payload) {
186
- logger.warn('message capping notification missing payload');
187
- return;
188
- }
189
- logger.info({ payload }, 'received message capping update');
190
- ev.emit('message-capping.update', payload);
191
- };
192
- const handleLegacyMexNewsletterNotification = async (node) => {
95
+ // Handles mex newsletter notifications
96
+ const handleMexNewsletterNotification = async (node) => {
193
97
  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');
98
+ if (!mexNode?.content) {
99
+ logger.warn({ node }, 'Invalid mex newsletter notification');
198
100
  return;
199
101
  }
200
102
  let data;
201
103
  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());
104
+ data = JSON.parse(mexNode.content.toString());
209
105
  }
210
106
  catch (error) {
211
- logger.error({ err: error, node: binaryNodeToString(node) }, 'failed to parse mex newsletter notification');
107
+ logger.error({ err: error, node }, 'Failed to parse mex newsletter notification');
212
108
  return;
213
109
  }
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
- }
110
+ const operation = data?.operation;
111
+ const updates = data?.updates;
222
112
  if (!updates || !operation) {
223
- logger.warn({ data }, 'invalid mex newsletter notification content');
113
+ logger.warn({ data }, 'Invalid mex newsletter notification content');
224
114
  return;
225
115
  }
226
116
  logger.info({ operation, updates }, 'got mex newsletter notification');
@@ -248,114 +138,91 @@ export const makeMessagesRecvSocket = (config) => {
248
138
  }
249
139
  }
250
140
  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
141
  default:
268
- logger.info({ operation, data }, 'unhandled mex newsletter notification');
142
+ logger.info({ operation, data }, 'Unhandled mex newsletter notification');
269
143
  break;
270
144
  }
271
145
  };
272
146
  // Handles newsletter notifications
273
147
  const handleNewsletterNotification = async (node) => {
274
148
  const from = node.attrs.from;
275
- const children = getAllBinaryNodeChildren(node);
149
+ const child = getAllBinaryNodeChildren(node)[0];
276
150
  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 = {
151
+ logger.info({ from, child }, 'got newsletter notification');
152
+ switch (child.tag) {
153
+ case 'reaction':
154
+ const reactionUpdate = {
155
+ id: from,
156
+ server_id: child.attrs.message_id,
157
+ reaction: {
158
+ code: getBinaryNodeChildString(child, 'reaction'),
159
+ count: 1
160
+ }
161
+ };
162
+ ev.emit('newsletter.reaction', reactionUpdate);
163
+ break;
164
+ case 'view':
165
+ const viewUpdate = {
166
+ id: from,
167
+ server_id: child.attrs.message_id,
168
+ count: parseInt(child.content?.toString() || '0', 10)
169
+ };
170
+ ev.emit('newsletter.view', viewUpdate);
171
+ break;
172
+ case 'participant':
173
+ const participantUpdate = {
174
+ id: from,
175
+ author,
176
+ user: child.attrs.jid,
177
+ action: child.attrs.action,
178
+ new_role: child.attrs.role
179
+ };
180
+ ev.emit('newsletter-participants.update', participantUpdate);
181
+ break;
182
+ case 'update':
183
+ const settingsNode = getBinaryNodeChild(child, 'settings');
184
+ if (settingsNode) {
185
+ const update = {};
186
+ const nameNode = getBinaryNodeChild(settingsNode, 'name');
187
+ if (nameNode?.content)
188
+ update.name = nameNode.content.toString();
189
+ const descriptionNode = getBinaryNodeChild(settingsNode, 'description');
190
+ if (descriptionNode?.content)
191
+ update.description = descriptionNode.content.toString();
192
+ ev.emit('newsletter-settings.update', {
303
193
  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;
194
+ update
195
+ });
311
196
  }
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
- });
197
+ break;
198
+ case 'message':
199
+ const plaintextNode = getBinaryNodeChild(child, 'plaintext');
200
+ if (plaintextNode?.content) {
201
+ try {
202
+ const contentBuf = typeof plaintextNode.content === 'string'
203
+ ? Buffer.from(plaintextNode.content, 'binary')
204
+ : Buffer.from(plaintextNode.content);
205
+ const messageProto = proto.Message.decode(contentBuf).toJSON();
206
+ const fullMessage = proto.WebMessageInfo.fromObject({
207
+ key: {
208
+ remoteJid: from,
209
+ id: child.attrs.message_id || child.attrs.server_id,
210
+ fromMe: false // TODO: is this really true though
211
+ },
212
+ message: messageProto,
213
+ messageTimestamp: +child.attrs.t
214
+ }).toJSON();
215
+ await upsertMessage(fullMessage, 'append');
216
+ logger.info('Processed plaintext newsletter message');
326
217
  }
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
- }
218
+ catch (error) {
219
+ logger.error({ error }, 'Failed to decode plaintext newsletter message');
352
220
  }
353
- break;
354
221
  }
355
- default:
356
- logger.warn({ node, child }, 'Unknown newsletter notification child');
357
- break;
358
- }
222
+ break;
223
+ default:
224
+ logger.warn({ node }, 'Unknown newsletter notification');
225
+ break;
359
226
  }
360
227
  };
361
228
  const sendMessageAck = async (node, errorCode) => {
@@ -384,6 +251,97 @@ export const makeMessagesRecvSocket = (config) => {
384
251
  };
385
252
  await query(stanza);
386
253
  };
254
+ // Lia@Note 01-03-26 --- Source: https://github.com/koptereli/Baileys/commit/575cd41e6f01a9b3e1d7e2708c2292fa93de91f2
255
+ const initiateCall = async (jid, options = {}) => {
256
+ const meId = authState.creds.me?.id;
257
+ if (!meId) {
258
+ throw new Boom('Not authenticated');
259
+ }
260
+ const callId = randomBytes(8).toString('hex');
261
+ const isVideo = !!options.isVideo;
262
+ const isGroup = isJidGroup(jid);
263
+ const stanza = {
264
+ tag: 'call',
265
+ attrs: {
266
+ id: generateMessageTag(),
267
+ from: meId,
268
+ to: jid,
269
+ t: String(unixTimestampSeconds()),
270
+ ...(authState.creds.me?.name ? { notify: authState.creds.me.name } : {})
271
+ },
272
+ content: [
273
+ {
274
+ tag: 'offer',
275
+ attrs: {
276
+ 'call-id': callId,
277
+ 'call-creator': meId,
278
+ count: '0'
279
+ },
280
+ content: [
281
+ {
282
+ tag: isVideo ? 'video' : 'audio',
283
+ attrs: {}
284
+ },
285
+ {
286
+ tag: 'net',
287
+ attrs: {}
288
+ },
289
+ {
290
+ tag: 'encopt',
291
+ attrs: { key: randomBytes(2).toString('hex') }
292
+ },
293
+ {
294
+ tag: 'relaylatency',
295
+ attrs: {}
296
+ },
297
+ {
298
+ tag: 'te',
299
+ attrs: {}
300
+ }
301
+ ]
302
+ }
303
+ ]
304
+ };
305
+ await query(stanza);
306
+ await callOfferCache.set(callId, {
307
+ chatId: jid,
308
+ from: meId,
309
+ id: callId,
310
+ date: new Date(),
311
+ offline: false,
312
+ status: 'offer',
313
+ isVideo,
314
+ isGroup,
315
+ groupJid: isGroup ? jid : undefined
316
+ });
317
+ // TODO: implement ICE/DTLS-SRTP call media setup once full signaling requirements are mapped.
318
+ return { callId, to: jid, isVideo };
319
+ };
320
+ const cancelCall = async (callId, callTo) => {
321
+ const meId = authState.creds.me?.id;
322
+ if (!meId) {
323
+ throw new Boom('Not authenticated');
324
+ }
325
+ const stanza = {
326
+ tag: 'call',
327
+ attrs: {
328
+ from: meId,
329
+ to: callTo
330
+ },
331
+ content: [
332
+ {
333
+ tag: 'terminate',
334
+ attrs: {
335
+ 'call-id': callId,
336
+ 'call-creator': meId,
337
+ count: '0'
338
+ }
339
+ }
340
+ ]
341
+ };
342
+ await query(stanza);
343
+ await callOfferCache.del(callId);
344
+ };
387
345
  const sendRetryRequest = async (node, forceIncludeKeys = false) => {
388
346
  const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '');
389
347
  const { key: msgKey } = fullMessage;
@@ -515,58 +473,15 @@ export const makeMessagesRecvSocket = (config) => {
515
473
  logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt');
516
474
  }, authState?.creds?.me?.id || 'sendRetryRequest');
517
475
  };
518
- // Mirrors WAWeb/Handle/PreKeyLow.js: skip a re-issued notification with the same stanza id.
519
- const inFlightPreKeyLow = new Set();
520
- /**
521
- * Fire-and-forget tctoken re-issuance after a peer's device identity changed.
522
- * Mirrors WAWebSendTcTokenWhenDeviceIdentityChange — runs in parallel with
523
- * the session refresh (not after it).
524
- */
525
- const reissueTcTokenAfterIdentityChange = (from) => {
526
- void (async () => {
527
- const normalizedJid = jidNormalizedUser(from);
528
- const tcJid = await resolveTcTokenJid(normalizedJid, getLIDForPN);
529
- const tcTokenData = await authState.keys.get('tctoken', [tcJid]);
530
- const senderTs = tcTokenData?.[tcJid]?.senderTimestamp;
531
- if (senderTs === null || senderTs === undefined || isTcTokenExpired(senderTs)) {
532
- return;
533
- }
534
- logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken');
535
- const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
536
- const issueJid = await resolveIssuanceJid(normalizedJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID);
537
- const result = await issuePrivacyTokens([issueJid], senderTs);
538
- await storeTcTokensFromIqResult({
539
- result,
540
- fallbackJid: tcJid,
541
- keys: authState.keys,
542
- getLIDForPN,
543
- onNewJidStored: trackTcTokenJid
544
- });
545
- })().catch(err => {
546
- logger.debug({ jid: from, err: err?.message }, 'failed to re-issue tctoken after identity change');
547
- });
548
- };
549
476
  const handleEncryptNotification = async (node) => {
550
477
  const from = node.attrs.from;
551
478
  if (from === S_WHATSAPP_NET) {
552
- const stanzaId = node.attrs.id;
553
- if (stanzaId && inFlightPreKeyLow.has(stanzaId)) {
554
- return;
555
- }
556
479
  const countChild = getBinaryNodeChild(node, 'count');
557
480
  const count = +countChild.attrs.value;
558
481
  const shouldUploadMorePreKeys = count < MIN_PREKEY_COUNT;
559
482
  logger.debug({ count, shouldUploadMorePreKeys }, 'recv pre-key count');
560
483
  if (shouldUploadMorePreKeys) {
561
- if (stanzaId)
562
- inFlightPreKeyLow.add(stanzaId);
563
- try {
564
- await uploadPreKeys();
565
- }
566
- finally {
567
- if (stanzaId)
568
- inFlightPreKeyLow.delete(stanzaId);
569
- }
484
+ await uploadPreKeys();
570
485
  }
571
486
  }
572
487
  else {
@@ -576,8 +491,7 @@ export const makeMessagesRecvSocket = (config) => {
576
491
  validateSession: signalRepository.validateSession,
577
492
  assertSessions,
578
493
  debounceCache: identityAssertDebounce,
579
- logger,
580
- onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange
494
+ logger
581
495
  });
582
496
  if (result.action === 'no_identity_node') {
583
497
  logger.info({ node }, 'unknown encrypt notification');
@@ -588,7 +502,6 @@ export const makeMessagesRecvSocket = (config) => {
588
502
  // TODO: Support PN/LID (Here is only LID now)
589
503
  const actingParticipantLid = fullNode.attrs.participant;
590
504
  const actingParticipantPn = fullNode.attrs.participant_pn;
591
- const actingParticipantUsername = fullNode.attrs.participant_username;
592
505
  const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid;
593
506
  const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn;
594
507
  switch (child?.tag) {
@@ -608,8 +521,7 @@ export const makeMessagesRecvSocket = (config) => {
608
521
  {
609
522
  ...metadata,
610
523
  author: actingParticipantLid,
611
- authorPn: actingParticipantPn,
612
- authorUsername: actingParticipantUsername
524
+ authorPn: actingParticipantPn
613
525
  }
614
526
  ]);
615
527
  break;
@@ -640,7 +552,6 @@ export const makeMessagesRecvSocket = (config) => {
640
552
  id: attrs.jid,
641
553
  phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
642
554
  lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
643
- username: attrs.participant_username || attrs.username || undefined,
644
555
  admin: (attrs.type || null)
645
556
  };
646
557
  });
@@ -713,83 +624,58 @@ export const makeMessagesRecvSocket = (config) => {
713
624
  const handleDevicesNotification = async (node) => {
714
625
  const [child] = getAllBinaryNodeChildren(node);
715
626
  const from = jidNormalizedUser(node.attrs.from);
716
- if (!child) {
717
- logger.debug({ from }, 'devices notification missing child, skipping');
718
- return;
719
- }
720
- const tag = child.tag;
721
- const deviceHash = child.attrs.device_hash;
722
627
  const devices = getBinaryNodeChildren(child, 'device');
723
- if (areJidsSameUser(from, authState.creds.me.id) || areJidsSameUser(from, authState.creds.me.lid)) {
628
+ if (areJidsSameUser(from, authState.creds.me.id) ||
629
+ areJidsSameUser(from, authState.creds.me.lid)) {
724
630
  const deviceJids = devices.map(d => d.attrs.jid);
725
631
  logger.info({ deviceJids }, 'got my own devices');
726
632
  }
727
- if (!devices.length) {
728
- logger.debug({ from, tag }, 'no devices in notification, skipping');
633
+ if (!devices || !devices.length || !devices[0]) {
634
+ logger.debug({ from }, 'no devices in notification, skipping');
729
635
  return;
730
636
  }
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 });
742
- }
743
- if (!decoded.length)
637
+ const deviceJid = devices[0].attrs.jid;
638
+ const decoded = jidDecode(deviceJid);
639
+ if (!decoded) return;
640
+ const { user, device } = decoded;
641
+ const tag = child.tag;
642
+ if (!deviceJid) {
643
+ logger.debug({ tag }, 'no device jid in notification, skipping');
744
644
  return;
645
+ }
745
646
  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);
647
+ if (tag === 'update') {
648
+ logger.debug({ user }, `${user}'s device list updated, dropping cached devices`);
649
+ if (userDevicesCache) {
650
+ await userDevicesCache.del(user);
792
651
  }
652
+ return;
653
+ }
654
+ const existingCache = (await (userDevicesCache?.get(user))) || [];
655
+ if (!existingCache.length) {
656
+ logger.debug({ user, tag }, 'device list not cached, skipping cache update')
657
+ return;
658
+ }
659
+ const deviceHash = child.attrs.device_hash;
660
+ let updatedDevices = [];
661
+ switch (tag) {
662
+ case 'add':
663
+ logger.info({ deviceHash }, 'device added');
664
+ updatedDevices = [
665
+ ...existingCache.filter(d => d.device !== device),
666
+ { user, device }
667
+ ];
668
+ break;
669
+ case 'remove':
670
+ logger.info({ deviceHash }, 'device removed');
671
+ updatedDevices = existingCache.filter(d => d.device !== device);
672
+ break;
673
+ default:
674
+ logger.debug({ tag }, 'Unknown device list change tag');
675
+ return;
676
+ }
677
+ if (updatedDevices.length > 0 && userDevicesCache) {
678
+ await userDevicesCache.set(user, updatedDevices);
793
679
  }
794
680
  });
795
681
  };
@@ -803,7 +689,7 @@ export const makeMessagesRecvSocket = (config) => {
803
689
  await handleNewsletterNotification(node);
804
690
  break;
805
691
  case 'mex':
806
- await handleMexNotification(node);
692
+ await handleMexNewsletterNotification(node);
807
693
  break;
808
694
  case 'w:gp2':
809
695
  // TODO: HANDLE PARTICIPANT_PN
@@ -821,7 +707,7 @@ export const makeMessagesRecvSocket = (config) => {
821
707
  await handleDevicesNotification(node);
822
708
  }
823
709
  catch (error) {
824
- logger.error({ error, node }, 'failed to handle devices notification');
710
+ logger.error({ error, node }, 'failed to handle devices notification');
825
711
  }
826
712
  break;
827
713
  case 'server_sync':
@@ -948,70 +834,27 @@ export const makeMessagesRecvSocket = (config) => {
948
834
  return result;
949
835
  }
950
836
  };
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
837
  const handlePrivacyTokenNotification = async (node) => {
997
838
  const tokensNode = getBinaryNodeChild(node, 'tokens');
839
+ const from = jidNormalizedUser(node.attrs.from);
998
840
  if (!tokensNode)
999
841
  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
- });
842
+ const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
843
+ for (const tokenNode of tokenNodes) {
844
+ const { attrs, content } = tokenNode;
845
+ const type = attrs.type;
846
+ const timestamp = attrs.t;
847
+ if (type === 'trusted_contact' && content instanceof Buffer) {
848
+ logger.debug({
849
+ from,
850
+ timestamp,
851
+ tcToken: content
852
+ }, 'received trusted contact token');
853
+ await authState.keys.set({
854
+ tctoken: { [from]: { token: content, timestamp } }
855
+ });
856
+ }
857
+ }
1015
858
  };
1016
859
  async function decipherLinkPublicKey(data) {
1017
860
  const buffer = toRequiredBuffer(data);
@@ -1037,11 +880,10 @@ export const makeMessagesRecvSocket = (config) => {
1037
880
  const newValue = ((await msgRetryCache.get(key)) || 0) + 1;
1038
881
  await msgRetryCache.set(key, newValue);
1039
882
  };
1040
- const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
883
+ const sendMessagesAgain = async (key, ids, retryNode) => {
1041
884
  const remoteJid = key.remoteJid;
1042
885
  const participant = key.participant || remoteJid;
1043
886
  const retryCount = +retryNode.attrs.count || 1;
1044
- const msgId = ids[0];
1045
887
  // Try to get messages from cache first, then fallback to getMessage
1046
888
  const msgs = [];
1047
889
  for (const id of ids) {
@@ -1073,49 +915,12 @@ export const makeMessagesRecvSocket = (config) => {
1073
915
  // just re-send the message to everyone
1074
916
  // prevents the first message decryption failure
1075
917
  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
- }
918
+ // Check if we should recreate session for this retry
1115
919
  let shouldRecreateSession = false;
1116
920
  let recreateReason = '';
1117
- if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
921
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
1118
922
  try {
923
+ const sessionId = signalRepository.jidToSignalProtocolAddress(participant);
1119
924
  const hasSession = await signalRepository.validateSession(participant);
1120
925
  const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists);
1121
926
  shouldRecreateSession = result.recreate;
@@ -1129,13 +934,11 @@ export const makeMessagesRecvSocket = (config) => {
1129
934
  logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry');
1130
935
  }
1131
936
  }
1132
- if (!injectedFromBundle) {
1133
- await assertSessions([participant], true);
1134
- }
937
+ await assertSessions([participant], true);
1135
938
  if (isJidGroup(remoteJid)) {
1136
939
  await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } });
1137
940
  }
1138
- logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason, injectedFromBundle }, 'prepared session for retry resend');
941
+ logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason }, 'forced new session for retry recp');
1139
942
  for (const [i, msg] of msgs.entries()) {
1140
943
  if (!ids[i])
1141
944
  continue;
@@ -1170,6 +973,11 @@ export const makeMessagesRecvSocket = (config) => {
1170
973
  fromMe,
1171
974
  participant: attrs.participant
1172
975
  };
976
+ if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
977
+ logger.debug({ remoteJid }, 'ignoring receipt from jid');
978
+ await sendMessageAck(node);
979
+ return;
980
+ }
1173
981
  const ids = [attrs.id];
1174
982
  if (Array.isArray(content)) {
1175
983
  const items = getBinaryNodeChildren(content[0], 'item');
@@ -1211,7 +1019,7 @@ export const makeMessagesRecvSocket = (config) => {
1211
1019
  try {
1212
1020
  await updateSendMessageAgainCount(ids[0], key.participant);
1213
1021
  logger.debug({ attrs, key }, 'recv retry request');
1214
- await sendMessagesAgain(key, ids, retryNode, node);
1022
+ await sendMessagesAgain(key, ids, retryNode);
1215
1023
  }
1216
1024
  catch (error) {
1217
1025
  logger.error({ key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' }, 'error in sending message again');
@@ -1234,6 +1042,11 @@ export const makeMessagesRecvSocket = (config) => {
1234
1042
  };
1235
1043
  const handleNotification = async (node) => {
1236
1044
  const remoteJid = node.attrs.from;
1045
+ if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
1046
+ logger.debug({ remoteJid, id: node.attrs.id }, 'ignored notification');
1047
+ await sendMessageAck(node);
1048
+ return;
1049
+ }
1237
1050
  try {
1238
1051
  await Promise.all([
1239
1052
  notificationMutex.mutex(async () => {
@@ -1246,7 +1059,6 @@ export const makeMessagesRecvSocket = (config) => {
1246
1059
  fromMe,
1247
1060
  participant: node.attrs.participant,
1248
1061
  participantAlt,
1249
- participantUsername: node.attrs.participant_username,
1250
1062
  addressingMode,
1251
1063
  id: node.attrs.id,
1252
1064
  ...(msg.key || {})
@@ -1264,6 +1076,11 @@ export const makeMessagesRecvSocket = (config) => {
1264
1076
  }
1265
1077
  };
1266
1078
  const handleMessage = async (node) => {
1079
+ if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== S_WHATSAPP_NET) {
1080
+ logger.debug({ key: node.attrs.key }, 'ignored message');
1081
+ await sendMessageAck(node, NACK_REASONS.UnhandledError);
1082
+ return;
1083
+ }
1267
1084
  const encNode = getBinaryNodeChild(node, 'enc');
1268
1085
  // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
1269
1086
  if (encNode?.attrs.type === 'msmsg') {
@@ -1367,14 +1184,29 @@ export const makeMessagesRecvSocket = (config) => {
1367
1184
  return sendMessageAck(node);
1368
1185
  }
1369
1186
  }
1370
- logger.debug('[handleMessage] Attempting retry request for failed decryption');
1371
- // WAWeb only retry-receipts here; server emits PreKeyLow if prekeys run low.
1187
+ const errorMessage = msg?.messageStubParameters?.[0] || '';
1188
+ const isPreKeyError = errorMessage.includes('PreKey');
1189
+ logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
1190
+ // Handle both pre-key and normal retries in single mutex
1372
1191
  await retryMutex.mutex(async () => {
1373
1192
  try {
1374
1193
  if (!ws.isOpen) {
1375
1194
  logger.debug({ node }, 'Connection closed, skipping retry');
1376
1195
  return;
1377
1196
  }
1197
+ // Handle pre-key errors with upload and delay
1198
+ if (isPreKeyError) {
1199
+ logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
1200
+ try {
1201
+ logger.debug('Uploading pre-keys for error recovery');
1202
+ await uploadPreKeys(5);
1203
+ logger.debug('Waiting for server to process new pre-keys');
1204
+ await delay(1000);
1205
+ }
1206
+ catch (uploadErr) {
1207
+ logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
1208
+ }
1209
+ }
1378
1210
  const encNode = getBinaryNodeChild(node, 'enc');
1379
1211
  await sendRetryRequest(node, !encNode);
1380
1212
  if (retryRequestDelayMs) {
@@ -1382,7 +1214,15 @@ export const makeMessagesRecvSocket = (config) => {
1382
1214
  }
1383
1215
  }
1384
1216
  catch (err) {
1385
- logger.error({ err }, 'Failed to send retry');
1217
+ logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
1218
+ // Still attempt retry even if pre-key upload failed
1219
+ try {
1220
+ const encNode = getBinaryNodeChild(node, 'enc');
1221
+ await sendRetryRequest(node, !encNode);
1222
+ }
1223
+ catch (retryErr) {
1224
+ logger.error({ retryErr }, 'Failed to send retry after error handling');
1225
+ }
1386
1226
  }
1387
1227
  acked = true;
1388
1228
  await sendMessageAck(node, NACK_REASONS.UnhandledError);
@@ -1458,13 +1298,6 @@ export const makeMessagesRecvSocket = (config) => {
1458
1298
  offline: !!attrs.offline,
1459
1299
  status
1460
1300
  };
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
1301
  if (status === 'offer') {
1469
1302
  call.isVideo = !!getBinaryNodeChild(infoChild, 'video');
1470
1303
  call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'];
@@ -1509,61 +1342,29 @@ export const makeMessagesRecvSocket = (config) => {
1509
1342
  // error in acknowledgement,
1510
1343
  // device could not display the message
1511
1344
  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');
1549
- }
1550
- else if (isReachoutTimelocked) {
1551
- // user is temporarily restricted, fetch current restriction details
1552
- await fetchAccountReachoutTimelock().catch(err => logger.warn({ err }, 'failed to fetch reachout timelock'));
1553
- logger.warn({ attrs }, 'received error in ack');
1554
- }
1555
- else {
1556
- logger.warn({ attrs }, 'received error in ack');
1557
- }
1345
+ logger.warn({ attrs }, 'received error in ack');
1558
1346
  ev.emit('messages.update', [
1559
1347
  {
1560
1348
  key,
1561
1349
  update: {
1562
1350
  status: WAMessageStatus.ERROR,
1563
- messageStubParameters: isReachoutTimelocked ? [attrs.error, ACCOUNT_RESTRICTED_TEXT] : [attrs.error]
1351
+ messageStubParameters: [attrs.error]
1564
1352
  }
1565
1353
  }
1566
1354
  ]);
1355
+ // resend the message with device_fanout=false, use at your own risk
1356
+ // if (attrs.error === '475') {
1357
+ // const msg = await getMessage(key)
1358
+ // if (msg) {
1359
+ // await relayMessage(key.remoteJid!, msg, {
1360
+ // messageId: key.id!,
1361
+ // useUserDevicesCache: false,
1362
+ // additionalAttributes: {
1363
+ // device_fanout: 'false'
1364
+ // }
1365
+ // })
1366
+ // }
1367
+ // }
1567
1368
  }
1568
1369
  };
1569
1370
  /// processes a node with the given function
@@ -1587,19 +1388,6 @@ export const makeMessagesRecvSocket = (config) => {
1587
1388
  yieldToEventLoop: () => new Promise(resolve => setImmediate(resolve))
1588
1389
  });
1589
1390
  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
- }
1603
1391
  const isOffline = !!node.attrs.offline;
1604
1392
  if (isOffline) {
1605
1393
  offlineNodeProcessor.enqueue(type, node);
@@ -1655,38 +1443,11 @@ export const makeMessagesRecvSocket = (config) => {
1655
1443
  await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
1656
1444
  }
1657
1445
  });
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 }) => {
1446
+ ev.on('connection.update', ({ isOnline }) => {
1663
1447
  if (typeof isOnline !== 'undefined') {
1664
1448
  sendActiveReceipts = isOnline;
1665
1449
  logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`);
1666
1450
  }
1667
- // Flush pending tctoken index save on disconnect to avoid writing after close
1668
- if (connection === 'close' && tcTokenIndexTimer) {
1669
- clearTimeout(tcTokenIndexTimer);
1670
- tcTokenIndexTimer = undefined;
1671
- // Best-effort flush — may fail if store is already closed
1672
- try {
1673
- void Promise.resolve(flushTcTokenIndex()).catch(() => { });
1674
- }
1675
- catch {
1676
- /* ignore sync errors */
1677
- }
1678
- }
1679
- // Prune expired tctokens when coming online, at most once per 24 hours
1680
- // Matches WA Web's CLEAN_TC_TOKENS task
1681
- // Note: don't gate on tcTokenKnownJids.size — the index may still be loading
1682
- if (isOnline) {
1683
- const now = Date.now();
1684
- const DAY_MS = 24 * 60 * 60 * 1000;
1685
- if (now - lastTcTokenPruneTs >= DAY_MS) {
1686
- lastTcTokenPruneTs = now;
1687
- void pruneExpiredTcTokens();
1688
- }
1689
- }
1690
1451
  });
1691
1452
  registerSocketEndHandler(() => {
1692
1453
  if (!config.msgRetryCounterCache && msgRetryCache.close) {
@@ -1695,78 +1456,21 @@ export const makeMessagesRecvSocket = (config) => {
1695
1456
  if (!config.callOfferCache && callOfferCache.close) {
1696
1457
  callOfferCache.close();
1697
1458
  }
1459
+ if (!config.placeholderResendCache && placeholderResendCache.close) {
1460
+ placeholderResendCache.close();
1461
+ }
1698
1462
  identityAssertDebounce.close();
1699
1463
  sendActiveReceipts = false;
1700
1464
  });
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
- }
1762
1465
  return {
1763
1466
  ...sock,
1764
1467
  sendMessageAck,
1765
1468
  sendRetryRequest,
1766
1469
  rejectCall,
1470
+ initiateCall,
1471
+ cancelCall,
1767
1472
  fetchMessageHistory,
1768
1473
  requestPlaceholderResend,
1769
1474
  messageRetryManager
1770
1475
  };
1771
- };
1772
- //# sourceMappingURL=messages-recv.js.map
1476
+ };