@ryuu-reinzz/baileys 3.5.1 → 5.0.2

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 (41) hide show
  1. package/README.md +30 -25
  2. package/WAProto/fix-imports.js +22 -18
  3. package/WAProto/index.js +22 -18
  4. package/lib/Defaults/index.js +10 -9
  5. package/lib/Signal/libsignal.js +45 -17
  6. package/lib/Signal/lid-mapping.js +6 -0
  7. package/lib/Socket/chats.js +241 -39
  8. package/lib/Socket/groups.js +20 -0
  9. package/lib/Socket/messages-recv.js +736 -314
  10. package/lib/Socket/messages-send.js +279 -129
  11. package/lib/Socket/newsletter.js +2 -2
  12. package/lib/Socket/socket.js +56 -25
  13. package/lib/Types/{Newsletter.js → Mex.js} +9 -3
  14. package/lib/Types/State.js +43 -0
  15. package/lib/Types/index.js +1 -1
  16. package/lib/Utils/auth-utils.js +12 -0
  17. package/lib/Utils/chat-utils.js +80 -20
  18. package/lib/Utils/companion-reg-client-utils.js +35 -0
  19. package/lib/Utils/decode-wa-message.js +34 -0
  20. package/lib/Utils/event-buffer.js +49 -1
  21. package/lib/Utils/generics.js +12 -3
  22. package/lib/Utils/history.js +12 -9
  23. package/lib/Utils/identity-change-handler.js +1 -0
  24. package/lib/Utils/index.js +3 -1
  25. package/lib/Utils/link-preview.js +2 -2
  26. package/lib/Utils/message-retry-manager.js +40 -0
  27. package/lib/Utils/messages-media.js +21 -7
  28. package/lib/Utils/messages.js +28 -5
  29. package/lib/Utils/offline-node-processor.js +40 -0
  30. package/lib/Utils/process-message.js +103 -1
  31. package/lib/Utils/signal.js +42 -0
  32. package/lib/Utils/stanza-ack.js +38 -0
  33. package/lib/Utils/sync-action-utils.js +1 -0
  34. package/lib/Utils/tc-token-utils.js +149 -4
  35. package/lib/Utils/validate-connection.js +3 -0
  36. package/lib/WAUSync/Protocols/USyncContactProtocol.js +26 -3
  37. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
  38. package/lib/WAUSync/Protocols/index.js +1 -0
  39. package/lib/WAUSync/USyncQuery.js +6 -2
  40. package/lib/WAUSync/USyncUser.js +8 -0
  41. package/package.json +39 -12
@@ -1,22 +1,31 @@
1
1
  import NodeCache from '@cacheable/node-cache';
2
2
  import { Boom } from '@hapi/boom';
3
3
  import { proto } from '../../WAProto/index.js';
4
- import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
4
+ import { DEFAULT_CACHE_TTLS, HISTORY_SYNC_PAUSED_TIMEOUT_MS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
5
5
  import { ALL_WA_PATCH_NAMES } from '../Types/index.js';
6
6
  import { SyncState } from '../Types/State.js';
7
- import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, getHistoryMsg, newLTHashState, processSyncAction } from '../Utils/index.js';
7
+ import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, ensureLTHashStateVersion, extractSyncdPatches, generateProfilePicture, getHistoryMsg, isAppStateSyncIrrecoverable, isMissingKeyError, MAX_SYNC_ATTEMPTS, 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
10
  import { buildTcTokenFromJid } from '../Utils/tc-token-utils.js';
11
- import { getBinaryNodeChild, getBinaryNodeChildren, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
11
+ import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
12
12
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
13
13
  import { makeSocket } from './socket.js';
14
- const MAX_SYNC_ATTEMPTS = 2;
15
14
  export const makeChatsSocket = (config) => {
16
15
  const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
17
16
  const sock = makeSocket(config);
18
- const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession } = sock;
17
+ const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession, registerSocketEndHandler } = sock;
18
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
19
19
  let privacySettings;
20
+ /** Server-assigned AB props for protocol behavior. */
21
+ const serverProps = {
22
+ /** AB prop 10518: gate tctoken on 1:1 messages. Default true (safe: avoids 463). */
23
+ privacyTokenOn1to1: true,
24
+ /** AB prop 9666: gate tctoken on profile picture IQs. WA Web default: true. */
25
+ profilePicPrivacyToken: true,
26
+ /** AB prop 14303: issue tctokens to LID instead of PN. WA Web default: false. */
27
+ lidTrustedTokenIssueToLid: false
28
+ };
20
29
  let syncState = SyncState.Connecting;
21
30
  /** this mutex ensures that messages are processed in order */
22
31
  const messageMutex = makeMutex();
@@ -28,14 +37,20 @@ export const makeChatsSocket = (config) => {
28
37
  const notificationMutex = makeMutex();
29
38
  // Timeout for AwaitingInitialSync state
30
39
  let awaitingSyncTimeout;
40
+ // In-memory history sync completion tracking (resets on reconnection)
41
+ const historySyncStatus = {
42
+ initialBootstrapComplete: false,
43
+ recentSyncComplete: false
44
+ };
45
+ let historySyncPausedTimeout;
46
+ // Collections blocked on missing app state sync keys (mirrors WA Web's "Blocked" state).
47
+ // When a key arrives via APP_STATE_SYNC_KEY_SHARE, these are re-synced.
48
+ const blockedCollections = new Set();
31
49
  const placeholderResendCache = config.placeholderResendCache ||
32
50
  new NodeCache({
33
51
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
34
52
  useClones: false
35
53
  });
36
- if (!config.placeholderResendCache) {
37
- config.placeholderResendCache = placeholderResendCache;
38
- }
39
54
  /** helper function to fetch the given app state sync key */
40
55
  const getAppStateSyncKey = async (keyId) => {
41
56
  const { [keyId]: key } = await authState.keys.get('app-state-sync-key', [keyId]);
@@ -258,6 +273,42 @@ export const makeChatsSocket = (config) => {
258
273
  return getBinaryNodeChildren(listNode, 'item').map(n => n.attrs.jid);
259
274
  };
260
275
  const updateBlockStatus = async (jid, action) => {
276
+ const normalizedJid = jidNormalizedUser(jid);
277
+ let lid;
278
+ let pn_jid;
279
+ if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
280
+ lid = normalizedJid;
281
+ if (action === 'block') {
282
+ const pn = await signalRepository.lidMapping.getPNForLID(normalizedJid);
283
+ if (!pn) {
284
+ throw new Boom(`Unable to resolve PN JID for LID: ${jid}`, { statusCode: 400 });
285
+ }
286
+ pn_jid = jidNormalizedUser(pn);
287
+ }
288
+ }
289
+ else if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
290
+ const mapped = await signalRepository.lidMapping.getLIDForPN(normalizedJid);
291
+ if (!mapped) {
292
+ throw new Boom(`Unable to resolve LID for PN JID: ${jid}`, { statusCode: 400 });
293
+ }
294
+ lid = mapped;
295
+ if (action === 'block') {
296
+ pn_jid = jidNormalizedUser(normalizedJid);
297
+ }
298
+ }
299
+ else {
300
+ throw new Boom(`Invalid jid: ${jid}`, { statusCode: 400 });
301
+ }
302
+ const itemAttrs = {
303
+ action,
304
+ jid: lid
305
+ };
306
+ if (action === 'block') {
307
+ if (!pn_jid) {
308
+ throw new Boom(`pn_jid required for block: ${jid}`, { statusCode: 400 });
309
+ }
310
+ itemAttrs.pn_jid = pn_jid;
311
+ }
261
312
  await query({
262
313
  tag: 'iq',
263
314
  attrs: {
@@ -268,10 +319,7 @@ export const makeChatsSocket = (config) => {
268
319
  content: [
269
320
  {
270
321
  tag: 'item',
271
- attrs: {
272
- action,
273
- jid
274
- }
322
+ attrs: itemAttrs
275
323
  }
276
324
  ]
277
325
  });
@@ -370,6 +418,9 @@ export const makeChatsSocket = (config) => {
370
418
  const collectionsToHandle = new Set(collections);
371
419
  // in case something goes wrong -- ensure we don't enter a loop that cannot be exited from
372
420
  const attemptsMap = {};
421
+ // collections that failed and need a full snapshot on retry
422
+ // mirrors WA Web's ErrorFatal -> force snapshot behavior
423
+ const forceSnapshotCollections = new Set();
373
424
  // keep executing till all collections are done
374
425
  // sometimes a single patch request will not return all the patches (God knows why)
375
426
  // so we fetch till they're all done (this is determined by the "has_more_patches" flag)
@@ -380,6 +431,7 @@ export const makeChatsSocket = (config) => {
380
431
  const result = await authState.keys.get('app-state-sync-version', [name]);
381
432
  let state = result[name];
382
433
  if (state) {
434
+ state = ensureLTHashStateVersion(state);
383
435
  if (typeof initialVersionMap[name] === 'undefined') {
384
436
  initialVersionMap[name] = state.version;
385
437
  }
@@ -388,14 +440,18 @@ export const makeChatsSocket = (config) => {
388
440
  state = newLTHashState();
389
441
  }
390
442
  states[name] = state;
391
- logger.info(`resyncing ${name} from v${state.version}`);
443
+ const shouldForceSnapshot = forceSnapshotCollections.has(name);
444
+ if (shouldForceSnapshot) {
445
+ forceSnapshotCollections.delete(name);
446
+ }
447
+ logger.info(`resyncing ${name} from v${state.version}${shouldForceSnapshot ? ' (forcing snapshot)' : ''}`);
392
448
  nodes.push({
393
449
  tag: 'collection',
394
450
  attrs: {
395
451
  name,
396
452
  version: state.version.toString(),
397
- // return snapshot if being synced from scratch
398
- return_snapshot: (!state.version).toString()
453
+ // return snapshot if syncing from scratch or forcing after a failed attempt
454
+ return_snapshot: (shouldForceSnapshot || !state.version).toString()
399
455
  }
400
456
  });
401
457
  }
@@ -421,7 +477,7 @@ export const makeChatsSocket = (config) => {
421
477
  const { patches, hasMorePatches, snapshot } = decoded[name];
422
478
  try {
423
479
  if (snapshot) {
424
- const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot);
480
+ const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot, logger);
425
481
  states[name] = newState;
426
482
  Object.assign(globalMutationMap, mutationMap);
427
483
  logger.info(`restored state of ${name} from snapshot to v${newState.version} with mutations`);
@@ -444,19 +500,37 @@ export const makeChatsSocket = (config) => {
444
500
  }
445
501
  }
446
502
  catch (error) {
447
- // if retry attempts overshoot
448
- // or key not found
449
- const isIrrecoverableError = attemptsMap[name] >= MAX_SYNC_ATTEMPTS ||
450
- error.output?.statusCode === 404 ||
451
- error.name === 'TypeError';
452
- logger.info({ name, error: error.stack }, `failed to sync state from version${isIrrecoverableError ? '' : ', removing and trying from scratch'}`);
453
- await authState.keys.set({ 'app-state-sync-version': { [name]: null } });
454
- // increment number of retries
455
503
  attemptsMap[name] = (attemptsMap[name] || 0) + 1;
456
- if (isIrrecoverableError) {
457
- // stop retrying
504
+ const logData = {
505
+ name,
506
+ attempt: attemptsMap[name],
507
+ version: states[name].version,
508
+ statusCode: error.output?.statusCode,
509
+ errorType: error.name,
510
+ error: error.stack
511
+ };
512
+ if (isMissingKeyError(error) && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
513
+ // WA Web treats missing keys as "Blocked" — park the collection
514
+ // until the key arrives via APP_STATE_SYNC_KEY_SHARE.
515
+ logger.warn(logData, `${name} blocked on missing key from v${states[name].version}, parking after ${attemptsMap[name]} attempts`);
516
+ blockedCollections.add(name);
458
517
  collectionsToHandle.delete(name);
459
518
  }
519
+ else if (isMissingKeyError(error)) {
520
+ // Retry with a snapshot which may use a different key.
521
+ logger.info(logData, `${name} blocked on missing key from v${states[name].version}, retrying with snapshot`);
522
+ forceSnapshotCollections.add(name);
523
+ }
524
+ else if (isAppStateSyncIrrecoverable(error, attemptsMap[name])) {
525
+ logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`);
526
+ collectionsToHandle.delete(name);
527
+ }
528
+ else {
529
+ logger.info(logData, `failed to sync ${name} from v${states[name].version}, forcing snapshot retry`);
530
+ // force a full snapshot on retry to recover from
531
+ // corrupted local state (e.g. LTHash MAC mismatch)
532
+ forceSnapshotCollections.add(name);
533
+ }
460
534
  }
461
535
  }
462
536
  }
@@ -473,7 +547,22 @@ export const makeChatsSocket = (config) => {
473
547
  */
474
548
  const profilePictureUrl = async (jid, type = 'preview', timeoutMs) => {
475
549
  const baseContent = [{ tag: 'picture', attrs: { type, query: 'url' } }];
476
- const tcTokenContent = await buildTcTokenFromJid({ authState, jid, baseContent });
550
+ // WA Web only includes tctoken for user JIDs (not groups/newsletters)
551
+ // and never for own profile pic (Chat model for self has no tcToken).
552
+ // Including tctoken for own JID causes the server to never respond.
553
+ const normalizedJid = jidNormalizedUser(jid);
554
+ const isUserJid = isPnUser(normalizedJid) || isLidUser(normalizedJid);
555
+ const me = authState.creds.me;
556
+ const isSelf = me && (normalizedJid === jidNormalizedUser(me.id) || (me.lid && normalizedJid === jidNormalizedUser(me.lid)));
557
+ let content = baseContent;
558
+ if (serverProps.profilePicPrivacyToken && isUserJid && !isSelf) {
559
+ content = await buildTcTokenFromJid({
560
+ authState,
561
+ jid: normalizedJid,
562
+ baseContent,
563
+ getLIDForPN
564
+ });
565
+ }
477
566
  jid = jidNormalizedUser(jid);
478
567
  const result = await query({
479
568
  tag: 'iq',
@@ -483,7 +572,7 @@ export const makeChatsSocket = (config) => {
483
572
  type: 'get',
484
573
  xmlns: 'w:profile:picture'
485
574
  },
486
- content: tcTokenContent
575
+ content
487
576
  }, timeoutMs);
488
577
  const child = getBinaryNodeChild(result, 'picture');
489
578
  return child?.attrs?.url;
@@ -549,7 +638,12 @@ export const makeChatsSocket = (config) => {
549
638
  * @param tcToken token for subscription, use if present
550
639
  */
551
640
  const presenceSubscribe = async (toJid) => {
552
- const tcTokenContent = await buildTcTokenFromJid({ authState, jid: toJid });
641
+ // Only include tctoken for user JIDs groups/newsletters don't use tctokens
642
+ const normalizedToJid = jidNormalizedUser(toJid);
643
+ const isUserJid = isPnUser(normalizedToJid) || isLidUser(normalizedToJid);
644
+ const tcTokenContent = isUserJid
645
+ ? await buildTcTokenFromJid({ authState, jid: normalizedToJid, getLIDForPN })
646
+ : undefined;
553
647
  return sendNode({
554
648
  tag: 'presence',
555
649
  attrs: {
@@ -570,7 +664,8 @@ export const makeChatsSocket = (config) => {
570
664
  if (tag === 'presence') {
571
665
  presence = {
572
666
  lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available',
573
- lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined
667
+ lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined,
668
+ groupOnlineCount: attrs.count ? +attrs.count : undefined
574
669
  };
575
670
  }
576
671
  else if (Array.isArray(content)) {
@@ -604,7 +699,7 @@ export const makeChatsSocket = (config) => {
604
699
  logger.debug({ patch: patchCreate }, 'applying app patch');
605
700
  await resyncAppState([name], false);
606
701
  const { [name]: currentSyncVersion } = await authState.keys.get('app-state-sync-version', [name]);
607
- initial = currentSyncVersion || newLTHashState();
702
+ initial = currentSyncVersion ? ensureLTHashStateVersion(currentSyncVersion) : newLTHashState();
608
703
  encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey);
609
704
  const { patch, state } = encodeResult;
610
705
  const node = {
@@ -650,22 +745,21 @@ export const makeChatsSocket = (config) => {
650
745
  }
651
746
  }
652
747
  };
653
- /** sending non-abt props may fix QR scan fail if server expects */
748
+ /** fetch AB props */
654
749
  const fetchProps = async () => {
655
- //TODO: implement both protocol 1 and protocol 2 prop fetching, specially for abKey for WM
656
750
  const resultNode = await query({
657
751
  tag: 'iq',
658
752
  attrs: {
659
753
  to: S_WHATSAPP_NET,
660
- xmlns: 'w',
754
+ xmlns: 'abt',
661
755
  type: 'get'
662
756
  },
663
757
  content: [
664
758
  {
665
759
  tag: 'props',
666
760
  attrs: {
667
- protocol: '2',
668
- hash: authState?.creds?.lastPropHash || ''
761
+ protocol: '1',
762
+ ...(authState?.creds?.lastPropHash ? { hash: authState.creds.lastPropHash } : {})
669
763
  }
670
764
  }
671
765
  ]
@@ -680,7 +774,20 @@ export const makeChatsSocket = (config) => {
680
774
  }
681
775
  props = reduceBinaryNodeToDictionary(propsNode, 'prop');
682
776
  }
683
- logger.debug('fetched props');
777
+ // Extract protocol-relevant AB props (only the ones we need)
778
+ const privacyTokenProp = props['10518'] ?? props['privacy_token_sending_on_all_1_on_1_messages'];
779
+ if (privacyTokenProp !== undefined) {
780
+ serverProps.privacyTokenOn1to1 = privacyTokenProp === 'true' || privacyTokenProp === '1';
781
+ }
782
+ const profilePicProp = props['9666'] ?? props['profile_scraping_privacy_token_in_photo_iq'];
783
+ if (profilePicProp !== undefined) {
784
+ serverProps.profilePicPrivacyToken = profilePicProp === 'true' || profilePicProp === '1';
785
+ }
786
+ const lidIssueProp = props['14303'] ?? props['lid_trusted_token_issue_to_lid'];
787
+ if (lidIssueProp !== undefined) {
788
+ serverProps.lidTrustedTokenIssueToLid = lidIssueProp === 'true' || lidIssueProp === '1';
789
+ }
790
+ logger.debug({ serverProps }, 'fetched props');
684
791
  return props;
685
792
  };
686
793
  /**
@@ -820,6 +927,47 @@ export const makeChatsSocket = (config) => {
820
927
  ? shouldSyncHistoryMessage(historyMsg) &&
821
928
  PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
822
929
  : false;
930
+ if (historyMsg && shouldProcessHistoryMsg) {
931
+ const syncType = historyMsg.syncType;
932
+ // INITIAL_BOOTSTRAP — fire immediately, no progress check (same as WA Web K function)
933
+ if (syncType === proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP &&
934
+ !historySyncStatus.initialBootstrapComplete) {
935
+ historySyncStatus.initialBootstrapComplete = true;
936
+ ev.emit('messaging-history.status', {
937
+ syncType,
938
+ status: 'complete',
939
+ explicit: true
940
+ });
941
+ }
942
+ // RECENT with progress === 100 — explicit completion
943
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT &&
944
+ historyMsg.progress === 100 &&
945
+ !historySyncStatus.recentSyncComplete) {
946
+ historySyncStatus.recentSyncComplete = true;
947
+ clearTimeout(historySyncPausedTimeout);
948
+ historySyncPausedTimeout = undefined;
949
+ ev.emit('messaging-history.status', {
950
+ syncType,
951
+ status: 'complete',
952
+ explicit: true
953
+ });
954
+ }
955
+ // Reset 120s paused timeout on any RECENT chunk (like WA Web's handleChunkProgress)
956
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT && !historySyncStatus.recentSyncComplete) {
957
+ clearTimeout(historySyncPausedTimeout);
958
+ historySyncPausedTimeout = setTimeout(() => {
959
+ if (!historySyncStatus.recentSyncComplete) {
960
+ historySyncStatus.recentSyncComplete = true;
961
+ ev.emit('messaging-history.status', {
962
+ syncType: proto.HistorySync.HistorySyncType.RECENT,
963
+ status: 'paused',
964
+ explicit: false
965
+ });
966
+ }
967
+ historySyncPausedTimeout = undefined;
968
+ }, HISTORY_SYNC_PAUSED_TIMEOUT_MS);
969
+ }
970
+ }
823
971
  // State machine: decide on sync and flush
824
972
  if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
825
973
  if (awaitingSyncTimeout) {
@@ -839,6 +987,8 @@ export const makeChatsSocket = (config) => {
839
987
  }
840
988
  const doAppStateSync = async () => {
841
989
  if (syncState === SyncState.Syncing) {
990
+ // All collections will be synced, so clear any blocked ones
991
+ blockedCollections.clear();
842
992
  logger.info('Doing app state sync');
843
993
  await resyncAppState(ALL_WA_PATCH_NAMES, true);
844
994
  // Sync is complete, go online and flush everything
@@ -898,6 +1048,11 @@ export const makeChatsSocket = (config) => {
898
1048
  }
899
1049
  });
900
1050
  ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
1051
+ if (connection === 'close') {
1052
+ blockedCollections.clear();
1053
+ clearTimeout(historySyncPausedTimeout);
1054
+ historySyncPausedTimeout = undefined;
1055
+ }
901
1056
  if (connection === 'open') {
902
1057
  if (fireInitQueries) {
903
1058
  executeInitQueries().catch(error => onUnexpectedError(error, 'init queries'));
@@ -907,6 +1062,10 @@ export const makeChatsSocket = (config) => {
907
1062
  if (!receivedPendingNotifications || syncState !== SyncState.Connecting) {
908
1063
  return;
909
1064
  }
1065
+ historySyncStatus.initialBootstrapComplete = false;
1066
+ historySyncStatus.recentSyncComplete = false;
1067
+ clearTimeout(historySyncPausedTimeout);
1068
+ historySyncPausedTimeout = undefined;
910
1069
  syncState = SyncState.AwaitingInitialSync;
911
1070
  logger.info('Connection is now AwaitingInitialSync, buffering events');
912
1071
  ev.buffer();
@@ -919,19 +1078,49 @@ export const makeChatsSocket = (config) => {
919
1078
  setTimeout(() => ev.flush(), 0);
920
1079
  return;
921
1080
  }
922
- logger.info('History sync is enabled, awaiting notification with a 20s timeout.');
1081
+ // On reconnection (accountSyncCounter > 0), the server does not push
1082
+ // history sync notifications — the device already has its data.
1083
+ // Skip the 20s wait and go online immediately.
1084
+ if (authState.creds.accountSyncCounter > 0) {
1085
+ logger.info('Reconnection with existing sync data, skipping history sync wait. Transitioning to Online.');
1086
+ syncState = SyncState.Online;
1087
+ setTimeout(() => ev.flush(), 0);
1088
+ return;
1089
+ }
1090
+ logger.info('First connection, awaiting history sync notification with a 20s timeout.');
923
1091
  if (awaitingSyncTimeout) {
924
1092
  clearTimeout(awaitingSyncTimeout);
925
1093
  }
926
1094
  awaitingSyncTimeout = setTimeout(() => {
927
1095
  if (syncState === SyncState.AwaitingInitialSync) {
928
- // TODO: investigate
929
1096
  logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer');
930
1097
  syncState = SyncState.Online;
931
1098
  ev.flush();
1099
+ // Increment so subsequent reconnections skip the 20s wait.
1100
+ // Late-arriving history is still processed via processMessage
1101
+ // regardless of the state machine phase.
1102
+ const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1;
1103
+ ev.emit('creds.update', { accountSyncCounter });
932
1104
  }
933
1105
  }, 20000);
934
1106
  });
1107
+ // When an app state sync key arrives (myAppStateKeyId is set) and there are
1108
+ // collections blocked on a missing key, trigger a re-sync for just those collections.
1109
+ // This mirrors WA Web's Blocked → retry-on-key-arrival behavior.
1110
+ ev.on('creds.update', ({ myAppStateKeyId }) => {
1111
+ if (!myAppStateKeyId || blockedCollections.size === 0) {
1112
+ return;
1113
+ }
1114
+ // If we're in the middle of a full sync, doAppStateSync handles all collections
1115
+ if (syncState === SyncState.Syncing) {
1116
+ blockedCollections.clear();
1117
+ return;
1118
+ }
1119
+ const collections = [...blockedCollections];
1120
+ blockedCollections.clear();
1121
+ logger.info({ collections }, 'app state sync key arrived, re-syncing blocked collections');
1122
+ resyncAppState(collections, false).catch(error => onUnexpectedError(error, 'blocked collections resync'));
1123
+ });
935
1124
  ev.on('lid-mapping.update', async ({ lid, pn }) => {
936
1125
  try {
937
1126
  await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
@@ -940,8 +1129,20 @@ export const makeChatsSocket = (config) => {
940
1129
  logger.warn({ lid, pn, error }, 'Failed to store LID-PN mapping');
941
1130
  }
942
1131
  });
1132
+ registerSocketEndHandler(() => {
1133
+ if (awaitingSyncTimeout) {
1134
+ clearTimeout(awaitingSyncTimeout);
1135
+ awaitingSyncTimeout = undefined;
1136
+ }
1137
+ if (!config.placeholderResendCache && placeholderResendCache.close) {
1138
+ placeholderResendCache.close();
1139
+ }
1140
+ syncState = SyncState.Connecting;
1141
+ privacySettings = undefined;
1142
+ });
943
1143
  return {
944
1144
  ...sock,
1145
+ serverProps,
945
1146
  createCallLink,
946
1147
  getBotListV2,
947
1148
  messageMutex,
@@ -978,6 +1179,7 @@ export const makeChatsSocket = (config) => {
978
1179
  cleanDirtyBits,
979
1180
  addOrEditContact,
980
1181
  removeContact,
1182
+ placeholderResendCache,
981
1183
  addLabel,
982
1184
  addChatLabel,
983
1185
  removeChatLabel,
@@ -1,3 +1,4 @@
1
+ import { Boom } from '@hapi/boom';
1
2
  import { proto } from '../../WAProto/index.js';
2
3
  import { WAMessageAddressingMode, WAMessageStubType } from '../Types/index.js';
3
4
  import { generateMessageIDV2, unixTimestampSeconds } from '../Utils/index.js';
@@ -270,16 +271,31 @@ export const makeGroupsSocket = (config) => {
270
271
  };
271
272
  export const extractGroupMetadata = (result) => {
272
273
  const group = getBinaryNodeChild(result, 'group');
274
+ if (!group) {
275
+ // Mirror WAWeb: surface server/client errors with their code+text instead of crashing.
276
+ const errorNode = getBinaryNodeChild(result, 'error');
277
+ if (errorNode) {
278
+ const code = errorNode.attrs.code ? +errorNode.attrs.code : 500;
279
+ const text = errorNode.attrs.text || 'group metadata query failed';
280
+ throw new Boom(text, { statusCode: code, data: errorNode });
281
+ }
282
+ throw new Boom('Invalid group metadata response: missing <group> node', { data: result });
283
+ }
284
+ if (!group.attrs.id) {
285
+ throw new Boom('Invalid group metadata response: missing group id', { data: group });
286
+ }
273
287
  const descChild = getBinaryNodeChild(group, 'description');
274
288
  let desc;
275
289
  let descId;
276
290
  let descOwner;
277
291
  let descOwnerPn;
292
+ let descOwnerUsername;
278
293
  let descTime;
279
294
  if (descChild) {
280
295
  desc = getBinaryNodeChildString(descChild, 'body');
281
296
  descOwner = descChild.attrs.participant ? jidNormalizedUser(descChild.attrs.participant) : undefined;
282
297
  descOwnerPn = descChild.attrs.participant_pn ? jidNormalizedUser(descChild.attrs.participant_pn) : undefined;
298
+ descOwnerUsername = descChild.attrs.participant_username || undefined;
283
299
  descTime = +descChild.attrs.t;
284
300
  descId = descChild.attrs.id;
285
301
  }
@@ -293,16 +309,19 @@ export const extractGroupMetadata = (result) => {
293
309
  subject: group.attrs.subject,
294
310
  subjectOwner: group.attrs.s_o,
295
311
  subjectOwnerPn: group.attrs.s_o_pn,
312
+ subjectOwnerUsername: group.attrs.s_o_username,
296
313
  subjectTime: +group.attrs.s_t,
297
314
  size: group.attrs.size ? +group.attrs.size : getBinaryNodeChildren(group, 'participant').length,
298
315
  creation: +group.attrs.creation,
299
316
  owner: group.attrs.creator ? jidNormalizedUser(group.attrs.creator) : undefined,
300
317
  ownerPn: group.attrs.creator_pn ? jidNormalizedUser(group.attrs.creator_pn) : undefined,
318
+ ownerUsername: group.attrs.creator_username || undefined,
301
319
  owner_country_code: group.attrs.creator_country_code,
302
320
  desc,
303
321
  descId,
304
322
  descOwner,
305
323
  descOwnerPn,
324
+ descOwnerUsername,
306
325
  descTime,
307
326
  linkedParent: getBinaryNodeChild(group, 'linked_parent')?.attrs.jid || undefined,
308
327
  restrict: !!getBinaryNodeChild(group, 'locked'),
@@ -317,6 +336,7 @@ export const extractGroupMetadata = (result) => {
317
336
  id: attrs.jid,
318
337
  phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
319
338
  lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
339
+ username: attrs.participant_username || attrs.username || undefined,
320
340
  admin: (attrs.type || null)
321
341
  };
322
342
  }),