baileys-joss 1.0.3 → 2.0.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.
Files changed (193) hide show
  1. package/README.md +1052 -2534
  2. package/WAProto/fix-imports.js +22 -18
  3. package/WAProto/index.js +22 -18
  4. package/lib/Defaults/index.d.ts +15 -6
  5. package/lib/Defaults/index.d.ts.map +1 -1
  6. package/lib/Defaults/index.js +16 -7
  7. package/lib/Defaults/index.js.map +1 -1
  8. package/lib/Signal/libsignal.d.ts.map +1 -1
  9. package/lib/Signal/libsignal.js +105 -16
  10. package/lib/Signal/libsignal.js.map +1 -1
  11. package/lib/Signal/lid-mapping.d.ts +8 -9
  12. package/lib/Signal/lid-mapping.d.ts.map +1 -1
  13. package/lib/Signal/lid-mapping.js +88 -31
  14. package/lib/Signal/lid-mapping.js.map +1 -1
  15. package/lib/Socket/business.d.ts +21 -4
  16. package/lib/Socket/business.d.ts.map +1 -1
  17. package/lib/Socket/chats.d.ts +18 -2
  18. package/lib/Socket/chats.d.ts.map +1 -1
  19. package/lib/Socket/chats.js +263 -43
  20. package/lib/Socket/chats.js.map +1 -1
  21. package/lib/Socket/communities.d.ts +21 -4
  22. package/lib/Socket/communities.d.ts.map +1 -1
  23. package/lib/Socket/groups.d.ts +15 -2
  24. package/lib/Socket/groups.d.ts.map +1 -1
  25. package/lib/Socket/groups.js +20 -0
  26. package/lib/Socket/groups.js.map +1 -1
  27. package/lib/Socket/index.d.ts +21 -4
  28. package/lib/Socket/index.d.ts.map +1 -1
  29. package/lib/Socket/messages-recv.d.ts +25 -7
  30. package/lib/Socket/messages-recv.d.ts.map +1 -1
  31. package/lib/Socket/messages-recv.js +810 -332
  32. package/lib/Socket/messages-recv.js.map +1 -1
  33. package/lib/Socket/messages-send.d.ts +20 -2
  34. package/lib/Socket/messages-send.d.ts.map +1 -1
  35. package/lib/Socket/messages-send.js +149 -47
  36. package/lib/Socket/messages-send.js.map +1 -1
  37. package/lib/Socket/newsletter.d.ts +13 -1
  38. package/lib/Socket/newsletter.d.ts.map +1 -1
  39. package/lib/Socket/newsletter.js +2 -2
  40. package/lib/Socket/newsletter.js.map +1 -1
  41. package/lib/Socket/socket.d.ts +8 -2
  42. package/lib/Socket/socket.d.ts.map +1 -1
  43. package/lib/Socket/socket.js +109 -26
  44. package/lib/Socket/socket.js.map +1 -1
  45. package/lib/Types/Auth.d.ts +2 -0
  46. package/lib/Types/Auth.d.ts.map +1 -1
  47. package/lib/Types/Call.d.ts +2 -1
  48. package/lib/Types/Call.d.ts.map +1 -1
  49. package/lib/Types/Chat.d.ts +1 -0
  50. package/lib/Types/Chat.d.ts.map +1 -1
  51. package/lib/Types/Contact.d.ts +2 -0
  52. package/lib/Types/Contact.d.ts.map +1 -1
  53. package/lib/Types/Events.d.ts +21 -1
  54. package/lib/Types/Events.d.ts.map +1 -1
  55. package/lib/Types/GroupMetadata.d.ts +4 -0
  56. package/lib/Types/GroupMetadata.d.ts.map +1 -1
  57. package/lib/Types/Message.d.ts +20 -5
  58. package/lib/Types/Message.d.ts.map +1 -1
  59. package/lib/Types/Message.js.map +1 -1
  60. package/lib/Types/{Newsletter.d.ts → Mex.d.ts} +11 -5
  61. package/lib/Types/Mex.d.ts.map +1 -0
  62. package/lib/Types/{Newsletter.js → Mex.js} +9 -3
  63. package/lib/Types/Mex.js.map +1 -0
  64. package/lib/Types/Signal.d.ts +12 -1
  65. package/lib/Types/Signal.d.ts.map +1 -1
  66. package/lib/Types/Socket.d.ts +3 -0
  67. package/lib/Types/Socket.d.ts.map +1 -1
  68. package/lib/Types/State.d.ts +58 -0
  69. package/lib/Types/State.d.ts.map +1 -1
  70. package/lib/Types/State.js +43 -0
  71. package/lib/Types/State.js.map +1 -1
  72. package/lib/Types/index.d.ts +2 -1
  73. package/lib/Types/index.d.ts.map +1 -1
  74. package/lib/Types/index.js +1 -1
  75. package/lib/Types/index.js.map +1 -1
  76. package/lib/Utils/auth-utils.d.ts +5 -0
  77. package/lib/Utils/auth-utils.d.ts.map +1 -1
  78. package/lib/Utils/auth-utils.js +12 -0
  79. package/lib/Utils/auth-utils.js.map +1 -1
  80. package/lib/Utils/browser-utils.d.ts.map +1 -1
  81. package/lib/Utils/browser-utils.js +1 -0
  82. package/lib/Utils/browser-utils.js.map +1 -1
  83. package/lib/Utils/chat-export.js +30 -30
  84. package/lib/Utils/chat-utils.d.ts +31 -1
  85. package/lib/Utils/chat-utils.d.ts.map +1 -1
  86. package/lib/Utils/chat-utils.js +123 -63
  87. package/lib/Utils/chat-utils.js.map +1 -1
  88. package/lib/Utils/companion-reg-client-utils.d.ts +17 -0
  89. package/lib/Utils/companion-reg-client-utils.d.ts.map +1 -0
  90. package/lib/Utils/companion-reg-client-utils.js +35 -0
  91. package/lib/Utils/companion-reg-client-utils.js.map +1 -0
  92. package/lib/Utils/crypto.d.ts +4 -8
  93. package/lib/Utils/crypto.d.ts.map +1 -1
  94. package/lib/Utils/crypto.js +2 -26
  95. package/lib/Utils/crypto.js.map +1 -1
  96. package/lib/Utils/decode-wa-message.d.ts +18 -0
  97. package/lib/Utils/decode-wa-message.d.ts.map +1 -1
  98. package/lib/Utils/decode-wa-message.js +34 -0
  99. package/lib/Utils/decode-wa-message.js.map +1 -1
  100. package/lib/Utils/event-buffer.d.ts +2 -0
  101. package/lib/Utils/event-buffer.d.ts.map +1 -1
  102. package/lib/Utils/event-buffer.js +49 -1
  103. package/lib/Utils/event-buffer.js.map +1 -1
  104. package/lib/Utils/generics.d.ts +1 -0
  105. package/lib/Utils/generics.d.ts.map +1 -1
  106. package/lib/Utils/generics.js +10 -1
  107. package/lib/Utils/generics.js.map +1 -1
  108. package/lib/Utils/history.d.ts +2 -0
  109. package/lib/Utils/history.d.ts.map +1 -1
  110. package/lib/Utils/history.js +12 -9
  111. package/lib/Utils/history.js.map +1 -1
  112. package/lib/Utils/identity-change-handler.d.ts +7 -0
  113. package/lib/Utils/identity-change-handler.d.ts.map +1 -1
  114. package/lib/Utils/identity-change-handler.js +1 -0
  115. package/lib/Utils/identity-change-handler.js.map +1 -1
  116. package/lib/Utils/index.d.ts +27 -48
  117. package/lib/Utils/index.d.ts.map +1 -1
  118. package/lib/Utils/index.js +27 -77
  119. package/lib/Utils/index.js.map +1 -1
  120. package/lib/Utils/link-preview.js +2 -2
  121. package/lib/Utils/link-preview.js.map +1 -1
  122. package/lib/Utils/lt-hash.d.ts +7 -12
  123. package/lib/Utils/lt-hash.d.ts.map +1 -1
  124. package/lib/Utils/lt-hash.js +2 -42
  125. package/lib/Utils/lt-hash.js.map +1 -1
  126. package/lib/Utils/meme-generator.js +33 -33
  127. package/lib/Utils/message-retry-manager.d.ts +35 -2
  128. package/lib/Utils/message-retry-manager.d.ts.map +1 -1
  129. package/lib/Utils/message-retry-manager.js +99 -2
  130. package/lib/Utils/message-retry-manager.js.map +1 -1
  131. package/lib/Utils/messages-media.d.ts +6 -3
  132. package/lib/Utils/messages-media.d.ts.map +1 -1
  133. package/lib/Utils/messages-media.js +25 -11
  134. package/lib/Utils/messages-media.js.map +1 -1
  135. package/lib/Utils/messages.d.ts.map +1 -1
  136. package/lib/Utils/messages.js +28 -5
  137. package/lib/Utils/messages.js.map +1 -1
  138. package/lib/Utils/mini-games.js +7 -7
  139. package/lib/Utils/noise-handler.d.ts +7 -7
  140. package/lib/Utils/noise-handler.d.ts.map +1 -1
  141. package/lib/Utils/noise-handler.js +33 -27
  142. package/lib/Utils/noise-handler.js.map +1 -1
  143. package/lib/Utils/offline-node-processor.d.ts +17 -0
  144. package/lib/Utils/offline-node-processor.d.ts.map +1 -0
  145. package/lib/Utils/offline-node-processor.js +40 -0
  146. package/lib/Utils/offline-node-processor.js.map +1 -0
  147. package/lib/Utils/process-message.d.ts.map +1 -1
  148. package/lib/Utils/process-message.js +140 -14
  149. package/lib/Utils/process-message.js.map +1 -1
  150. package/lib/Utils/qr-generator.js +16 -16
  151. package/lib/Utils/reporting-utils.js +2 -2
  152. package/lib/Utils/reporting-utils.js.map +1 -1
  153. package/lib/Utils/signal.d.ts +13 -0
  154. package/lib/Utils/signal.d.ts.map +1 -1
  155. package/lib/Utils/signal.js +42 -0
  156. package/lib/Utils/signal.js.map +1 -1
  157. package/lib/Utils/stanza-ack.d.ts +11 -0
  158. package/lib/Utils/stanza-ack.d.ts.map +1 -0
  159. package/lib/Utils/stanza-ack.js +38 -0
  160. package/lib/Utils/stanza-ack.js.map +1 -0
  161. package/lib/Utils/sync-action-utils.d.ts.map +1 -1
  162. package/lib/Utils/sync-action-utils.js +1 -0
  163. package/lib/Utils/sync-action-utils.js.map +1 -1
  164. package/lib/Utils/tc-token-utils.d.ts +26 -1
  165. package/lib/Utils/tc-token-utils.d.ts.map +1 -1
  166. package/lib/Utils/tc-token-utils.js +151 -5
  167. package/lib/Utils/tc-token-utils.js.map +1 -1
  168. package/lib/Utils/templates.js +104 -104
  169. package/lib/Utils/validate-connection.d.ts.map +1 -1
  170. package/lib/Utils/validate-connection.js +12 -2
  171. package/lib/Utils/validate-connection.js.map +1 -1
  172. package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts.map +1 -1
  173. package/lib/WAUSync/Protocols/USyncContactProtocol.js +26 -3
  174. package/lib/WAUSync/Protocols/USyncContactProtocol.js.map +1 -1
  175. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts +10 -0
  176. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts.map +1 -0
  177. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
  178. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js.map +1 -0
  179. package/lib/WAUSync/Protocols/index.d.ts +1 -0
  180. package/lib/WAUSync/Protocols/index.d.ts.map +1 -1
  181. package/lib/WAUSync/Protocols/index.js +1 -0
  182. package/lib/WAUSync/Protocols/index.js.map +1 -1
  183. package/lib/WAUSync/USyncQuery.d.ts +1 -0
  184. package/lib/WAUSync/USyncQuery.d.ts.map +1 -1
  185. package/lib/WAUSync/USyncQuery.js +6 -2
  186. package/lib/WAUSync/USyncQuery.js.map +1 -1
  187. package/lib/WAUSync/USyncUser.d.ts +4 -0
  188. package/lib/WAUSync/USyncUser.d.ts.map +1 -1
  189. package/lib/WAUSync/USyncUser.js +8 -0
  190. package/lib/WAUSync/USyncUser.js.map +1 -1
  191. package/package.json +41 -12
  192. package/lib/Types/Newsletter.d.ts.map +0 -1
  193. package/lib/Types/Newsletter.js.map +0 -1
@@ -1,22 +1,38 @@
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, generatePanoramaProfilePicture, getHistoryMsg, newLTHashState, processSyncAction } from '../Utils/index.js';
7
+ import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, ensureLTHashStateVersion, extractSyncdPatches, generateProfilePicture, generatePanoramaProfilePicture, 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;
14
+ export const buildProfilePictureQueryContent = (type, tcTokenContent) => {
15
+ const picture = { tag: 'picture', attrs: { type, query: 'url' } };
16
+ if (tcTokenContent?.length) {
17
+ picture.content = tcTokenContent;
18
+ }
19
+ return [picture];
20
+ };
15
21
  export const makeChatsSocket = (config) => {
16
22
  const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
17
23
  const sock = makeSocket(config);
18
- const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError } = sock;
24
+ const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession, registerSocketEndHandler } = sock;
25
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
19
26
  let privacySettings;
27
+ /** Server-assigned AB props for protocol behavior. */
28
+ const serverProps = {
29
+ /** AB prop 10518: gate tctoken on 1:1 messages. Default true (safe: avoids 463). */
30
+ privacyTokenOn1to1: true,
31
+ /** AB prop 9666: gate tctoken on profile picture IQs. WA Web default: true. */
32
+ profilePicPrivacyToken: true,
33
+ /** AB prop 14303: issue tctokens to LID instead of PN. WA Web default: false. */
34
+ lidTrustedTokenIssueToLid: false
35
+ };
20
36
  let syncState = SyncState.Connecting;
21
37
  /** this mutex ensures that messages are processed in order */
22
38
  const messageMutex = makeMutex();
@@ -28,14 +44,20 @@ export const makeChatsSocket = (config) => {
28
44
  const notificationMutex = makeMutex();
29
45
  // Timeout for AwaitingInitialSync state
30
46
  let awaitingSyncTimeout;
47
+ // In-memory history sync completion tracking (resets on reconnection)
48
+ const historySyncStatus = {
49
+ initialBootstrapComplete: false,
50
+ recentSyncComplete: false
51
+ };
52
+ let historySyncPausedTimeout;
53
+ // Collections blocked on missing app state sync keys (mirrors WA Web's "Blocked" state).
54
+ // When a key arrives via APP_STATE_SYNC_KEY_SHARE, these are re-synced.
55
+ const blockedCollections = new Set();
31
56
  const placeholderResendCache = config.placeholderResendCache ||
32
57
  new NodeCache({
33
58
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
34
59
  useClones: false
35
60
  });
36
- if (!config.placeholderResendCache) {
37
- config.placeholderResendCache = placeholderResendCache;
38
- }
39
61
  /** helper function to fetch the given app state sync key */
40
62
  const getAppStateSyncKey = async (keyId) => {
41
63
  const { [keyId]: key } = await authState.keys.get('app-state-sync-key', [keyId]);
@@ -300,6 +322,42 @@ export const makeChatsSocket = (config) => {
300
322
  return getBinaryNodeChildren(listNode, 'item').map(n => n.attrs.jid);
301
323
  };
302
324
  const updateBlockStatus = async (jid, action) => {
325
+ const normalizedJid = jidNormalizedUser(jid);
326
+ let lid;
327
+ let pn_jid;
328
+ if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
329
+ lid = normalizedJid;
330
+ if (action === 'block') {
331
+ const pn = await signalRepository.lidMapping.getPNForLID(normalizedJid);
332
+ if (!pn) {
333
+ throw new Boom(`Unable to resolve PN JID for LID: ${jid}`, { statusCode: 400 });
334
+ }
335
+ pn_jid = jidNormalizedUser(pn);
336
+ }
337
+ }
338
+ else if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
339
+ const mapped = await signalRepository.lidMapping.getLIDForPN(normalizedJid);
340
+ if (!mapped) {
341
+ throw new Boom(`Unable to resolve LID for PN JID: ${jid}`, { statusCode: 400 });
342
+ }
343
+ lid = mapped;
344
+ if (action === 'block') {
345
+ pn_jid = jidNormalizedUser(normalizedJid);
346
+ }
347
+ }
348
+ else {
349
+ throw new Boom(`Invalid jid: ${jid}`, { statusCode: 400 });
350
+ }
351
+ const itemAttrs = {
352
+ action,
353
+ jid: lid
354
+ };
355
+ if (action === 'block') {
356
+ if (!pn_jid) {
357
+ throw new Boom(`pn_jid required for block: ${jid}`, { statusCode: 400 });
358
+ }
359
+ itemAttrs.pn_jid = pn_jid;
360
+ }
303
361
  await query({
304
362
  tag: 'iq',
305
363
  attrs: {
@@ -310,10 +368,7 @@ export const makeChatsSocket = (config) => {
310
368
  content: [
311
369
  {
312
370
  tag: 'item',
313
- attrs: {
314
- action,
315
- jid
316
- }
371
+ attrs: itemAttrs
317
372
  }
318
373
  ]
319
374
  });
@@ -395,6 +450,15 @@ export const makeChatsSocket = (config) => {
395
450
  };
396
451
  };
397
452
  const resyncAppState = ev.createBufferedFunction(async (collections, isInitialSync) => {
453
+ const appStateSyncKeyCache = new Map();
454
+ const getCachedAppStateSyncKey = async (keyId) => {
455
+ if (appStateSyncKeyCache.has(keyId)) {
456
+ return appStateSyncKeyCache.get(keyId) ?? undefined;
457
+ }
458
+ const key = await getAppStateSyncKey(keyId);
459
+ appStateSyncKeyCache.set(keyId, key ?? null);
460
+ return key;
461
+ };
398
462
  // we use this to determine which events to fire
399
463
  // otherwise when we resync from scratch -- all notifications will fire
400
464
  const initialVersionMap = {};
@@ -403,6 +467,9 @@ export const makeChatsSocket = (config) => {
403
467
  const collectionsToHandle = new Set(collections);
404
468
  // in case something goes wrong -- ensure we don't enter a loop that cannot be exited from
405
469
  const attemptsMap = {};
470
+ // collections that failed and need a full snapshot on retry
471
+ // mirrors WA Web's ErrorFatal -> force snapshot behavior
472
+ const forceSnapshotCollections = new Set();
406
473
  // keep executing till all collections are done
407
474
  // sometimes a single patch request will not return all the patches (God knows why)
408
475
  // so we fetch till they're all done (this is determined by the "has_more_patches" flag)
@@ -413,6 +480,7 @@ export const makeChatsSocket = (config) => {
413
480
  const result = await authState.keys.get('app-state-sync-version', [name]);
414
481
  let state = result[name];
415
482
  if (state) {
483
+ state = ensureLTHashStateVersion(state);
416
484
  if (typeof initialVersionMap[name] === 'undefined') {
417
485
  initialVersionMap[name] = state.version;
418
486
  }
@@ -421,14 +489,18 @@ export const makeChatsSocket = (config) => {
421
489
  state = newLTHashState();
422
490
  }
423
491
  states[name] = state;
424
- logger.info(`resyncing ${name} from v${state.version}`);
492
+ const shouldForceSnapshot = forceSnapshotCollections.has(name);
493
+ if (shouldForceSnapshot) {
494
+ forceSnapshotCollections.delete(name);
495
+ }
496
+ logger.info(`resyncing ${name} from v${state.version}${shouldForceSnapshot ? ' (forcing snapshot)' : ''}`);
425
497
  nodes.push({
426
498
  tag: 'collection',
427
499
  attrs: {
428
500
  name,
429
501
  version: state.version.toString(),
430
- // return snapshot if being synced from scratch
431
- return_snapshot: (!state.version).toString()
502
+ // return snapshot if syncing from scratch or forcing after a failed attempt
503
+ return_snapshot: (shouldForceSnapshot || !state.version).toString()
432
504
  }
433
505
  });
434
506
  }
@@ -454,7 +526,7 @@ export const makeChatsSocket = (config) => {
454
526
  const { patches, hasMorePatches, snapshot } = decoded[name];
455
527
  try {
456
528
  if (snapshot) {
457
- const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot);
529
+ const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot, logger);
458
530
  states[name] = newState;
459
531
  Object.assign(globalMutationMap, mutationMap);
460
532
  logger.info(`restored state of ${name} from snapshot to v${newState.version} with mutations`);
@@ -462,7 +534,7 @@ export const makeChatsSocket = (config) => {
462
534
  }
463
535
  // only process if there are syncd patches
464
536
  if (patches.length) {
465
- const { state: newState, mutationMap } = await decodePatches(name, patches, states[name], getAppStateSyncKey, config.options, initialVersionMap[name], logger, appStateMacVerification.patch);
537
+ const { state: newState, mutationMap } = await decodePatches(name, patches, states[name], getCachedAppStateSyncKey, config.options, initialVersionMap[name], logger, appStateMacVerification.patch);
466
538
  await authState.keys.set({ 'app-state-sync-version': { [name]: newState } });
467
539
  logger.info(`synced ${name} to v${newState.version}`);
468
540
  initialVersionMap[name] = newState.version;
@@ -477,19 +549,37 @@ export const makeChatsSocket = (config) => {
477
549
  }
478
550
  }
479
551
  catch (error) {
480
- // if retry attempts overshoot
481
- // or key not found
482
- const isIrrecoverableError = attemptsMap[name] >= MAX_SYNC_ATTEMPTS ||
483
- error.output?.statusCode === 404 ||
484
- error.name === 'TypeError';
485
- logger.info({ name, error: error.stack }, `failed to sync state from version${isIrrecoverableError ? '' : ', removing and trying from scratch'}`);
486
- await authState.keys.set({ 'app-state-sync-version': { [name]: null } });
487
- // increment number of retries
488
552
  attemptsMap[name] = (attemptsMap[name] || 0) + 1;
489
- if (isIrrecoverableError) {
490
- // stop retrying
553
+ const logData = {
554
+ name,
555
+ attempt: attemptsMap[name],
556
+ version: states[name].version,
557
+ statusCode: error.output?.statusCode,
558
+ errorType: error.name,
559
+ error: error.stack
560
+ };
561
+ if (isMissingKeyError(error) && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
562
+ // WA Web treats missing keys as "Blocked" — park the collection
563
+ // until the key arrives via APP_STATE_SYNC_KEY_SHARE.
564
+ logger.warn(logData, `${name} blocked on missing key from v${states[name].version}, parking after ${attemptsMap[name]} attempts`);
565
+ blockedCollections.add(name);
491
566
  collectionsToHandle.delete(name);
492
567
  }
568
+ else if (isMissingKeyError(error)) {
569
+ // Retry with a snapshot which may use a different key.
570
+ logger.info(logData, `${name} blocked on missing key from v${states[name].version}, retrying with snapshot`);
571
+ forceSnapshotCollections.add(name);
572
+ }
573
+ else if (isAppStateSyncIrrecoverable(error, attemptsMap[name])) {
574
+ logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`);
575
+ collectionsToHandle.delete(name);
576
+ }
577
+ else {
578
+ logger.info(logData, `failed to sync ${name} from v${states[name].version}, forcing snapshot retry`);
579
+ // force a full snapshot on retry to recover from
580
+ // corrupted local state (e.g. LTHash MAC mismatch)
581
+ forceSnapshotCollections.add(name);
582
+ }
493
583
  }
494
584
  }
495
585
  }
@@ -505,8 +595,21 @@ export const makeChatsSocket = (config) => {
505
595
  * type = "image for the high res picture"
506
596
  */
507
597
  const profilePictureUrl = async (jid, type = 'preview', timeoutMs) => {
508
- const baseContent = [{ tag: 'picture', attrs: { type, query: 'url' } }];
509
- const tcTokenContent = await buildTcTokenFromJid({ authState, jid, baseContent });
598
+ // WA Web only includes tctoken for user JIDs (not groups/newsletters)
599
+ // and never for own profile pic (Chat model for self has no tcToken).
600
+ // Including tctoken for own JID causes the server to never respond.
601
+ const normalizedJid = jidNormalizedUser(jid);
602
+ const isUserJid = isPnUser(normalizedJid) || isLidUser(normalizedJid);
603
+ const me = authState.creds.me;
604
+ const isSelf = me && (normalizedJid === jidNormalizedUser(me.id) || (me.lid && normalizedJid === jidNormalizedUser(me.lid)));
605
+ let tcTokenContent;
606
+ if (serverProps.profilePicPrivacyToken && isUserJid && !isSelf) {
607
+ tcTokenContent = await buildTcTokenFromJid({
608
+ authState,
609
+ jid: normalizedJid,
610
+ getLIDForPN
611
+ });
612
+ }
510
613
  jid = jidNormalizedUser(jid);
511
614
  const result = await query({
512
615
  tag: 'iq',
@@ -516,7 +619,7 @@ export const makeChatsSocket = (config) => {
516
619
  type: 'get',
517
620
  xmlns: 'w:profile:picture'
518
621
  },
519
- content: tcTokenContent
622
+ content: buildProfilePictureQueryContent(type, tcTokenContent)
520
623
  }, timeoutMs);
521
624
  const child = getBinaryNodeChild(result, 'picture');
522
625
  return child?.attrs?.url;
@@ -541,12 +644,16 @@ export const makeChatsSocket = (config) => {
541
644
  };
542
645
  const sendPresenceUpdate = async (type, toJid) => {
543
646
  const me = authState.creds.me;
544
- if (type === 'available' || type === 'unavailable') {
647
+ const isAvailableType = type === 'available';
648
+ if (isAvailableType || type === 'unavailable') {
545
649
  if (!me.name) {
546
650
  logger.warn('no name present, ignoring presence update request...');
547
651
  return;
548
652
  }
549
- ev.emit('connection.update', { isOnline: type === 'available' });
653
+ ev.emit('connection.update', { isOnline: isAvailableType });
654
+ if (isAvailableType) {
655
+ void sendUnifiedSession();
656
+ }
550
657
  await sendNode({
551
658
  tag: 'presence',
552
659
  attrs: {
@@ -578,7 +685,12 @@ export const makeChatsSocket = (config) => {
578
685
  * @param tcToken token for subscription, use if present
579
686
  */
580
687
  const presenceSubscribe = async (toJid) => {
581
- const tcTokenContent = await buildTcTokenFromJid({ authState, jid: toJid });
688
+ // Only include tctoken for user JIDs groups/newsletters don't use tctokens
689
+ const normalizedToJid = jidNormalizedUser(toJid);
690
+ const isUserJid = isPnUser(normalizedToJid) || isLidUser(normalizedToJid);
691
+ const tcTokenContent = isUserJid
692
+ ? await buildTcTokenFromJid({ authState, jid: normalizedToJid, getLIDForPN })
693
+ : undefined;
582
694
  return sendNode({
583
695
  tag: 'presence',
584
696
  attrs: {
@@ -599,7 +711,8 @@ export const makeChatsSocket = (config) => {
599
711
  if (tag === 'presence') {
600
712
  presence = {
601
713
  lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available',
602
- lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined
714
+ lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined,
715
+ groupOnlineCount: attrs.count ? +attrs.count : undefined
603
716
  };
604
717
  }
605
718
  else if (Array.isArray(content)) {
@@ -633,7 +746,7 @@ export const makeChatsSocket = (config) => {
633
746
  logger.debug({ patch: patchCreate }, 'applying app patch');
634
747
  await resyncAppState([name], false);
635
748
  const { [name]: currentSyncVersion } = await authState.keys.get('app-state-sync-version', [name]);
636
- initial = currentSyncVersion || newLTHashState();
749
+ initial = currentSyncVersion ? ensureLTHashStateVersion(currentSyncVersion) : newLTHashState();
637
750
  encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey);
638
751
  const { patch, state } = encodeResult;
639
752
  const node = {
@@ -679,22 +792,21 @@ export const makeChatsSocket = (config) => {
679
792
  }
680
793
  }
681
794
  };
682
- /** sending non-abt props may fix QR scan fail if server expects */
795
+ /** fetch AB props */
683
796
  const fetchProps = async () => {
684
- //TODO: implement both protocol 1 and protocol 2 prop fetching, specially for abKey for WM
685
797
  const resultNode = await query({
686
798
  tag: 'iq',
687
799
  attrs: {
688
800
  to: S_WHATSAPP_NET,
689
- xmlns: 'w',
801
+ xmlns: 'abt',
690
802
  type: 'get'
691
803
  },
692
804
  content: [
693
805
  {
694
806
  tag: 'props',
695
807
  attrs: {
696
- protocol: '2',
697
- hash: authState?.creds?.lastPropHash || ''
808
+ protocol: '1',
809
+ ...(authState?.creds?.lastPropHash ? { hash: authState.creds.lastPropHash } : {})
698
810
  }
699
811
  }
700
812
  ]
@@ -709,7 +821,20 @@ export const makeChatsSocket = (config) => {
709
821
  }
710
822
  props = reduceBinaryNodeToDictionary(propsNode, 'prop');
711
823
  }
712
- logger.debug('fetched props');
824
+ // Extract protocol-relevant AB props (only the ones we need)
825
+ const privacyTokenProp = props['10518'] ?? props['privacy_token_sending_on_all_1_on_1_messages'];
826
+ if (privacyTokenProp !== undefined) {
827
+ serverProps.privacyTokenOn1to1 = privacyTokenProp === 'true' || privacyTokenProp === '1';
828
+ }
829
+ const profilePicProp = props['9666'] ?? props['profile_scraping_privacy_token_in_photo_iq'];
830
+ if (profilePicProp !== undefined) {
831
+ serverProps.profilePicPrivacyToken = profilePicProp === 'true' || profilePicProp === '1';
832
+ }
833
+ const lidIssueProp = props['14303'] ?? props['lid_trusted_token_issue_to_lid'];
834
+ if (lidIssueProp !== undefined) {
835
+ serverProps.lidTrustedTokenIssueToLid = lidIssueProp === 'true' || lidIssueProp === '1';
836
+ }
837
+ logger.debug({ serverProps }, 'fetched props');
713
838
  return props;
714
839
  };
715
840
  /**
@@ -849,6 +974,47 @@ export const makeChatsSocket = (config) => {
849
974
  ? shouldSyncHistoryMessage(historyMsg) &&
850
975
  PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
851
976
  : false;
977
+ if (historyMsg && shouldProcessHistoryMsg) {
978
+ const syncType = historyMsg.syncType;
979
+ // INITIAL_BOOTSTRAP — fire immediately, no progress check (same as WA Web K function)
980
+ if (syncType === proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP &&
981
+ !historySyncStatus.initialBootstrapComplete) {
982
+ historySyncStatus.initialBootstrapComplete = true;
983
+ ev.emit('messaging-history.status', {
984
+ syncType,
985
+ status: 'complete',
986
+ explicit: true
987
+ });
988
+ }
989
+ // RECENT with progress === 100 — explicit completion
990
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT &&
991
+ historyMsg.progress === 100 &&
992
+ !historySyncStatus.recentSyncComplete) {
993
+ historySyncStatus.recentSyncComplete = true;
994
+ clearTimeout(historySyncPausedTimeout);
995
+ historySyncPausedTimeout = undefined;
996
+ ev.emit('messaging-history.status', {
997
+ syncType,
998
+ status: 'complete',
999
+ explicit: true
1000
+ });
1001
+ }
1002
+ // Reset 120s paused timeout on any RECENT chunk (like WA Web's handleChunkProgress)
1003
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT && !historySyncStatus.recentSyncComplete) {
1004
+ clearTimeout(historySyncPausedTimeout);
1005
+ historySyncPausedTimeout = setTimeout(() => {
1006
+ if (!historySyncStatus.recentSyncComplete) {
1007
+ historySyncStatus.recentSyncComplete = true;
1008
+ ev.emit('messaging-history.status', {
1009
+ syncType: proto.HistorySync.HistorySyncType.RECENT,
1010
+ status: 'paused',
1011
+ explicit: false
1012
+ });
1013
+ }
1014
+ historySyncPausedTimeout = undefined;
1015
+ }, HISTORY_SYNC_PAUSED_TIMEOUT_MS);
1016
+ }
1017
+ }
852
1018
  // State machine: decide on sync and flush
853
1019
  if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
854
1020
  if (awaitingSyncTimeout) {
@@ -868,6 +1034,8 @@ export const makeChatsSocket = (config) => {
868
1034
  }
869
1035
  const doAppStateSync = async () => {
870
1036
  if (syncState === SyncState.Syncing) {
1037
+ // All collections will be synced, so clear any blocked ones
1038
+ blockedCollections.clear();
871
1039
  logger.info('Doing app state sync');
872
1040
  await resyncAppState(ALL_WA_PATCH_NAMES, true);
873
1041
  // Sync is complete, go online and flush everything
@@ -927,6 +1095,11 @@ export const makeChatsSocket = (config) => {
927
1095
  }
928
1096
  });
929
1097
  ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
1098
+ if (connection === 'close') {
1099
+ blockedCollections.clear();
1100
+ clearTimeout(historySyncPausedTimeout);
1101
+ historySyncPausedTimeout = undefined;
1102
+ }
930
1103
  if (connection === 'open') {
931
1104
  if (fireInitQueries) {
932
1105
  executeInitQueries().catch(error => onUnexpectedError(error, 'init queries'));
@@ -936,6 +1109,10 @@ export const makeChatsSocket = (config) => {
936
1109
  if (!receivedPendingNotifications || syncState !== SyncState.Connecting) {
937
1110
  return;
938
1111
  }
1112
+ historySyncStatus.initialBootstrapComplete = false;
1113
+ historySyncStatus.recentSyncComplete = false;
1114
+ clearTimeout(historySyncPausedTimeout);
1115
+ historySyncPausedTimeout = undefined;
939
1116
  syncState = SyncState.AwaitingInitialSync;
940
1117
  logger.info('Connection is now AwaitingInitialSync, buffering events');
941
1118
  ev.buffer();
@@ -948,19 +1125,49 @@ export const makeChatsSocket = (config) => {
948
1125
  setTimeout(() => ev.flush(), 0);
949
1126
  return;
950
1127
  }
951
- logger.info('History sync is enabled, awaiting notification with a 20s timeout.');
1128
+ // On reconnection (accountSyncCounter > 0), the server does not push
1129
+ // history sync notifications — the device already has its data.
1130
+ // Skip the 20s wait and go online immediately.
1131
+ if (authState.creds.accountSyncCounter > 0) {
1132
+ logger.info('Reconnection with existing sync data, skipping history sync wait. Transitioning to Online.');
1133
+ syncState = SyncState.Online;
1134
+ setTimeout(() => ev.flush(), 0);
1135
+ return;
1136
+ }
1137
+ logger.info('First connection, awaiting history sync notification with a 20s timeout.');
952
1138
  if (awaitingSyncTimeout) {
953
1139
  clearTimeout(awaitingSyncTimeout);
954
1140
  }
955
1141
  awaitingSyncTimeout = setTimeout(() => {
956
1142
  if (syncState === SyncState.AwaitingInitialSync) {
957
- // TODO: investigate
958
1143
  logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer');
959
1144
  syncState = SyncState.Online;
960
1145
  ev.flush();
1146
+ // Increment so subsequent reconnections skip the 20s wait.
1147
+ // Late-arriving history is still processed via processMessage
1148
+ // regardless of the state machine phase.
1149
+ const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1;
1150
+ ev.emit('creds.update', { accountSyncCounter });
961
1151
  }
962
1152
  }, 20000);
963
1153
  });
1154
+ // When an app state sync key arrives (myAppStateKeyId is set) and there are
1155
+ // collections blocked on a missing key, trigger a re-sync for just those collections.
1156
+ // This mirrors WA Web's Blocked → retry-on-key-arrival behavior.
1157
+ ev.on('creds.update', ({ myAppStateKeyId }) => {
1158
+ if (!myAppStateKeyId || blockedCollections.size === 0) {
1159
+ return;
1160
+ }
1161
+ // If we're in the middle of a full sync, doAppStateSync handles all collections
1162
+ if (syncState === SyncState.Syncing) {
1163
+ blockedCollections.clear();
1164
+ return;
1165
+ }
1166
+ const collections = [...blockedCollections];
1167
+ blockedCollections.clear();
1168
+ logger.info({ collections }, 'app state sync key arrived, re-syncing blocked collections');
1169
+ resyncAppState(collections, false).catch(error => onUnexpectedError(error, 'blocked collections resync'));
1170
+ });
964
1171
  ev.on('lid-mapping.update', async ({ lid, pn }) => {
965
1172
  try {
966
1173
  await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
@@ -969,8 +1176,20 @@ export const makeChatsSocket = (config) => {
969
1176
  logger.warn({ lid, pn, error }, 'Failed to store LID-PN mapping');
970
1177
  }
971
1178
  });
1179
+ registerSocketEndHandler(() => {
1180
+ if (awaitingSyncTimeout) {
1181
+ clearTimeout(awaitingSyncTimeout);
1182
+ awaitingSyncTimeout = undefined;
1183
+ }
1184
+ if (!config.placeholderResendCache && placeholderResendCache.close) {
1185
+ placeholderResendCache.close();
1186
+ }
1187
+ syncState = SyncState.Connecting;
1188
+ privacySettings = undefined;
1189
+ });
972
1190
  return {
973
1191
  ...sock,
1192
+ serverProps,
974
1193
  createCallLink,
975
1194
  getBotListV2,
976
1195
  messageMutex,
@@ -1008,6 +1227,7 @@ export const makeChatsSocket = (config) => {
1008
1227
  cleanDirtyBits,
1009
1228
  addOrEditContact,
1010
1229
  removeContact,
1230
+ placeholderResendCache,
1011
1231
  addLabel,
1012
1232
  addChatLabel,
1013
1233
  removeChatLabel,