@crysnovax/baileys 2.5.6 → 2.6.1

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.
@@ -1,24 +1,32 @@
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
+ import { getStream, toBuffer } from '../Utils/messages-media.js';
8
9
  import { makeMutex } from '../Utils/make-mutex.js';
9
10
  import processMessage from '../Utils/process-message.js';
10
11
  import { buildTcTokenFromJid } from '../Utils/tc-token-utils.js';
11
- import { getBinaryNodeChild, getBinaryNodeChildren, isLidUser, isPnUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
12
+ import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
12
13
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
13
14
  import { makeSocket } from './socket.js';
14
- const MAX_SYNC_ATTEMPTS = 2;
15
- // Lia@Note 08-02-26 --- I know it's not efficient for RSS ಥ⁠‿⁠ಥ
16
- const USER_ID_CACHE = new Map();
17
15
  export const makeChatsSocket = (config) => {
18
16
  const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
19
17
  const sock = makeSocket(config);
20
18
  const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession, registerSocketEndHandler } = sock;
19
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
21
20
  let privacySettings;
21
+ /** Server-assigned AB props for protocol behavior. */
22
+ const serverProps = {
23
+ /** AB prop 10518: gate tctoken on 1:1 messages. Default true (safe: avoids 463). */
24
+ privacyTokenOn1to1: true,
25
+ /** AB prop 9666: gate tctoken on profile picture IQs. WA Web default: true. */
26
+ profilePicPrivacyToken: true,
27
+ /** AB prop 14303: issue tctokens to LID instead of PN. WA Web default: false. */
28
+ lidTrustedTokenIssueToLid: false
29
+ };
22
30
  let syncState = SyncState.Connecting;
23
31
  /** this mutex ensures that messages are processed in order */
24
32
  const messageMutex = makeMutex();
@@ -30,14 +38,20 @@ export const makeChatsSocket = (config) => {
30
38
  const notificationMutex = makeMutex();
31
39
  // Timeout for AwaitingInitialSync state
32
40
  let awaitingSyncTimeout;
41
+ // In-memory history sync completion tracking (resets on reconnection)
42
+ const historySyncStatus = {
43
+ initialBootstrapComplete: false,
44
+ recentSyncComplete: false
45
+ };
46
+ let historySyncPausedTimeout;
47
+ // Collections blocked on missing app state sync keys (mirrors WA Web's "Blocked" state).
48
+ // When a key arrives via APP_STATE_SYNC_KEY_SHARE, these are re-synced.
49
+ const blockedCollections = new Set();
33
50
  const placeholderResendCache = config.placeholderResendCache ||
34
51
  new NodeCache({
35
52
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
36
53
  useClones: false
37
54
  });
38
- // if (!config.placeholderResendCache) {
39
- // config.placeholderResendCache = placeholderResendCache;
40
- // }
41
55
  /** helper function to fetch the given app state sync key */
42
56
  const getAppStateSyncKey = async (keyId) => {
43
57
  const { [keyId]: key } = await authState.keys.get('app-state-sync-key', [keyId]);
@@ -176,38 +190,33 @@ export const makeChatsSocket = (config) => {
176
190
  };
177
191
  // Lia@Note 06-02-26 --- Quick helper only. This function exists solely for faster lookup with caching.
178
192
  const findUserId = async (pnLid) => {
179
- const cachedId = USER_ID_CACHE.get(pnLid);
180
- if (cachedId) {
181
- return cachedId;
182
- }
183
- const userId = {};
184
- if (isPnUser(pnLid)) {
185
- userId.phoneNumber = pnLid;
186
- userId.lid = (await signalRepository.lidMapping.getLIDsForPNs([pnLid]))?.[0]?.lid;
187
- if (!userId.lid) {
188
- userId.lid = 'id-not-found';
189
- return userId; // Lia@Note 06-02-26 --- Early return to skip caching for non-existent ID
190
- }
193
+ const normalizedJid = jidNormalizedUser(pnLid);
194
+ const userId = {
195
+ lid: undefined,
196
+ phoneNumber: undefined
197
+ };
198
+ if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
199
+ userId.phoneNumber = normalizedJid;
200
+ userId.lid = jidNormalizedUser((await signalRepository.lidMapping.getLIDsForPNs([normalizedJid]))?.[0]?.lid);
191
201
  }
192
- else if (isLidUser(pnLid)) {
193
- userId.lid = pnLid;
194
- userId.phoneNumber = (await signalRepository.lidMapping.getPNsForLIDs([pnLid]))?.[0]?.pn;
195
- if (!userId.phoneNumber) {
196
- userId.phoneNumber = 'id-not-found';
197
- return userId; // Lia@Note 06-02-26 --- Early return to skip caching for non-existent ID
198
- }
202
+ else if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
203
+ userId.lid = normalizedJid;
204
+ userId.phoneNumber = jidNormalizedUser((await signalRepository.lidMapping.getPNsForLIDs([normalizedJid]))?.[0]?.pn);
199
205
  }
200
206
  else {
201
207
  throw new Boom('Invalid id input to find user ids', { statusCode: 400 });
202
208
  }
203
- userId.phoneNumber = jidNormalizedUser(userId.phoneNumber);
204
- userId.lid = jidNormalizedUser(userId.lid);
205
- // Lia@Note 06-02-26 --- I know... it's dirty (⁠╯⁠︵⁠╰⁠,⁠)
206
- USER_ID_CACHE.set(userId.phoneNumber, userId);
207
- USER_ID_CACHE.set(userId.lid, userId);
208
209
  return userId;
209
210
  };
210
- /** update the profile picture for yourself or a group */
211
+ /** update the profile picture for yourself or a group
212
+ *
213
+ * Standard (crop + resize to 720×720):
214
+ * sock.updateProfilePicture(jid, content)
215
+ * sock.updateProfilePicture(jid, content, { width: 640, height: 640 })
216
+ *
217
+ * HD (full-size, no crop, no resize):
218
+ * sock.updateProfilePicture(jid, content, { hd: true })
219
+ */
211
220
  const updateProfilePicture = async (jid, content, dimensions) => {
212
221
  let targetJid;
213
222
  if (!jid) {
@@ -219,7 +228,18 @@ export const makeChatsSocket = (config) => {
219
228
  else {
220
229
  targetJid = undefined;
221
230
  }
222
- const { img } = await generateProfilePicture(content, dimensions);
231
+ let img;
232
+ if (dimensions?.hd) {
233
+ // crysnovax@HD-Profile --- bypass resize/crop, send raw buffer directly
234
+ if (Buffer.isBuffer(content)) {
235
+ img = content;
236
+ } else {
237
+ const { stream } = await getStream(content);
238
+ img = await toBuffer(stream);
239
+ }
240
+ } else {
241
+ ({ img } = await generateProfilePicture(content, dimensions));
242
+ }
223
243
  await query({
224
244
  tag: 'iq',
225
245
  attrs: {
@@ -293,6 +313,42 @@ export const makeChatsSocket = (config) => {
293
313
  return getBinaryNodeChildren(listNode, 'item').map(n => n.attrs.jid);
294
314
  };
295
315
  const updateBlockStatus = async (jid, action) => {
316
+ const normalizedJid = jidNormalizedUser(jid);
317
+ let lid;
318
+ let pn_jid;
319
+ if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
320
+ lid = normalizedJid;
321
+ if (action === 'block') {
322
+ const pn = (await findUserId(normalizedJid)).phoneNumber;
323
+ if (!pn) {
324
+ throw new Boom(`Unable to resolve PN JID for LID: ${jid}`, { statusCode: 400 });
325
+ }
326
+ pn_jid = jidNormalizedUser(pn);
327
+ }
328
+ }
329
+ else if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
330
+ const mapped = (await findUserId(normalizedJid)).lid;
331
+ if (!mapped) {
332
+ throw new Boom(`Unable to resolve LID for PN JID: ${jid}`, { statusCode: 400 });
333
+ }
334
+ lid = mapped;
335
+ if (action === 'block') {
336
+ pn_jid = jidNormalizedUser(normalizedJid);
337
+ }
338
+ }
339
+ else {
340
+ throw new Boom(`Invalid jid: ${jid}`, { statusCode: 400 });
341
+ }
342
+ const itemAttrs = {
343
+ action,
344
+ jid: lid
345
+ };
346
+ if (action === 'block') {
347
+ if (!pn_jid) {
348
+ throw new Boom(`pn_jid required for block: ${jid}`, { statusCode: 400 });
349
+ }
350
+ itemAttrs.pn_jid = pn_jid;
351
+ }
296
352
  await query({
297
353
  tag: 'iq',
298
354
  attrs: {
@@ -303,10 +359,7 @@ export const makeChatsSocket = (config) => {
303
359
  content: [
304
360
  {
305
361
  tag: 'item',
306
- attrs: {
307
- action,
308
- jid
309
- }
362
+ attrs: itemAttrs
310
363
  }
311
364
  ]
312
365
  });
@@ -405,6 +458,9 @@ export const makeChatsSocket = (config) => {
405
458
  const collectionsToHandle = new Set(collections);
406
459
  // in case something goes wrong -- ensure we don't enter a loop that cannot be exited from
407
460
  const attemptsMap = {};
461
+ // collections that failed and need a full snapshot on retry
462
+ // mirrors WA Web's ErrorFatal -> force snapshot behavior
463
+ const forceSnapshotCollections = new Set();
408
464
  // keep executing till all collections are done
409
465
  // sometimes a single patch request will not return all the patches (God knows why)
410
466
  // so we fetch till they're all done (this is determined by the "has_more_patches" flag)
@@ -415,6 +471,7 @@ export const makeChatsSocket = (config) => {
415
471
  const result = await authState.keys.get('app-state-sync-version', [name]);
416
472
  let state = result[name];
417
473
  if (state) {
474
+ state = ensureLTHashStateVersion(state);
418
475
  if (typeof initialVersionMap[name] === 'undefined') {
419
476
  initialVersionMap[name] = state.version;
420
477
  }
@@ -423,14 +480,18 @@ export const makeChatsSocket = (config) => {
423
480
  state = newLTHashState();
424
481
  }
425
482
  states[name] = state;
426
- logger.info(`resyncing ${name} from v${state.version}`);
483
+ const shouldForceSnapshot = forceSnapshotCollections.has(name);
484
+ if (shouldForceSnapshot) {
485
+ forceSnapshotCollections.delete(name);
486
+ }
487
+ logger.info(`resyncing ${name} from v${state.version}${shouldForceSnapshot ? ' (forcing snapshot)' : ''}`);
427
488
  nodes.push({
428
489
  tag: 'collection',
429
490
  attrs: {
430
491
  name,
431
492
  version: state.version.toString(),
432
- // return snapshot if being synced from scratch
433
- return_snapshot: (!state.version).toString()
493
+ // return snapshot if syncing from scratch or forcing after a failed attempt
494
+ return_snapshot: (shouldForceSnapshot || !state.version).toString()
434
495
  }
435
496
  });
436
497
  }
@@ -456,7 +517,7 @@ export const makeChatsSocket = (config) => {
456
517
  const { patches, hasMorePatches, snapshot } = decoded[name];
457
518
  try {
458
519
  if (snapshot) {
459
- const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot);
520
+ const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot, logger);
460
521
  states[name] = newState;
461
522
  Object.assign(globalMutationMap, mutationMap);
462
523
  logger.info(`restored state of ${name} from snapshot to v${newState.version} with mutations`);
@@ -479,19 +540,37 @@ export const makeChatsSocket = (config) => {
479
540
  }
480
541
  }
481
542
  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
543
  attemptsMap[name] = (attemptsMap[name] || 0) + 1;
491
- if (isIrrecoverableError) {
492
- // stop retrying
544
+ const logData = {
545
+ name,
546
+ attempt: attemptsMap[name],
547
+ version: states[name].version,
548
+ statusCode: error.output?.statusCode,
549
+ errorType: error.name,
550
+ error: error.stack
551
+ };
552
+ if (isMissingKeyError(error) && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
553
+ // WA Web treats missing keys as "Blocked" — park the collection
554
+ // until the key arrives via APP_STATE_SYNC_KEY_SHARE.
555
+ logger.warn(logData, `${name} blocked on missing key from v${states[name].version}, parking after ${attemptsMap[name]} attempts`);
556
+ blockedCollections.add(name);
493
557
  collectionsToHandle.delete(name);
494
558
  }
559
+ else if (isMissingKeyError(error)) {
560
+ // Retry with a snapshot which may use a different key.
561
+ logger.info(logData, `${name} blocked on missing key from v${states[name].version}, retrying with snapshot`);
562
+ forceSnapshotCollections.add(name);
563
+ }
564
+ else if (isAppStateSyncIrrecoverable(error, attemptsMap[name])) {
565
+ logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`);
566
+ collectionsToHandle.delete(name);
567
+ }
568
+ else {
569
+ logger.info(logData, `failed to sync ${name} from v${states[name].version}, forcing snapshot retry`);
570
+ // force a full snapshot on retry to recover from
571
+ // corrupted local state (e.g. LTHash MAC mismatch)
572
+ forceSnapshotCollections.add(name);
573
+ }
495
574
  }
496
575
  }
497
576
  }
@@ -506,25 +585,25 @@ export const makeChatsSocket = (config) => {
506
585
  * type = "preview" for a low res picture
507
586
  * type = "image for the high res picture"
508
587
  */
509
- 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
- }];
588
+ const profilePictureUrl = async (jid, type = 'image', timeoutMs = 5000, shouldIncludeTcToken = false) => {
589
+ const baseContent = [{ tag: 'picture', attrs: { type, query: 'url' } }];
590
+ // WA Web only includes tctoken for user JIDs (not groups/newsletters)
591
+ // and never for own profile pic (Chat model for self has no tcToken).
592
+ // Including tctoken for own JID causes the server to never respond.
593
+ const normalizedJid = jidNormalizedUser(jid);
594
+ const isUserJid = isPnUser(normalizedJid) || isLidUser(normalizedJid);
595
+ const me = authState.creds.me;
596
+ const isSelf = me && (normalizedJid === jidNormalizedUser(me.id) || (me.lid && normalizedJid === jidNormalizedUser(me.lid)));
597
+ let content = baseContent;
598
+ if (shouldIncludeTcToken && serverProps.profilePicPrivacyToken && isUserJid && !isSelf) {
599
+ content = await buildTcTokenFromJid({
600
+ authState,
601
+ jid: normalizedJid,
602
+ baseContent,
603
+ getLIDForPN
604
+ });
527
605
  }
606
+ jid = jidNormalizedUser(jid);
528
607
  const result = await query({
529
608
  tag: 'iq',
530
609
  attrs: {
@@ -533,16 +612,9 @@ export const makeChatsSocket = (config) => {
533
612
  type: 'get',
534
613
  xmlns: 'w:profile:picture'
535
614
  },
536
- content: [baseContent]
615
+ content
537
616
  }, timeoutMs);
538
617
  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
618
  return child?.attrs?.url;
547
619
  };
548
620
  const createCallLink = async (type, event, timeoutMs) => {
@@ -606,7 +678,12 @@ export const makeChatsSocket = (config) => {
606
678
  * @param tcToken token for subscription, use if present
607
679
  */
608
680
  const presenceSubscribe = async (toJid) => {
609
- const tcTokenContent = await buildTcTokenFromJid({ authState, jid: toJid });
681
+ // Only include tctoken for user JIDs groups/newsletters don't use tctokens
682
+ const normalizedToJid = jidNormalizedUser(toJid);
683
+ const isUserJid = isPnUser(normalizedToJid) || isLidUser(normalizedToJid);
684
+ const tcTokenContent = isUserJid
685
+ ? await buildTcTokenFromJid({ authState, jid: normalizedToJid, getLIDForPN })
686
+ : undefined;
610
687
  return sendNode({
611
688
  tag: 'presence',
612
689
  attrs: {
@@ -627,7 +704,8 @@ export const makeChatsSocket = (config) => {
627
704
  if (tag === 'presence') {
628
705
  presence = {
629
706
  lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available',
630
- lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined
707
+ lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined,
708
+ groupOnlineCount: attrs.count ? +attrs.count : undefined
631
709
  };
632
710
  }
633
711
  else if (Array.isArray(content)) {
@@ -661,7 +739,7 @@ export const makeChatsSocket = (config) => {
661
739
  logger.debug({ patch: patchCreate }, 'applying app patch');
662
740
  await resyncAppState([name], false);
663
741
  const { [name]: currentSyncVersion } = await authState.keys.get('app-state-sync-version', [name]);
664
- initial = currentSyncVersion || newLTHashState();
742
+ initial = currentSyncVersion ? ensureLTHashStateVersion(currentSyncVersion) : newLTHashState();
665
743
  encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey);
666
744
  const { patch, state } = encodeResult;
667
745
  const node = {
@@ -707,22 +785,21 @@ export const makeChatsSocket = (config) => {
707
785
  }
708
786
  }
709
787
  };
710
- /** sending non-abt props may fix QR scan fail if server expects */
788
+ /** fetch AB props */
711
789
  const fetchProps = async () => {
712
- //TODO: implement both protocol 1 and protocol 2 prop fetching, specially for abKey for WM
713
790
  const resultNode = await query({
714
791
  tag: 'iq',
715
792
  attrs: {
716
793
  to: S_WHATSAPP_NET,
717
- xmlns: 'w',
794
+ xmlns: 'abt',
718
795
  type: 'get'
719
796
  },
720
797
  content: [
721
798
  {
722
799
  tag: 'props',
723
800
  attrs: {
724
- protocol: '2',
725
- hash: authState?.creds?.lastPropHash || ''
801
+ protocol: '1',
802
+ ...(authState?.creds?.lastPropHash ? { hash: authState.creds.lastPropHash } : {})
726
803
  }
727
804
  }
728
805
  ]
@@ -737,7 +814,20 @@ export const makeChatsSocket = (config) => {
737
814
  }
738
815
  props = reduceBinaryNodeToDictionary(propsNode, 'prop');
739
816
  }
740
- logger.debug('fetched props');
817
+ // Extract protocol-relevant AB props (only the ones we need)
818
+ const privacyTokenProp = props['10518'] ?? props['privacy_token_sending_on_all_1_on_1_messages'];
819
+ if (privacyTokenProp !== undefined) {
820
+ serverProps.privacyTokenOn1to1 = privacyTokenProp === 'true' || privacyTokenProp === '1';
821
+ }
822
+ const profilePicProp = props['9666'] ?? props['profile_scraping_privacy_token_in_photo_iq'];
823
+ if (profilePicProp !== undefined) {
824
+ serverProps.profilePicPrivacyToken = profilePicProp === 'true' || profilePicProp === '1';
825
+ }
826
+ const lidIssueProp = props['14303'] ?? props['lid_trusted_token_issue_to_lid'];
827
+ if (lidIssueProp !== undefined) {
828
+ serverProps.lidTrustedTokenIssueToLid = lidIssueProp === 'true' || lidIssueProp === '1';
829
+ }
830
+ logger.debug({ serverProps }, 'fetched props');
741
831
  return props;
742
832
  };
743
833
  /**
@@ -877,6 +967,47 @@ export const makeChatsSocket = (config) => {
877
967
  ? shouldSyncHistoryMessage(historyMsg) &&
878
968
  PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
879
969
  : false;
970
+ if (historyMsg && shouldProcessHistoryMsg) {
971
+ const syncType = historyMsg.syncType;
972
+ // INITIAL_BOOTSTRAP — fire immediately, no progress check (same as WA Web K function)
973
+ if (syncType === proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP &&
974
+ !historySyncStatus.initialBootstrapComplete) {
975
+ historySyncStatus.initialBootstrapComplete = true;
976
+ ev.emit('messaging-history.status', {
977
+ syncType,
978
+ status: 'complete',
979
+ explicit: true
980
+ });
981
+ }
982
+ // RECENT with progress === 100 — explicit completion
983
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT &&
984
+ historyMsg.progress === 100 &&
985
+ !historySyncStatus.recentSyncComplete) {
986
+ historySyncStatus.recentSyncComplete = true;
987
+ clearTimeout(historySyncPausedTimeout);
988
+ historySyncPausedTimeout = undefined;
989
+ ev.emit('messaging-history.status', {
990
+ syncType,
991
+ status: 'complete',
992
+ explicit: true
993
+ });
994
+ }
995
+ // Reset 120s paused timeout on any RECENT chunk (like WA Web's handleChunkProgress)
996
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT && !historySyncStatus.recentSyncComplete) {
997
+ clearTimeout(historySyncPausedTimeout);
998
+ historySyncPausedTimeout = setTimeout(() => {
999
+ if (!historySyncStatus.recentSyncComplete) {
1000
+ historySyncStatus.recentSyncComplete = true;
1001
+ ev.emit('messaging-history.status', {
1002
+ syncType: proto.HistorySync.HistorySyncType.RECENT,
1003
+ status: 'paused',
1004
+ explicit: false
1005
+ });
1006
+ }
1007
+ historySyncPausedTimeout = undefined;
1008
+ }, HISTORY_SYNC_PAUSED_TIMEOUT_MS);
1009
+ }
1010
+ }
880
1011
  // State machine: decide on sync and flush
881
1012
  if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
882
1013
  if (awaitingSyncTimeout) {
@@ -896,6 +1027,8 @@ export const makeChatsSocket = (config) => {
896
1027
  }
897
1028
  const doAppStateSync = async () => {
898
1029
  if (syncState === SyncState.Syncing) {
1030
+ // All collections will be synced, so clear any blocked ones
1031
+ blockedCollections.clear();
899
1032
  logger.info('Doing app state sync');
900
1033
  await resyncAppState(ALL_WA_PATCH_NAMES, true);
901
1034
  // Sync is complete, go online and flush everything
@@ -955,6 +1088,11 @@ export const makeChatsSocket = (config) => {
955
1088
  }
956
1089
  });
957
1090
  ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
1091
+ if (connection === 'close') {
1092
+ blockedCollections.clear();
1093
+ clearTimeout(historySyncPausedTimeout);
1094
+ historySyncPausedTimeout = undefined;
1095
+ }
958
1096
  if (connection === 'open') {
959
1097
  if (fireInitQueries) {
960
1098
  executeInitQueries().catch(error => onUnexpectedError(error, 'init queries'));
@@ -964,6 +1102,10 @@ export const makeChatsSocket = (config) => {
964
1102
  if (!receivedPendingNotifications || syncState !== SyncState.Connecting) {
965
1103
  return;
966
1104
  }
1105
+ historySyncStatus.initialBootstrapComplete = false;
1106
+ historySyncStatus.recentSyncComplete = false;
1107
+ clearTimeout(historySyncPausedTimeout);
1108
+ historySyncPausedTimeout = undefined;
967
1109
  syncState = SyncState.AwaitingInitialSync;
968
1110
  logger.info('Connection is now AwaitingInitialSync, buffering events');
969
1111
  ev.buffer();
@@ -976,19 +1118,49 @@ export const makeChatsSocket = (config) => {
976
1118
  setTimeout(() => ev.flush(), 0);
977
1119
  return;
978
1120
  }
979
- logger.info('History sync is enabled, awaiting notification with a 20s timeout.');
1121
+ // On reconnection (accountSyncCounter > 0), the server does not push
1122
+ // history sync notifications — the device already has its data.
1123
+ // Skip the 20s wait and go online immediately.
1124
+ if (authState.creds.accountSyncCounter > 0) {
1125
+ logger.info('Reconnection with existing sync data, skipping history sync wait. Transitioning to Online.');
1126
+ syncState = SyncState.Online;
1127
+ setTimeout(() => ev.flush(), 0);
1128
+ return;
1129
+ }
1130
+ logger.info('First connection, awaiting history sync notification with a 20s timeout.');
980
1131
  if (awaitingSyncTimeout) {
981
1132
  clearTimeout(awaitingSyncTimeout);
982
1133
  }
983
1134
  awaitingSyncTimeout = setTimeout(() => {
984
1135
  if (syncState === SyncState.AwaitingInitialSync) {
985
- // TODO: investigate
986
1136
  logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer');
987
1137
  syncState = SyncState.Online;
988
1138
  ev.flush();
1139
+ // Increment so subsequent reconnections skip the 20s wait.
1140
+ // Late-arriving history is still processed via processMessage
1141
+ // regardless of the state machine phase.
1142
+ const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1;
1143
+ ev.emit('creds.update', { accountSyncCounter });
989
1144
  }
990
1145
  }, 20000);
991
1146
  });
1147
+ // When an app state sync key arrives (myAppStateKeyId is set) and there are
1148
+ // collections blocked on a missing key, trigger a re-sync for just those collections.
1149
+ // This mirrors WA Web's Blocked → retry-on-key-arrival behavior.
1150
+ ev.on('creds.update', ({ myAppStateKeyId }) => {
1151
+ if (!myAppStateKeyId || blockedCollections.size === 0) {
1152
+ return;
1153
+ }
1154
+ // If we're in the middle of a full sync, doAppStateSync handles all collections
1155
+ if (syncState === SyncState.Syncing) {
1156
+ blockedCollections.clear();
1157
+ return;
1158
+ }
1159
+ const collections = [...blockedCollections];
1160
+ blockedCollections.clear();
1161
+ logger.info({ collections }, 'app state sync key arrived, re-syncing blocked collections');
1162
+ resyncAppState(collections, false).catch(error => onUnexpectedError(error, 'blocked collections resync'));
1163
+ });
992
1164
  ev.on('lid-mapping.update', async ({ lid, pn }) => {
993
1165
  try {
994
1166
  await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
@@ -1010,6 +1182,8 @@ export const makeChatsSocket = (config) => {
1010
1182
  });
1011
1183
  return {
1012
1184
  ...sock,
1185
+ findUserId,
1186
+ serverProps,
1013
1187
  createCallLink,
1014
1188
  getBotListV2,
1015
1189
  messageMutex,
@@ -1025,7 +1199,6 @@ export const makeChatsSocket = (config) => {
1025
1199
  fetchBlocklist,
1026
1200
  fetchStatus,
1027
1201
  fetchDisappearingDuration,
1028
- findUserId,
1029
1202
  updateProfilePicture,
1030
1203
  removeProfilePicture,
1031
1204
  updateProfileStatus,
@@ -1047,6 +1220,7 @@ export const makeChatsSocket = (config) => {
1047
1220
  cleanDirtyBits,
1048
1221
  addOrEditContact,
1049
1222
  removeContact,
1223
+ placeholderResendCache,
1050
1224
  addLabel,
1051
1225
  addChatLabel,
1052
1226
  removeChatLabel,
@@ -1056,4 +1230,4 @@ export const makeChatsSocket = (config) => {
1056
1230
  addOrEditQuickReply,
1057
1231
  removeQuickReply
1058
1232
  };
1059
- };
1233
+ };