@itsliaaa/baileys 0.1.32 → 0.2.0

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.
package/WAProto/index.js CHANGED
@@ -12,14 +12,18 @@ function longToString(value, unsigned) {
12
12
  if (typeof value === "number") {
13
13
  return String(value);
14
14
  }
15
- if (!$util.Long) {
16
- return String(value);
15
+ // Fast path: convert Long {low, high} directly via native BigInt
16
+ // BigInt.toString() is a native C++ operation, much faster than Long's pure JS division loops
17
+ if (value && typeof value.low === "number" && typeof value.high === "number") {
18
+ const lo = BigInt(value.low >>> 0);
19
+ const hi = BigInt(value.high >>> 0);
20
+ const combined = (hi << 32n) | lo;
21
+ if (!unsigned && value.high < 0) {
22
+ return (combined - (1n << 64n)).toString();
23
+ }
24
+ return combined.toString();
17
25
  }
18
- const normalized = $util.Long.fromValue(value);
19
- const prepared = unsigned && normalized && typeof normalized.toUnsigned === "function"
20
- ? normalized.toUnsigned()
21
- : normalized;
22
- return prepared.toString();
26
+ return String(value);
23
27
  }
24
28
 
25
29
  function longToNumber(value, unsigned) {
@@ -27,19 +31,19 @@ function longToNumber(value, unsigned) {
27
31
  return value;
28
32
  }
29
33
  if (typeof value === "string") {
30
- const numeric = Number(value);
31
- return numeric;
32
- }
33
- if (!$util.Long) {
34
34
  return Number(value);
35
35
  }
36
- const normalized = $util.Long.fromValue(value);
37
- const prepared = unsigned && normalized && typeof normalized.toUnsigned === "function"
38
- ? normalized.toUnsigned()
39
- : typeof normalized.toSigned === "function"
40
- ? normalized.toSigned()
41
- : normalized;
42
- return prepared.toNumber();
36
+ // Fast path: convert Long {low, high} directly via native BigInt
37
+ if (value && typeof value.low === "number" && typeof value.high === "number") {
38
+ const lo = BigInt(value.low >>> 0);
39
+ const hi = BigInt(value.high >>> 0);
40
+ const combined = (hi << 32n) | lo;
41
+ if (!unsigned && value.high < 0) {
42
+ return Number(combined - (1n << 64n));
43
+ }
44
+ return Number(combined);
45
+ }
46
+ return Number(value);
43
47
  }
44
48
 
45
49
  export const proto = $root.proto = (() => {
@@ -126,6 +126,8 @@ export const MEDIA_HKDF_KEY_MAPPING = {
126
126
  'biz-cover-photo': 'Image'
127
127
  };
128
128
  export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP);
129
+ /** 120s timeout for history sync stall detection, same as WA Web's handleChunkProgress / restartPausedTimer (g = 120) */
130
+ export const HISTORY_SYNC_PAUSED_TIMEOUT_MS = 120_000;
129
131
  export const MIN_PREKEY_COUNT = 5;
130
132
  export const INITIAL_PREKEY_COUNT = 812;
131
133
  export const UPLOAD_TIMEOUT = 30000; // 30 seconds
@@ -1,24 +1,33 @@
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, isLidUser, isPnUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
11
+ import { getBinaryNodeChild, getBinaryNodeChildren, isPnUser, isLidUser, isHostedLidUser, isHostedPnUser, 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
  // Lia@Note 08-02-26 --- I know it's not efficient for RSS ಥ⁠‿⁠ಥ
16
15
  const USER_ID_CACHE = new Map();
17
16
  export const makeChatsSocket = (config) => {
18
17
  const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
19
18
  const sock = makeSocket(config);
20
19
  const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession, registerSocketEndHandler } = sock;
20
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
21
21
  let privacySettings;
22
+ /** Server-assigned AB props for protocol behavior. */
23
+ const serverProps = {
24
+ /** AB prop 10518: gate tctoken on 1:1 messages. Default true (safe: avoids 463). */
25
+ privacyTokenOn1to1: true,
26
+ /** AB prop 9666: gate tctoken on profile picture IQs. WA Web default: true. */
27
+ profilePicPrivacyToken: true,
28
+ /** AB prop 14303: issue tctokens to LID instead of PN. WA Web default: false. */
29
+ lidTrustedTokenIssueToLid: false
30
+ };
22
31
  let syncState = SyncState.Connecting;
23
32
  /** this mutex ensures that messages are processed in order */
24
33
  const messageMutex = makeMutex();
@@ -30,6 +39,15 @@ export const makeChatsSocket = (config) => {
30
39
  const notificationMutex = makeMutex();
31
40
  // Timeout for AwaitingInitialSync state
32
41
  let awaitingSyncTimeout;
42
+ // In-memory history sync completion tracking (resets on reconnection)
43
+ const historySyncStatus = {
44
+ initialBootstrapComplete: false,
45
+ recentSyncComplete: false
46
+ };
47
+ let historySyncPausedTimeout;
48
+ // Collections blocked on missing app state sync keys (mirrors WA Web's "Blocked" state).
49
+ // When a key arrives via APP_STATE_SYNC_KEY_SHARE, these are re-synced.
50
+ const blockedCollections = new Set();
33
51
  const placeholderResendCache = config.placeholderResendCache ||
34
52
  new NodeCache({
35
53
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
@@ -293,6 +311,42 @@ export const makeChatsSocket = (config) => {
293
311
  return getBinaryNodeChildren(listNode, 'item').map(n => n.attrs.jid);
294
312
  };
295
313
  const updateBlockStatus = async (jid, action) => {
314
+ const normalizedJid = jidNormalizedUser(jid);
315
+ let lid;
316
+ let pn_jid;
317
+ if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
318
+ lid = normalizedJid;
319
+ if (action === 'block') {
320
+ const pn = (await findUserId(normalizedJid)).phoneNumber;
321
+ if (pn.startsWith('id')) {
322
+ throw new Boom(`Unable to resolve PN JID for LID: ${jid}`, { statusCode: 400 });
323
+ }
324
+ pn_jid = jidNormalizedUser(pn);
325
+ }
326
+ }
327
+ else if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
328
+ const mapped = (await findUserId(normalizedJid)).lid;
329
+ if (mapped.startsWith('id')) {
330
+ throw new Boom(`Unable to resolve LID for PN JID: ${jid}`, { statusCode: 400 });
331
+ }
332
+ lid = mapped;
333
+ if (action === 'block') {
334
+ pn_jid = jidNormalizedUser(normalizedJid);
335
+ }
336
+ }
337
+ else {
338
+ throw new Boom(`Invalid jid: ${jid}`, { statusCode: 400 });
339
+ }
340
+ const itemAttrs = {
341
+ action,
342
+ jid: lid
343
+ };
344
+ if (action === 'block') {
345
+ if (!pn_jid) {
346
+ throw new Boom(`pn_jid required for block: ${jid}`, { statusCode: 400 });
347
+ }
348
+ itemAttrs.pn_jid = pn_jid;
349
+ }
296
350
  await query({
297
351
  tag: 'iq',
298
352
  attrs: {
@@ -303,10 +357,7 @@ export const makeChatsSocket = (config) => {
303
357
  content: [
304
358
  {
305
359
  tag: 'item',
306
- attrs: {
307
- action,
308
- jid
309
- }
360
+ attrs: itemAttrs
310
361
  }
311
362
  ]
312
363
  });
@@ -405,6 +456,9 @@ export const makeChatsSocket = (config) => {
405
456
  const collectionsToHandle = new Set(collections);
406
457
  // in case something goes wrong -- ensure we don't enter a loop that cannot be exited from
407
458
  const attemptsMap = {};
459
+ // collections that failed and need a full snapshot on retry
460
+ // mirrors WA Web's ErrorFatal -> force snapshot behavior
461
+ const forceSnapshotCollections = new Set();
408
462
  // keep executing till all collections are done
409
463
  // sometimes a single patch request will not return all the patches (God knows why)
410
464
  // so we fetch till they're all done (this is determined by the "has_more_patches" flag)
@@ -415,6 +469,7 @@ export const makeChatsSocket = (config) => {
415
469
  const result = await authState.keys.get('app-state-sync-version', [name]);
416
470
  let state = result[name];
417
471
  if (state) {
472
+ state = ensureLTHashStateVersion(state);
418
473
  if (typeof initialVersionMap[name] === 'undefined') {
419
474
  initialVersionMap[name] = state.version;
420
475
  }
@@ -423,14 +478,18 @@ export const makeChatsSocket = (config) => {
423
478
  state = newLTHashState();
424
479
  }
425
480
  states[name] = state;
426
- logger.info(`resyncing ${name} from v${state.version}`);
481
+ const shouldForceSnapshot = forceSnapshotCollections.has(name);
482
+ if (shouldForceSnapshot) {
483
+ forceSnapshotCollections.delete(name);
484
+ }
485
+ logger.info(`resyncing ${name} from v${state.version}${shouldForceSnapshot ? ' (forcing snapshot)' : ''}`);
427
486
  nodes.push({
428
487
  tag: 'collection',
429
488
  attrs: {
430
489
  name,
431
490
  version: state.version.toString(),
432
- // return snapshot if being synced from scratch
433
- return_snapshot: (!state.version).toString()
491
+ // return snapshot if syncing from scratch or forcing after a failed attempt
492
+ return_snapshot: (shouldForceSnapshot || !state.version).toString()
434
493
  }
435
494
  });
436
495
  }
@@ -479,19 +538,37 @@ export const makeChatsSocket = (config) => {
479
538
  }
480
539
  }
481
540
  catch (error) {
482
- // if retry attempts overshoot
483
- // or key not found
484
- const isIrrecoverableError = attemptsMap[name] >= MAX_SYNC_ATTEMPTS ||
485
- error.output?.statusCode === 404 ||
486
- error.name === 'TypeError';
487
- logger.info({ name, error: error.stack }, `failed to sync state from version${isIrrecoverableError ? '' : ', removing and trying from scratch'}`);
488
- await authState.keys.set({ 'app-state-sync-version': { [name]: null } });
489
- // increment number of retries
490
541
  attemptsMap[name] = (attemptsMap[name] || 0) + 1;
491
- if (isIrrecoverableError) {
492
- // stop retrying
542
+ const logData = {
543
+ name,
544
+ attempt: attemptsMap[name],
545
+ version: states[name].version,
546
+ statusCode: error.output?.statusCode,
547
+ errorType: error.name,
548
+ error: error.stack
549
+ };
550
+ if (isMissingKeyError(error) && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
551
+ // WA Web treats missing keys as "Blocked" — park the collection
552
+ // until the key arrives via APP_STATE_SYNC_KEY_SHARE.
553
+ logger.warn(logData, `${name} blocked on missing key from v${states[name].version}, parking after ${attemptsMap[name]} attempts`);
554
+ blockedCollections.add(name);
555
+ collectionsToHandle.delete(name);
556
+ }
557
+ else if (isMissingKeyError(error)) {
558
+ // Retry with a snapshot which may use a different key.
559
+ logger.info(logData, `${name} blocked on missing key from v${states[name].version}, retrying with snapshot`);
560
+ forceSnapshotCollections.add(name);
561
+ }
562
+ else if (isAppStateSyncIrrecoverable(error, attemptsMap[name])) {
563
+ logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`);
493
564
  collectionsToHandle.delete(name);
494
565
  }
566
+ else {
567
+ logger.info(logData, `failed to sync ${name} from v${states[name].version}, forcing snapshot retry`);
568
+ // force a full snapshot on retry to recover from
569
+ // corrupted local state (e.g. LTHash MAC mismatch)
570
+ forceSnapshotCollections.add(name);
571
+ }
495
572
  }
496
573
  }
497
574
  }
@@ -507,42 +584,34 @@ export const makeChatsSocket = (config) => {
507
584
  * type = "image for the high res picture"
508
585
  */
509
586
  const profilePictureUrl = async (jid, type = 'image', timeoutMs) => {
510
- // Lia@Changes 06-02-26 --- Refactor profilePictureUrl() to use tctoken and adjust error handling
511
- jid = jidNormalizedUser(jid);
512
- const baseContent = {
513
- tag: 'picture',
514
- attrs: {
515
- type,
516
- query: 'url'
517
- }
518
- };
519
- const tcTokenData = await authState.keys.get('tctoken', [jid]);
520
- const tcTokenBuffer = tcTokenData?.[jid]?.token
521
- if (tcTokenBuffer) {
522
- baseContent.content = [{
523
- tag: 'tctoken',
524
- attrs: {},
525
- content: tcTokenBuffer
526
- }];
587
+ const baseContent = [{ tag: 'picture', attrs: { type, query: 'url' } }];
588
+ // WA Web only includes tctoken for user JIDs (not groups/newsletters)
589
+ // and never for own profile pic (Chat model for self has no tcToken).
590
+ // Including tctoken for own JID causes the server to never respond.
591
+ const normalizedJid = jidNormalizedUser(jid);
592
+ const isUserJid = isPnUser(normalizedJid) || isLidUser(normalizedJid);
593
+ const me = authState.creds.me;
594
+ const isSelf = me && (normalizedJid === jidNormalizedUser(me.id) || (me.lid && normalizedJid === jidNormalizedUser(me.lid)));
595
+ let content = baseContent;
596
+ if (serverProps.profilePicPrivacyToken && isUserJid && !isSelf) {
597
+ content = await buildTcTokenFromJid({
598
+ authState,
599
+ jid: normalizedJid,
600
+ baseContent,
601
+ getLIDForPN
602
+ });
527
603
  }
528
604
  const result = await query({
529
605
  tag: 'iq',
530
606
  attrs: {
531
- target: jid,
607
+ target: normalizedJid,
532
608
  to: S_WHATSAPP_NET,
533
609
  type: 'get',
534
610
  xmlns: 'w:profile:picture'
535
611
  },
536
- content: [baseContent]
612
+ content
537
613
  }, timeoutMs);
538
614
  const child = getBinaryNodeChild(result, 'picture');
539
- if (!child) {
540
- throw new Boom('Picture node missing', { statusCode: 404 });
541
- }
542
- const status = child.attrs?.status;
543
- if (status === '404' || status === '204') {
544
- throw new Boom('Profile picture not set', { statusCode: 404 });
545
- }
546
615
  return child?.attrs?.url;
547
616
  };
548
617
  const createCallLink = async (type, event, timeoutMs) => {
@@ -606,7 +675,12 @@ export const makeChatsSocket = (config) => {
606
675
  * @param tcToken token for subscription, use if present
607
676
  */
608
677
  const presenceSubscribe = async (toJid) => {
609
- const tcTokenContent = await buildTcTokenFromJid({ authState, jid: toJid });
678
+ // Only include tctoken for user JIDs groups/newsletters don't use tctokens
679
+ const normalizedToJid = jidNormalizedUser(toJid);
680
+ const isUserJid = isPnUser(normalizedToJid) || isLidUser(normalizedToJid);
681
+ const tcTokenContent = isUserJid
682
+ ? await buildTcTokenFromJid({ authState, jid: normalizedToJid, getLIDForPN })
683
+ : undefined;
610
684
  return sendNode({
611
685
  tag: 'presence',
612
686
  attrs: {
@@ -661,7 +735,7 @@ export const makeChatsSocket = (config) => {
661
735
  logger.debug({ patch: patchCreate }, 'applying app patch');
662
736
  await resyncAppState([name], false);
663
737
  const { [name]: currentSyncVersion } = await authState.keys.get('app-state-sync-version', [name]);
664
- initial = currentSyncVersion || newLTHashState();
738
+ initial = currentSyncVersion ? ensureLTHashStateVersion(currentSyncVersion) : newLTHashState();
665
739
  encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey);
666
740
  const { patch, state } = encodeResult;
667
741
  const node = {
@@ -707,22 +781,21 @@ export const makeChatsSocket = (config) => {
707
781
  }
708
782
  }
709
783
  };
710
- /** sending non-abt props may fix QR scan fail if server expects */
784
+ /** fetch AB props */
711
785
  const fetchProps = async () => {
712
- //TODO: implement both protocol 1 and protocol 2 prop fetching, specially for abKey for WM
713
786
  const resultNode = await query({
714
787
  tag: 'iq',
715
788
  attrs: {
716
789
  to: S_WHATSAPP_NET,
717
- xmlns: 'w',
790
+ xmlns: 'abt',
718
791
  type: 'get'
719
792
  },
720
793
  content: [
721
794
  {
722
795
  tag: 'props',
723
796
  attrs: {
724
- protocol: '2',
725
- hash: authState?.creds?.lastPropHash || ''
797
+ protocol: '1',
798
+ ...(authState?.creds?.lastPropHash ? { hash: authState.creds.lastPropHash } : {})
726
799
  }
727
800
  }
728
801
  ]
@@ -737,7 +810,20 @@ export const makeChatsSocket = (config) => {
737
810
  }
738
811
  props = reduceBinaryNodeToDictionary(propsNode, 'prop');
739
812
  }
740
- logger.debug('fetched props');
813
+ // Extract protocol-relevant AB props (only the ones we need)
814
+ const privacyTokenProp = props['10518'] ?? props['privacy_token_sending_on_all_1_on_1_messages'];
815
+ if (privacyTokenProp !== undefined) {
816
+ serverProps.privacyTokenOn1to1 = privacyTokenProp === 'true' || privacyTokenProp === '1';
817
+ }
818
+ const profilePicProp = props['9666'] ?? props['profile_scraping_privacy_token_in_photo_iq'];
819
+ if (profilePicProp !== undefined) {
820
+ serverProps.profilePicPrivacyToken = profilePicProp === 'true' || profilePicProp === '1';
821
+ }
822
+ const lidIssueProp = props['14303'] ?? props['lid_trusted_token_issue_to_lid'];
823
+ if (lidIssueProp !== undefined) {
824
+ serverProps.lidTrustedTokenIssueToLid = lidIssueProp === 'true' || lidIssueProp === '1';
825
+ }
826
+ logger.debug({ serverProps }, 'fetched props');
741
827
  return props;
742
828
  };
743
829
  /**
@@ -877,6 +963,44 @@ export const makeChatsSocket = (config) => {
877
963
  ? shouldSyncHistoryMessage(historyMsg) &&
878
964
  PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
879
965
  : false;
966
+ if (historyMsg && shouldProcessHistoryMsg) {
967
+ const syncType = historyMsg.syncType;
968
+ // INITIAL_BOOTSTRAP — fire immediately, no progress check (same as WA Web K function)
969
+ if (syncType === proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP && !historySyncStatus.initialBootstrapComplete) {
970
+ historySyncStatus.initialBootstrapComplete = true;
971
+ ev.emit('messaging-history.status', {
972
+ syncType,
973
+ status: 'complete',
974
+ explicit: true
975
+ });
976
+ }
977
+ // RECENT with progress === 100 — explicit completion
978
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT && historyMsg.progress === 100 && !historySyncStatus.recentSyncComplete) {
979
+ historySyncStatus.recentSyncComplete = true;
980
+ clearTimeout(historySyncPausedTimeout);
981
+ historySyncPausedTimeout = undefined;
982
+ ev.emit('messaging-history.status', {
983
+ syncType,
984
+ status: 'complete',
985
+ explicit: true
986
+ });
987
+ }
988
+ // Reset 120s paused timeout on any RECENT chunk (like WA Web's handleChunkProgress)
989
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT && !historySyncStatus.recentSyncComplete) {
990
+ clearTimeout(historySyncPausedTimeout);
991
+ historySyncPausedTimeout = setTimeout(() => {
992
+ if (!historySyncStatus.recentSyncComplete) {
993
+ historySyncStatus.recentSyncComplete = true;
994
+ ev.emit('messaging-history.status', {
995
+ syncType: proto.HistorySync.HistorySyncType.RECENT,
996
+ status: 'paused',
997
+ explicit: false
998
+ });
999
+ }
1000
+ historySyncPausedTimeout = undefined;
1001
+ }, HISTORY_SYNC_PAUSED_TIMEOUT_MS);
1002
+ }
1003
+ }
880
1004
  // State machine: decide on sync and flush
881
1005
  if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
882
1006
  if (awaitingSyncTimeout) {
@@ -896,6 +1020,8 @@ export const makeChatsSocket = (config) => {
896
1020
  }
897
1021
  const doAppStateSync = async () => {
898
1022
  if (syncState === SyncState.Syncing) {
1023
+ // All collections will be synced, so clear any blocked ones
1024
+ blockedCollections.clear();
899
1025
  logger.info('Doing app state sync');
900
1026
  await resyncAppState(ALL_WA_PATCH_NAMES, true);
901
1027
  // Sync is complete, go online and flush everything
@@ -955,6 +1081,11 @@ export const makeChatsSocket = (config) => {
955
1081
  }
956
1082
  });
957
1083
  ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
1084
+ if (connection === 'close') {
1085
+ blockedCollections.clear();
1086
+ clearTimeout(historySyncPausedTimeout);
1087
+ historySyncPausedTimeout = undefined;
1088
+ }
958
1089
  if (connection === 'open') {
959
1090
  if (fireInitQueries) {
960
1091
  executeInitQueries().catch(error => onUnexpectedError(error, 'init queries'));
@@ -964,6 +1095,10 @@ export const makeChatsSocket = (config) => {
964
1095
  if (!receivedPendingNotifications || syncState !== SyncState.Connecting) {
965
1096
  return;
966
1097
  }
1098
+ historySyncStatus.initialBootstrapComplete = false;
1099
+ historySyncStatus.recentSyncComplete = false;
1100
+ clearTimeout(historySyncPausedTimeout);
1101
+ historySyncPausedTimeout = undefined;
967
1102
  syncState = SyncState.AwaitingInitialSync;
968
1103
  logger.info('Connection is now AwaitingInitialSync, buffering events');
969
1104
  ev.buffer();
@@ -976,19 +1111,49 @@ export const makeChatsSocket = (config) => {
976
1111
  setTimeout(() => ev.flush(), 0);
977
1112
  return;
978
1113
  }
979
- logger.info('History sync is enabled, awaiting notification with a 20s timeout.');
1114
+ // On reconnection (accountSyncCounter > 0), the server does not push
1115
+ // history sync notifications — the device already has its data.
1116
+ // Skip the 20s wait and go online immediately.
1117
+ if (authState.creds.accountSyncCounter > 0) {
1118
+ logger.info('Reconnection with existing sync data, skipping history sync wait. Transitioning to Online.');
1119
+ syncState = SyncState.Online;
1120
+ setTimeout(() => ev.flush(), 0);
1121
+ return;
1122
+ }
1123
+ logger.info('First connection, awaiting history sync notification with a 20s timeout.');
980
1124
  if (awaitingSyncTimeout) {
981
1125
  clearTimeout(awaitingSyncTimeout);
982
1126
  }
983
1127
  awaitingSyncTimeout = setTimeout(() => {
984
1128
  if (syncState === SyncState.AwaitingInitialSync) {
985
- // TODO: investigate
986
1129
  logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer');
987
1130
  syncState = SyncState.Online;
988
1131
  ev.flush();
1132
+ // Increment so subsequent reconnections skip the 20s wait.
1133
+ // Late-arriving history is still processed via processMessage
1134
+ // regardless of the state machine phase.
1135
+ const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1;
1136
+ ev.emit('creds.update', { accountSyncCounter });
989
1137
  }
990
1138
  }, 20000);
991
1139
  });
1140
+ // When an app state sync key arrives (myAppStateKeyId is set) and there are
1141
+ // collections blocked on a missing key, trigger a re-sync for just those collections.
1142
+ // This mirrors WA Web's Blocked → retry-on-key-arrival behavior.
1143
+ ev.on('creds.update', ({ myAppStateKeyId }) => {
1144
+ if (!myAppStateKeyId || blockedCollections.size === 0) {
1145
+ return;
1146
+ }
1147
+ // If we're in the middle of a full sync, doAppStateSync handles all collections
1148
+ if (syncState === SyncState.Syncing) {
1149
+ blockedCollections.clear();
1150
+ return;
1151
+ }
1152
+ const collections = [...blockedCollections];
1153
+ blockedCollections.clear();
1154
+ logger.info({ collections }, 'app state sync key arrived, re-syncing blocked collections');
1155
+ resyncAppState(collections, false).catch(error => onUnexpectedError(error, 'blocked collections resync'));
1156
+ });
992
1157
  ev.on('lid-mapping.update', async ({ lid, pn }) => {
993
1158
  try {
994
1159
  await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
@@ -1010,6 +1175,7 @@ export const makeChatsSocket = (config) => {
1010
1175
  });
1011
1176
  return {
1012
1177
  ...sock,
1178
+ serverProps,
1013
1179
  createCallLink,
1014
1180
  getBotListV2,
1015
1181
  messageMutex,
@@ -277,11 +277,13 @@ export const extractGroupMetadata = (result) => {
277
277
  let descId;
278
278
  let descOwner;
279
279
  let descOwnerPn;
280
+ let descOwnerUsername;
280
281
  let descTime;
281
282
  if (descChild) {
282
283
  desc = getBinaryNodeChildString(descChild, 'body');
283
284
  descOwner = descChild.attrs.participant ? jidNormalizedUser(descChild.attrs.participant) : undefined;
284
285
  descOwnerPn = descChild.attrs.participant_pn ? jidNormalizedUser(descChild.attrs.participant_pn) : undefined;
286
+ descOwnerUsername = descChild.attrs.participant_username || undefined;
285
287
  descTime = +descChild.attrs.t;
286
288
  descId = descChild.attrs.id;
287
289
  }
@@ -295,16 +297,19 @@ export const extractGroupMetadata = (result) => {
295
297
  subject: group.attrs.subject,
296
298
  subjectOwner: group.attrs.s_o,
297
299
  subjectOwnerPn: group.attrs.s_o_pn,
300
+ subjectOwnerUsername: group.attrs.s_o_username,
298
301
  subjectTime: +group.attrs.s_t,
299
302
  size: group.attrs.size ? +group.attrs.size : getBinaryNodeChildren(group, 'participant').length,
300
303
  creation: +group.attrs.creation,
301
304
  owner: group.attrs.creator ? jidNormalizedUser(group.attrs.creator) : undefined,
302
305
  ownerPn: group.attrs.creator_pn ? jidNormalizedUser(group.attrs.creator_pn) : undefined,
306
+ ownerUsername: group.attrs.creator_username || undefined,
303
307
  owner_country_code: group.attrs.creator_country_code,
304
308
  desc,
305
309
  descId,
306
310
  descOwner,
307
311
  descOwnerPn,
312
+ descOwnerUsername,
308
313
  descTime,
309
314
  linkedParent: getBinaryNodeChild(group, 'linked_parent')?.attrs.jid || undefined,
310
315
  restrict: !!getBinaryNodeChild(group, 'locked'),
@@ -319,6 +324,7 @@ export const extractGroupMetadata = (result) => {
319
324
  id: attrs.jid,
320
325
  phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
321
326
  lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
327
+ username: attrs.participant_username || attrs.username || undefined,
322
328
  admin: (attrs.type || null)
323
329
  };
324
330
  }),