@ryuu-reinzz/baileys 3.0.0-beta.2 → 3.0.0-beta.21

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.
@@ -7,6 +7,7 @@ import { SyncState } from '../Types/State.js';
7
7
  import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, getHistoryMsg, newLTHashState, processSyncAction } from '../Utils/index.js';
8
8
  import { makeMutex } from '../Utils/make-mutex.js';
9
9
  import processMessage from '../Utils/process-message.js';
10
+ import { buildTcTokenFromJid } from '../Utils/tc-token-utils.js';
10
11
  import { getBinaryNodeChild, getBinaryNodeChildren, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
11
12
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
12
13
  import { makeSocket } from './socket.js';
@@ -14,11 +15,17 @@ const MAX_SYNC_ATTEMPTS = 2;
14
15
  export const makeChatsSocket = (config) => {
15
16
  const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
16
17
  const sock = makeSocket(config);
17
- const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError } = sock;
18
+ const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession } = sock;
18
19
  let privacySettings;
19
20
  let syncState = SyncState.Connecting;
20
- /** this mutex ensures that the notifications (receipts, messages etc.) are processed in order */
21
- const processingMutex = makeMutex();
21
+ /** this mutex ensures that messages are processed in order */
22
+ const messageMutex = makeMutex();
23
+ /** this mutex ensures that receipts are processed in order */
24
+ const receiptMutex = makeMutex();
25
+ /** this mutex ensures that app state patches are processed in order */
26
+ const appStatePatchMutex = makeMutex();
27
+ /** this mutex ensures that notifications are processed in order */
28
+ const notificationMutex = makeMutex();
22
29
  // Timeout for AwaitingInitialSync state
23
30
  let awaitingSyncTimeout;
24
31
  const placeholderResendCache = config.placeholderResendCache ||
@@ -346,6 +353,15 @@ export const makeChatsSocket = (config) => {
346
353
  };
347
354
  };
348
355
  const resyncAppState = ev.createBufferedFunction(async (collections, isInitialSync) => {
356
+ const appStateSyncKeyCache = new Map();
357
+ const getCachedAppStateSyncKey = async (keyId) => {
358
+ if (appStateSyncKeyCache.has(keyId)) {
359
+ return appStateSyncKeyCache.get(keyId) ?? undefined;
360
+ }
361
+ const key = await getAppStateSyncKey(keyId);
362
+ appStateSyncKeyCache.set(keyId, key ?? null);
363
+ return key;
364
+ };
349
365
  // we use this to determine which events to fire
350
366
  // otherwise when we resync from scratch -- all notifications will fire
351
367
  const initialVersionMap = {};
@@ -405,7 +421,7 @@ export const makeChatsSocket = (config) => {
405
421
  const { patches, hasMorePatches, snapshot } = decoded[name];
406
422
  try {
407
423
  if (snapshot) {
408
- const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot);
424
+ const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot);
409
425
  states[name] = newState;
410
426
  Object.assign(globalMutationMap, mutationMap);
411
427
  logger.info(`restored state of ${name} from snapshot to v${newState.version} with mutations`);
@@ -413,7 +429,7 @@ export const makeChatsSocket = (config) => {
413
429
  }
414
430
  // only process if there are syncd patches
415
431
  if (patches.length) {
416
- const { state: newState, mutationMap } = await decodePatches(name, patches, states[name], getAppStateSyncKey, config.options, initialVersionMap[name], logger, appStateMacVerification.patch);
432
+ const { state: newState, mutationMap } = await decodePatches(name, patches, states[name], getCachedAppStateSyncKey, config.options, initialVersionMap[name], logger, appStateMacVerification.patch);
417
433
  await authState.keys.set({ 'app-state-sync-version': { [name]: newState } });
418
434
  logger.info(`synced ${name} to v${newState.version}`);
419
435
  initialVersionMap[name] = newState.version;
@@ -456,7 +472,8 @@ export const makeChatsSocket = (config) => {
456
472
  * type = "image for the high res picture"
457
473
  */
458
474
  const profilePictureUrl = async (jid, type = 'preview', timeoutMs) => {
459
- // TOOD: Add support for tctoken, existingID, and newsletter + group options
475
+ const baseContent = [{ tag: 'picture', attrs: { type, query: 'url' } }];
476
+ const tcTokenContent = await buildTcTokenFromJid({ authState, jid, baseContent });
460
477
  jid = jidNormalizedUser(jid);
461
478
  const result = await query({
462
479
  tag: 'iq',
@@ -466,7 +483,7 @@ export const makeChatsSocket = (config) => {
466
483
  type: 'get',
467
484
  xmlns: 'w:profile:picture'
468
485
  },
469
- content: [{ tag: 'picture', attrs: { type, query: 'url' } }]
486
+ content: tcTokenContent
470
487
  }, timeoutMs);
471
488
  const child = getBinaryNodeChild(result, 'picture');
472
489
  return child?.attrs?.url;
@@ -491,12 +508,16 @@ export const makeChatsSocket = (config) => {
491
508
  };
492
509
  const sendPresenceUpdate = async (type, toJid) => {
493
510
  const me = authState.creds.me;
494
- if (type === 'available' || type === 'unavailable') {
511
+ const isAvailableType = type === 'available';
512
+ if (isAvailableType || type === 'unavailable') {
495
513
  if (!me.name) {
496
514
  logger.warn('no name present, ignoring presence update request...');
497
515
  return;
498
516
  }
499
- ev.emit('connection.update', { isOnline: type === 'available' });
517
+ ev.emit('connection.update', { isOnline: isAvailableType });
518
+ if (isAvailableType) {
519
+ void sendUnifiedSession();
520
+ }
500
521
  await sendNode({
501
522
  tag: 'presence',
502
523
  attrs: {
@@ -527,23 +548,18 @@ export const makeChatsSocket = (config) => {
527
548
  * @param toJid the jid to subscribe to
528
549
  * @param tcToken token for subscription, use if present
529
550
  */
530
- const presenceSubscribe = (toJid, tcToken) => sendNode({
531
- tag: 'presence',
532
- attrs: {
533
- to: toJid,
534
- id: generateMessageTag(),
535
- type: 'subscribe'
536
- },
537
- content: tcToken
538
- ? [
539
- {
540
- tag: 'tctoken',
541
- attrs: {},
542
- content: tcToken
543
- }
544
- ]
545
- : undefined
546
- });
551
+ const presenceSubscribe = async (toJid) => {
552
+ const tcTokenContent = await buildTcTokenFromJid({ authState, jid: toJid });
553
+ return sendNode({
554
+ tag: 'presence',
555
+ attrs: {
556
+ to: toJid,
557
+ id: generateMessageTag(),
558
+ type: 'subscribe'
559
+ },
560
+ content: tcTokenContent
561
+ });
562
+ };
547
563
  const handlePresenceUpdate = ({ tag, attrs, content }) => {
548
564
  let presence;
549
565
  const jid = attrs.from;
@@ -583,7 +599,7 @@ export const makeChatsSocket = (config) => {
583
599
  }
584
600
  let initial;
585
601
  let encodeResult;
586
- await processingMutex.mutex(async () => {
602
+ await appStatePatchMutex.mutex(async () => {
587
603
  await authState.keys.transaction(async () => {
588
604
  logger.debug({ patch: patchCreate }, 'applying app patch');
589
605
  await resyncAppState([name], false);
@@ -916,11 +932,22 @@ export const makeChatsSocket = (config) => {
916
932
  }
917
933
  }, 20000);
918
934
  });
935
+ ev.on('lid-mapping.update', async ({ lid, pn }) => {
936
+ try {
937
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
938
+ }
939
+ catch (error) {
940
+ logger.warn({ lid, pn, error }, 'Failed to store LID-PN mapping');
941
+ }
942
+ });
919
943
  return {
920
944
  ...sock,
921
945
  createCallLink,
922
946
  getBotListV2,
923
- processingMutex,
947
+ messageMutex,
948
+ receiptMutex,
949
+ appStatePatchMutex,
950
+ notificationMutex,
924
951
  fetchPrivacySettings,
925
952
  upsertMessage,
926
953
  appPatch,
@@ -6,12 +6,6 @@ const makeWASocket = (config) => {
6
6
  ...DEFAULT_CONNECTION_CONFIG,
7
7
  ...config
8
8
  };
9
- // If the user hasn't provided their own history sync function,
10
- // let's create a default one that respects the syncFullHistory flag.
11
- // TODO: Change
12
- if (config.shouldSyncHistoryMessage === undefined) {
13
- newConfig.shouldSyncHistoryMessage = () => !!newConfig.syncFullHistory;
14
- }
15
9
  return makeCommunitiesSocket(newConfig);
16
10
  };
17
11
  export default makeWASocket;
@@ -3,9 +3,9 @@ import { Boom } from '@hapi/boom';
3
3
  import { randomBytes } from 'crypto';
4
4
  import Long from 'long';
5
5
  import { proto } from '../../WAProto/index.js';
6
- import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults/index.js';
6
+ import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults/index.js';
7
7
  import { WAMessageStatus, WAMessageStubType } from '../Types/index.js';
8
- import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/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 { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
11
11
  import { extractGroupMetadata } from './groups.js';
@@ -13,7 +13,7 @@ import { makeMessagesSocket } from './messages-send.js';
13
13
  export const makeMessagesRecvSocket = (config) => {
14
14
  const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
15
15
  const sock = makeMessagesSocket(config);
16
- const { ev, authState, ws, processingMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager } = sock;
16
+ const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager } = sock;
17
17
  /** this mutex ensures that each retryRequest will wait for the previous one to finish */
18
18
  const retryMutex = makeMutex();
19
19
  const msgRetryCache = config.msgRetryCounterCache ||
@@ -50,19 +50,21 @@ export const makeMessagesRecvSocket = (config) => {
50
50
  };
51
51
  return sendPeerDataOperationMessage(pdoMessage);
52
52
  };
53
- const requestPlaceholderResend = async (messageKey) => {
53
+ const requestPlaceholderResend = async (messageKey, msgData) => {
54
54
  if (!authState.creds.me?.id) {
55
55
  throw new Boom('Not authenticated');
56
56
  }
57
- if (placeholderResendCache.get(messageKey?.id)) {
57
+ if (await placeholderResendCache.get(messageKey?.id)) {
58
58
  logger.debug({ messageKey }, 'already requested resend');
59
59
  return;
60
60
  }
61
61
  else {
62
- await placeholderResendCache.set(messageKey?.id, true);
62
+ // Store original message data so PDO response handler can preserve
63
+ // metadata (LID details, timestamps, etc.) that the phone may omit
64
+ await placeholderResendCache.set(messageKey?.id, msgData || true);
63
65
  }
64
- await delay(5000);
65
- if (!placeholderResendCache.get(messageKey?.id)) {
66
+ await delay(2000);
67
+ if (!(await placeholderResendCache.get(messageKey?.id))) {
66
68
  logger.debug({ messageKey }, 'message received while resend requested');
67
69
  return 'RESOLVED';
68
70
  }
@@ -75,11 +77,11 @@ export const makeMessagesRecvSocket = (config) => {
75
77
  peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
76
78
  };
77
79
  setTimeout(async () => {
78
- if (placeholderResendCache.get(messageKey?.id)) {
79
- logger.debug({ messageKey }, 'PDO message without response after 15 seconds. Phone possibly offline');
80
+ if (await placeholderResendCache.get(messageKey?.id)) {
81
+ logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline');
80
82
  await placeholderResendCache.del(messageKey?.id);
81
83
  }
82
- }, 15000);
84
+ }, 8000);
83
85
  return sendPeerDataOperationMessage(pdoMessage);
84
86
  };
85
87
  // Handles mex newsletter notifications
@@ -300,12 +302,12 @@ export const makeMessagesRecvSocket = (config) => {
300
302
  // Check if we should recreate the session
301
303
  let shouldRecreateSession = false;
302
304
  let recreateReason = '';
303
- if (enableAutoSessionRecreation && messageRetryManager) {
305
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
304
306
  try {
305
307
  // Check if we have a session with this JID
306
308
  const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid);
307
309
  const hasSession = await signalRepository.validateSession(fromJid);
308
- const result = messageRetryManager.shouldRecreateSession(fromJid, retryCount, hasSession.exists);
310
+ const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists);
309
311
  shouldRecreateSession = result.recreate;
310
312
  recreateReason = result.reason;
311
313
  if (shouldRecreateSession) {
@@ -407,22 +409,15 @@ export const makeMessagesRecvSocket = (config) => {
407
409
  }
408
410
  }
409
411
  else {
410
- const identityNode = getBinaryNodeChild(node, 'identity');
411
- if (identityNode) {
412
- logger.info({ jid: from }, 'identity changed');
413
- if (identityAssertDebounce.get(from)) {
414
- logger.debug({ jid: from }, 'skipping identity assert (debounced)');
415
- return;
416
- }
417
- identityAssertDebounce.set(from, true);
418
- try {
419
- await assertSessions([from], true);
420
- }
421
- catch (error) {
422
- logger.warn({ error, jid: from }, 'failed to assert sessions after identity change');
423
- }
424
- }
425
- else {
412
+ const result = await handleIdentityChange(node, {
413
+ meId: authState.creds.me?.id,
414
+ meLid: authState.creds.me?.lid,
415
+ validateSession: signalRepository.validateSession,
416
+ assertSessions,
417
+ debounceCache: identityAssertDebounce,
418
+ logger
419
+ });
420
+ if (result.action === 'no_identity_node') {
426
421
  logger.info({ node }, 'unknown encrypt notification');
427
422
  }
428
423
  }
@@ -592,6 +587,7 @@ export const makeMessagesRecvSocket = (config) => {
592
587
  case 'picture':
593
588
  const setPicture = getBinaryNodeChild(node, 'set');
594
589
  const delPicture = getBinaryNodeChild(node, 'delete');
590
+ // TODO: WAJIDHASH stuff proper support inhouse
595
591
  ev.emit('contacts.update', [
596
592
  {
597
593
  id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
@@ -644,7 +640,7 @@ export const makeMessagesRecvSocket = (config) => {
644
640
  const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey);
645
641
  const random = randomBytes(32);
646
642
  const linkCodeSalt = randomBytes(32);
647
- const linkCodePairingExpanded = await hkdf(companionSharedKey, 32, {
643
+ const linkCodePairingExpanded = hkdf(companionSharedKey, 32, {
648
644
  salt: linkCodeSalt,
649
645
  info: 'link_code_pairing_key_bundle_encryption_key'
650
646
  });
@@ -658,7 +654,7 @@ export const makeMessagesRecvSocket = (config) => {
658
654
  const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted]);
659
655
  const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey);
660
656
  const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random]);
661
- authState.creds.advSecretKey = (await hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64');
657
+ authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64');
662
658
  await query({
663
659
  tag: 'iq',
664
660
  attrs: {
@@ -789,11 +785,11 @@ export const makeMessagesRecvSocket = (config) => {
789
785
  // Check if we should recreate session for this retry
790
786
  let shouldRecreateSession = false;
791
787
  let recreateReason = '';
792
- if (enableAutoSessionRecreation && messageRetryManager) {
788
+ if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
793
789
  try {
794
790
  const sessionId = signalRepository.jidToSignalProtocolAddress(participant);
795
791
  const hasSession = await signalRepository.validateSession(participant);
796
- const result = messageRetryManager.shouldRecreateSession(participant, retryCount, hasSession.exists);
792
+ const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists);
797
793
  shouldRecreateSession = result.recreate;
798
794
  recreateReason = result.reason;
799
795
  if (shouldRecreateSession) {
@@ -856,7 +852,7 @@ export const makeMessagesRecvSocket = (config) => {
856
852
  }
857
853
  try {
858
854
  await Promise.all([
859
- processingMutex.mutex(async () => {
855
+ receiptMutex.mutex(async () => {
860
856
  const status = getStatusFromReceiptType(attrs.type);
861
857
  if (typeof status !== 'undefined' &&
862
858
  // basically, we only want to know when a message from us has been delivered to/read by the other person
@@ -877,7 +873,7 @@ export const makeMessagesRecvSocket = (config) => {
877
873
  else {
878
874
  ev.emit('messages.update', ids.map(id => ({
879
875
  key: { ...key, id },
880
- update: { status }
876
+ update: { status, messageTimestamp: toNumber(+(attrs.t ?? 0)) }
881
877
  })));
882
878
  }
883
879
  }
@@ -920,7 +916,7 @@ export const makeMessagesRecvSocket = (config) => {
920
916
  }
921
917
  try {
922
918
  await Promise.all([
923
- processingMutex.mutex(async () => {
919
+ notificationMutex.mutex(async () => {
924
920
  const msg = await processNotification(node);
925
921
  if (msg) {
926
922
  const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id);
@@ -954,7 +950,7 @@ export const makeMessagesRecvSocket = (config) => {
954
950
  }
955
951
  const encNode = getBinaryNodeChild(node, 'enc');
956
952
  // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
957
- if (encNode && encNode.attrs.type === 'msmsg') {
953
+ if (encNode?.attrs.type === 'msmsg') {
958
954
  logger.debug({ key: node.attrs.key }, 'ignored msmsg');
959
955
  await sendMessageAck(node, NACK_REASONS.MissingMessageSecret);
960
956
  return;
@@ -984,58 +980,123 @@ export const makeMessagesRecvSocket = (config) => {
984
980
  }, 'Added message to recent cache for retry receipts');
985
981
  }
986
982
  try {
987
- await processingMutex.mutex(async () => {
983
+ await messageMutex.mutex(async () => {
988
984
  await decrypt();
989
985
  // message failed to decrypt
990
986
  if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
991
- if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT ||
992
- msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
993
- return sendMessageAck(node);
987
+ if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
988
+ return sendMessageAck(node, NACK_REASONS.ParsingError);
994
989
  }
995
- const errorMessage = msg?.messageStubParameters?.[0] || '';
996
- const isPreKeyError = errorMessage.includes('PreKey');
997
- logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
998
- // Handle both pre-key and normal retries in single mutex
999
- await retryMutex.mutex(async () => {
1000
- try {
1001
- if (!ws.isOpen) {
1002
- logger.debug({ node }, 'Connection closed, skipping retry');
1003
- return;
1004
- }
1005
- // Handle pre-key errors with upload and delay
1006
- if (isPreKeyError) {
1007
- logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
1008
- try {
1009
- logger.debug('Uploading pre-keys for error recovery');
1010
- await uploadPreKeys(5);
1011
- logger.debug('Waiting for server to process new pre-keys');
1012
- await delay(1000);
1013
- }
1014
- catch (uploadErr) {
1015
- logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
1016
- }
990
+ if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
991
+ // Message arrived without encryption (e.g. CTWA ads messages).
992
+ // Check if this is eligible for placeholder resend (matching WA Web filters).
993
+ const unavailableNode = getBinaryNodeChild(node, 'unavailable');
994
+ const unavailableType = unavailableNode?.attrs?.type;
995
+ if (unavailableType === 'bot_unavailable_fanout' ||
996
+ unavailableType === 'hosted_unavailable_fanout' ||
997
+ unavailableType === 'view_once_unavailable_fanout') {
998
+ logger.debug({ msgId: msg.key.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type');
999
+ return sendMessageAck(node);
1000
+ }
1001
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
1002
+ if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) {
1003
+ logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message');
1004
+ return sendMessageAck(node);
1005
+ }
1006
+ // Request the real content from the phone via placeholder resend PDO.
1007
+ // Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg),
1008
+ // and store the requestId in stubParameters[1] so users can correlate
1009
+ // with the incoming PDO response event.
1010
+ const cleanKey = {
1011
+ remoteJid: msg.key.remoteJid,
1012
+ fromMe: msg.key.fromMe,
1013
+ id: msg.key.id,
1014
+ participant: msg.key.participant
1015
+ };
1016
+ // Cache the original message metadata so the PDO response handler
1017
+ // can preserve key fields (LID details etc.) that the phone may omit
1018
+ const msgData = {
1019
+ key: msg.key,
1020
+ messageTimestamp: msg.messageTimestamp,
1021
+ pushName: msg.pushName,
1022
+ participant: msg.participant,
1023
+ verifiedBizName: msg.verifiedBizName
1024
+ };
1025
+ requestPlaceholderResend(cleanKey, msgData)
1026
+ .then(requestId => {
1027
+ if (requestId && requestId !== 'RESOLVED') {
1028
+ logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message');
1029
+ ev.emit('messages.update', [
1030
+ {
1031
+ key: msg.key,
1032
+ update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] }
1033
+ }
1034
+ ]);
1017
1035
  }
1018
- const encNode = getBinaryNodeChild(node, 'enc');
1019
- await sendRetryRequest(node, !encNode);
1020
- if (retryRequestDelayMs) {
1021
- await delay(retryRequestDelayMs);
1036
+ })
1037
+ .catch(err => {
1038
+ logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message');
1039
+ });
1040
+ await sendMessageAck(node);
1041
+ // Don't return — fall through to upsertMessage so the stub is emitted
1042
+ }
1043
+ else {
1044
+ // Skip retry for expired status messages (>24h old)
1045
+ if (isJidStatusBroadcast(msg.key.remoteJid)) {
1046
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
1047
+ if (messageAge > STATUS_EXPIRY_SECONDS) {
1048
+ logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, 'skipping retry for expired status message');
1049
+ return sendMessageAck(node);
1022
1050
  }
1023
1051
  }
1024
- catch (err) {
1025
- logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
1026
- // Still attempt retry even if pre-key upload failed
1052
+ const errorMessage = msg?.messageStubParameters?.[0] || '';
1053
+ const isPreKeyError = errorMessage.includes('PreKey');
1054
+ logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
1055
+ // Handle both pre-key and normal retries in single mutex
1056
+ await retryMutex.mutex(async () => {
1027
1057
  try {
1058
+ if (!ws.isOpen) {
1059
+ logger.debug({ node }, 'Connection closed, skipping retry');
1060
+ return;
1061
+ }
1062
+ // Handle pre-key errors with upload and delay
1063
+ if (isPreKeyError) {
1064
+ logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
1065
+ try {
1066
+ logger.debug('Uploading pre-keys for error recovery');
1067
+ await uploadPreKeys(5);
1068
+ logger.debug('Waiting for server to process new pre-keys');
1069
+ await delay(1000);
1070
+ }
1071
+ catch (uploadErr) {
1072
+ logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
1073
+ }
1074
+ }
1028
1075
  const encNode = getBinaryNodeChild(node, 'enc');
1029
1076
  await sendRetryRequest(node, !encNode);
1077
+ if (retryRequestDelayMs) {
1078
+ await delay(retryRequestDelayMs);
1079
+ }
1030
1080
  }
1031
- catch (retryErr) {
1032
- logger.error({ retryErr }, 'Failed to send retry after error handling');
1081
+ catch (err) {
1082
+ logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
1083
+ // Still attempt retry even if pre-key upload failed
1084
+ try {
1085
+ const encNode = getBinaryNodeChild(node, 'enc');
1086
+ await sendRetryRequest(node, !encNode);
1087
+ }
1088
+ catch (retryErr) {
1089
+ logger.error({ retryErr }, 'Failed to send retry after error handling');
1090
+ }
1033
1091
  }
1034
- }
1035
- await sendMessageAck(node, NACK_REASONS.UnhandledError);
1036
- });
1092
+ await sendMessageAck(node, NACK_REASONS.UnhandledError);
1093
+ });
1094
+ }
1037
1095
  }
1038
1096
  else {
1097
+ if (messageRetryManager && msg.key.id) {
1098
+ messageRetryManager.cancelPendingPhoneRequest(msg.key.id);
1099
+ }
1039
1100
  const isNewsletter = isJidNewsletter(msg.key.remoteJid);
1040
1101
  if (!isNewsletter) {
1041
1102
  // no type in the receipt => message delivered
@@ -1089,6 +1150,7 @@ export const makeMessagesRecvSocket = (config) => {
1089
1150
  const call = {
1090
1151
  chatId: attrs.from,
1091
1152
  from,
1153
+ callerPn: infoChild.attrs['caller_pn'],
1092
1154
  id: callId,
1093
1155
  date: new Date(+attrs.t * 1000),
1094
1156
  offline: !!attrs.offline,
@@ -1105,6 +1167,7 @@ export const makeMessagesRecvSocket = (config) => {
1105
1167
  if (existingCall) {
1106
1168
  call.isVideo = existingCall.isVideo;
1107
1169
  call.isGroup = existingCall.isGroup;
1170
+ call.callerPn = call.callerPn || existingCall.callerPn;
1108
1171
  }
1109
1172
  // delete data once call has ended
1110
1173
  if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
@@ -1166,6 +1229,10 @@ export const makeMessagesRecvSocket = (config) => {
1166
1229
  return exec(node, false).catch(err => onUnexpectedError(err, identifier));
1167
1230
  }
1168
1231
  };
1232
+ /** Yields control to the event loop to prevent blocking */
1233
+ const yieldToEventLoop = () => {
1234
+ return new Promise(resolve => setImmediate(resolve));
1235
+ };
1169
1236
  const makeOfflineNodeProcessor = () => {
1170
1237
  const nodeProcessorMap = new Map([
1171
1238
  ['message', handleMessage],
@@ -1175,6 +1242,8 @@ export const makeMessagesRecvSocket = (config) => {
1175
1242
  ]);
1176
1243
  const nodes = [];
1177
1244
  let isProcessing = false;
1245
+ // Number of nodes to process before yielding to event loop
1246
+ const BATCH_SIZE = 10;
1178
1247
  const enqueue = (type, node) => {
1179
1248
  nodes.push({ type, node });
1180
1249
  if (isProcessing) {
@@ -1182,6 +1251,7 @@ export const makeMessagesRecvSocket = (config) => {
1182
1251
  }
1183
1252
  isProcessing = true;
1184
1253
  const promise = async () => {
1254
+ let processedInBatch = 0;
1185
1255
  while (nodes.length && ws.isOpen) {
1186
1256
  const { type, node } = nodes.shift();
1187
1257
  const nodeProcessor = nodeProcessorMap.get(type);
@@ -1190,6 +1260,13 @@ export const makeMessagesRecvSocket = (config) => {
1190
1260
  continue;
1191
1261
  }
1192
1262
  await nodeProcessor(node);
1263
+ processedInBatch++;
1264
+ // Yield to event loop after processing a batch
1265
+ // This prevents blocking the event loop for too long when there are many offline nodes
1266
+ if (processedInBatch >= BATCH_SIZE) {
1267
+ processedInBatch = 0;
1268
+ await yieldToEventLoop();
1269
+ }
1193
1270
  }
1194
1271
  isProcessing = false;
1195
1272
  };