zapo-js 1.6.2 → 1.7.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 (141) hide show
  1. package/dist/abprops-spec.d.ts +10 -0
  2. package/dist/abprops-spec.js +25 -0
  3. package/dist/client/WaClient.d.ts +11 -0
  4. package/dist/client/WaClient.js +60 -0
  5. package/dist/client/WaClientFactory.d.ts +2 -2
  6. package/dist/client/WaClientFactory.js +50 -12
  7. package/dist/client/coordinators/WaAbPropsCoordinator.d.ts +2 -2
  8. package/dist/client/coordinators/WaAbPropsCoordinator.js +16 -4
  9. package/dist/client/coordinators/WaIncomingNodeCoordinator.d.ts +2 -1
  10. package/dist/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
  11. package/dist/client/coordinators/WaMessageCoordinator.d.ts +96 -0
  12. package/dist/client/coordinators/WaMessageCoordinator.js +163 -8
  13. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +64 -0
  14. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +217 -30
  15. package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +97 -19
  16. package/dist/client/coordinators/WaPrivacyCoordinator.js +152 -101
  17. package/dist/client/coordinators/WaRetryCoordinator.d.ts +22 -0
  18. package/dist/client/coordinators/WaRetryCoordinator.js +74 -11
  19. package/dist/client/events/dirty.d.ts +3 -0
  20. package/dist/client/events/dirty.js +12 -7
  21. package/dist/client/events/offline.d.ts +9 -0
  22. package/dist/client/events/offline.js +95 -0
  23. package/dist/client/events/privacy.d.ts +45 -0
  24. package/dist/client/events/privacy.js +130 -0
  25. package/dist/client/messaging/group-metadata.d.ts +12 -0
  26. package/dist/client/messaging/group-metadata.js +24 -8
  27. package/dist/client/messaging/link-preview.d.ts +7 -0
  28. package/dist/client/messaging/link-preview.js +27 -0
  29. package/dist/client/newsletter/content.d.ts +4 -1
  30. package/dist/client/newsletter/content.js +43 -18
  31. package/dist/client/newsletter/messaging.d.ts +2 -0
  32. package/dist/client/newsletter/messaging.js +2 -1
  33. package/dist/client/newsletter/mex.d.ts +2 -2
  34. package/dist/client/persistence/WriteBehindPersistence.js +2 -1
  35. package/dist/client/persistence/ephemeral-setting.d.ts +42 -0
  36. package/dist/client/persistence/ephemeral-setting.js +120 -0
  37. package/dist/client/persistence/group-history.d.ts +29 -0
  38. package/dist/client/persistence/group-history.js +149 -0
  39. package/dist/client/persistence/history-blob.d.ts +25 -0
  40. package/dist/client/persistence/history-blob.js +38 -0
  41. package/dist/client/persistence/history-sync.d.ts +2 -0
  42. package/dist/client/persistence/history-sync.js +31 -29
  43. package/dist/client/types.d.ts +150 -16
  44. package/dist/crypto/core/xeddsa.js +16 -0
  45. package/dist/crypto/curves/X25519.js +13 -0
  46. package/dist/crypto/nativeBackend.d.ts +20 -0
  47. package/dist/crypto/nativeBackend.js +95 -0
  48. package/dist/esm/abprops-spec.js +15 -0
  49. package/dist/esm/client/WaClient.js +60 -0
  50. package/dist/esm/client/WaClientFactory.js +51 -13
  51. package/dist/esm/client/coordinators/WaAbPropsCoordinator.js +17 -5
  52. package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
  53. package/dist/esm/client/coordinators/WaMessageCoordinator.js +166 -11
  54. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +218 -32
  55. package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +145 -94
  56. package/dist/esm/client/coordinators/WaRetryCoordinator.js +74 -11
  57. package/dist/esm/client/events/dirty.js +12 -7
  58. package/dist/esm/client/events/offline.js +92 -0
  59. package/dist/esm/client/events/privacy.js +123 -0
  60. package/dist/esm/client/messaging/group-metadata.js +25 -9
  61. package/dist/esm/client/messaging/link-preview.js +29 -2
  62. package/dist/esm/client/newsletter/content.js +44 -19
  63. package/dist/esm/client/newsletter/messaging.js +2 -1
  64. package/dist/esm/client/persistence/WriteBehindPersistence.js +2 -1
  65. package/dist/esm/client/persistence/ephemeral-setting.js +116 -0
  66. package/dist/esm/client/persistence/group-history.js +145 -0
  67. package/dist/esm/client/persistence/history-blob.js +34 -0
  68. package/dist/esm/client/persistence/history-sync.js +24 -22
  69. package/dist/esm/crypto/core/xeddsa.js +17 -1
  70. package/dist/esm/crypto/curves/X25519.js +13 -0
  71. package/dist/esm/crypto/nativeBackend.js +93 -0
  72. package/dist/esm/index.js +3 -1
  73. package/dist/esm/message/context-info.js +39 -0
  74. package/dist/esm/message/encode/content.js +20 -4
  75. package/dist/esm/message/encode/media-payload.js +3 -1
  76. package/dist/esm/message/kinds/group-history.js +36 -0
  77. package/dist/esm/message/primitives/incoming.js +18 -6
  78. package/dist/esm/protocol/abprops.js +18 -162
  79. package/dist/esm/protocol/constants.js +4 -3
  80. package/dist/esm/protocol/defaults.js +6 -0
  81. package/dist/esm/protocol/media.js +1 -0
  82. package/dist/esm/protocol/message.js +8 -0
  83. package/dist/esm/protocol/notification.js +1 -0
  84. package/dist/esm/protocol/privacy.js +70 -4
  85. package/dist/esm/store/contracts/chat-metadata.store.js +1 -0
  86. package/dist/esm/store/createStore.js +19 -2
  87. package/dist/esm/store/index.js +1 -0
  88. package/dist/esm/store/memory/chat-metadata.store.js +66 -0
  89. package/dist/esm/store/noop.store.js +8 -0
  90. package/dist/esm/transport/node/builders/privacy.js +63 -4
  91. package/dist/esm/util/coercion.js +12 -4
  92. package/dist/esm/util/index.js +1 -1
  93. package/dist/index.d.ts +11 -6
  94. package/dist/index.js +15 -2
  95. package/dist/media/types.d.ts +1 -1
  96. package/dist/message/context-info.d.ts +10 -0
  97. package/dist/message/context-info.js +40 -0
  98. package/dist/message/encode/content.d.ts +7 -3
  99. package/dist/message/encode/content.js +20 -4
  100. package/dist/message/encode/media-payload.d.ts +5 -2
  101. package/dist/message/encode/media-payload.js +3 -1
  102. package/dist/message/kinds/group-history.d.ts +30 -0
  103. package/dist/message/kinds/group-history.js +40 -0
  104. package/dist/message/primitives/incoming.d.ts +5 -0
  105. package/dist/message/primitives/incoming.js +18 -6
  106. package/dist/protocol/abprops.d.ts +27 -140
  107. package/dist/protocol/abprops.js +18 -163
  108. package/dist/protocol/constants.d.ts +4 -3
  109. package/dist/protocol/constants.js +10 -3
  110. package/dist/protocol/defaults.d.ts +6 -0
  111. package/dist/protocol/defaults.js +6 -0
  112. package/dist/protocol/media.d.ts +1 -0
  113. package/dist/protocol/media.js +1 -0
  114. package/dist/protocol/message.d.ts +6 -0
  115. package/dist/protocol/message.js +9 -0
  116. package/dist/protocol/notification.d.ts +1 -0
  117. package/dist/protocol/notification.js +1 -0
  118. package/dist/protocol/privacy.d.ts +52 -0
  119. package/dist/protocol/privacy.js +71 -5
  120. package/dist/store/contracts/chat-metadata.store.d.ts +34 -0
  121. package/dist/store/contracts/chat-metadata.store.js +2 -0
  122. package/dist/store/contracts/group-metadata.store.d.ts +7 -0
  123. package/dist/store/contracts/thread.store.d.ts +6 -0
  124. package/dist/store/createStore.d.ts +10 -2
  125. package/dist/store/createStore.js +18 -1
  126. package/dist/store/index.d.ts +3 -1
  127. package/dist/store/index.js +3 -1
  128. package/dist/store/memory/chat-metadata.store.d.ts +26 -0
  129. package/dist/store/memory/chat-metadata.store.js +70 -0
  130. package/dist/store/noop.store.d.ts +2 -0
  131. package/dist/store/noop.store.js +9 -1
  132. package/dist/store/types.d.ts +190 -39
  133. package/dist/transport/node/builders/privacy.d.ts +26 -2
  134. package/dist/transport/node/builders/privacy.js +63 -3
  135. package/dist/util/coercion.d.ts +6 -0
  136. package/dist/util/coercion.js +13 -4
  137. package/dist/util/index.d.ts +1 -1
  138. package/dist/util/index.js +2 -1
  139. package/package.json +3 -1
  140. package/spec/abprops/index.d.ts +4402 -0
  141. package/spec/abprops/index.js +4372 -0
@@ -55,7 +55,7 @@ import { SignalProtocol } from '../signal/session/SignalProtocol.js';
55
55
  import { WaKeepAlive } from '../transport/keepalive/WaKeepAlive.js';
56
56
  import { buildAckNode } from '../transport/node/builders/global.js';
57
57
  import { buildPresenceNode } from '../transport/node/builders/presence.js';
58
- import { getFirstNodeChild } from '../transport/node/helpers.js';
58
+ import { findNodeChild, getFirstNodeChild } from '../transport/node/helpers.js';
59
59
  import { createUsyncSidGenerator } from '../transport/node/usync.js';
60
60
  import { WaNodeOrchestrator } from '../transport/node/WaNodeOrchestrator.js';
61
61
  import { WaNodeTransport } from '../transport/node/WaNodeTransport.js';
@@ -145,6 +145,7 @@ function createIncomingNodeRuntime(input) {
145
145
  emitIncomingFailure: (event) => emitEvent('stream_failure', event),
146
146
  emitIncomingErrorStanza: (event) => emitEvent('stanza_error', event),
147
147
  emitIncomingNotification: (event) => emitEvent('debug_notification', event),
148
+ emitOfflineThreadMetadata: (event) => emitEvent('offline_thread_metadata', event),
148
149
  emitMexNotification: (event) => emitEvent('mex_notification', event),
149
150
  emitRegistrationCode: (event) => emitEvent('mobile_registration_code', event),
150
151
  emitAccountTakeoverNotice: (event) => emitEvent('mobile_account_takeover_notice', event),
@@ -240,6 +241,15 @@ export function buildWaClientDependencies(input) {
240
241
  allowPrivateHosts: linkPreviewOptions.allowPrivateHosts,
241
242
  proxy: linkPreviewOptions.proxy ?? options.proxy?.linkPreview
242
243
  });
244
+ const createLinkPreviewResolver = (surface) => (content) => resolveLinkPreview(content.text, content.linkPreview, {
245
+ logger,
246
+ mediaTransfer,
247
+ getMediaConn: () => getClientMediaConn(mediaMessageBuildOptions),
248
+ fetcher: linkPreviewFetcher,
249
+ options: linkPreviewOptions,
250
+ serverClock,
251
+ surface
252
+ });
243
253
  const mediaMessageBuildOptions = {
244
254
  logger,
245
255
  mediaTransfer,
@@ -257,14 +267,7 @@ export function buildWaClientDependencies(input) {
257
267
  },
258
268
  serverClock,
259
269
  media: options.media,
260
- linkPreviewResolver: (content) => resolveLinkPreview(content.text, content.linkPreview, {
261
- logger,
262
- mediaTransfer,
263
- getMediaConn: () => getClientMediaConn(mediaMessageBuildOptions),
264
- fetcher: linkPreviewFetcher,
265
- options: linkPreviewOptions,
266
- serverClock
267
- })
270
+ linkPreviewResolver: createLinkPreviewResolver()
268
271
  };
269
272
  const messageClient = new WaMessageClient({
270
273
  logger,
@@ -367,12 +370,16 @@ export function buildWaClientDependencies(input) {
367
370
  generateStanzaId: () => messageDispatch.generateOutgoingMessageId(),
368
371
  mediaTransfer,
369
372
  getMediaConn: () => getClientMediaConn(mediaMessageBuildOptions),
373
+ linkPreviewResolver: createLinkPreviewResolver('newsletter'),
370
374
  getAbPropString: (name) => abPropsCoordinator.getConfigValue(name),
371
375
  logger
372
376
  });
373
377
  const privacyCoordinator = createPrivacyCoordinator({
378
+ logger,
374
379
  queryWithContext: runtime.queryWithContext,
375
- resolveUserJidPair: (userJid) => signalDeviceSync.resolveUserJidPair(userJid)
380
+ resolveUserJidPair: (userJid) => signalDeviceSync.resolveUserJidPair(userJid),
381
+ getSelfLid: () => getCurrentCredentials()?.meLid ?? null,
382
+ emitPrivacy: (event) => runtime.emitEvent('privacy', event)
376
383
  });
377
384
  const businessCoordinator = createBusinessCoordinator({
378
385
  queryWithContext: runtime.queryWithContext,
@@ -411,7 +418,8 @@ export function buildWaClientDependencies(input) {
411
418
  }
412
419
  return {
413
420
  participants: participantJids,
414
- ephemeral: metadata.ephemeral
421
+ ephemeral: metadata.ephemeral,
422
+ ephemeralTrigger: metadata.ephemeralTrigger
415
423
  };
416
424
  },
417
425
  logger
@@ -466,6 +474,8 @@ export function buildWaClientDependencies(input) {
466
474
  sessionStore: sessionStore.session,
467
475
  identityStore: sessionStore.identity,
468
476
  deviceListStore: sessionStore.deviceList,
477
+ threadStore: sessionStore.threads,
478
+ chatMetadataStore: sessionStore.chatMetadata,
469
479
  signalDeviceSync,
470
480
  messageSecretStore: sessionStore.messageSecret,
471
481
  persistAllMessageSecrets: options.addons?.persistAllSecrets === true,
@@ -508,7 +518,9 @@ export function buildWaClientDependencies(input) {
508
518
  trustedContactToken,
509
519
  emitAddon: (event) => runtime.emitEvent('message_addon', event),
510
520
  mexSocket: { query: runtime.query },
511
- peerDataOperation
521
+ peerDataOperation,
522
+ isGroupHistorySendEnabled: () => abPropsCoordinator.getConfigValue('group_history_send'),
523
+ getAbPropNumber: (name) => abPropsCoordinator.getConfigValue(name)
512
524
  });
513
525
  const retryCoordinator = new WaRetryCoordinator({
514
526
  logger,
@@ -533,7 +545,8 @@ export function buildWaClientDependencies(input) {
533
545
  .handleIncomingMessageEvent(event)
534
546
  .catch((err) => runtime.handleError(toError(err)));
535
547
  },
536
- isMobilePrimary
548
+ isMobilePrimary,
549
+ getAbPropNumber: (name) => abPropsCoordinator.getConfigValue(name)
537
550
  });
538
551
  const botCoordinator = createBotCoordinator({
539
552
  logger,
@@ -645,6 +658,7 @@ export function buildWaClientDependencies(input) {
645
658
  const disconnectWithClientSideEffects = async (reason, isLogout, code) => {
646
659
  abPropsCoordinator.reset();
647
660
  offlineResume.reset();
661
+ privacyCoordinator.stopAccountSyncRefresh();
648
662
  await connectionManager?.disconnect();
649
663
  runtime.emitEvent('connection', {
650
664
  status: 'close',
@@ -691,6 +705,7 @@ export function buildWaClientDependencies(input) {
691
705
  signalProtocol,
692
706
  senderKeyManager,
693
707
  onDecryptFailure: (context, error) => retryCoordinator.onDecryptFailure(context, error),
708
+ requestPlaceholderResend: (context) => retryCoordinator.onUnavailableMessage(context),
694
709
  emitIncomingMessage: (event) => {
695
710
  void runtime
696
711
  .handleIncomingMessageEvent(event)
@@ -706,6 +721,10 @@ export function buildWaClientDependencies(input) {
706
721
  getCurrentCredentials,
707
722
  syncAppState: runtime.syncAppState,
708
723
  generateUsyncSid,
724
+ syncAccountPrivacy: async () => {
725
+ await privacyCoordinator.refreshFromAccountSync();
726
+ },
727
+ emitBlocklist: (blocklist) => runtime.emitEvent('blocklist', blocklist),
709
728
  newsletterListSubscribed: () => newsletterCoordinator.listSubscribed()
710
729
  }, dirtyBits);
711
730
  const offlineResume = new WaOfflineResumeCoordinator({
@@ -887,6 +906,25 @@ export function buildWaClientDependencies(input) {
887
906
  return false;
888
907
  }
889
908
  });
909
+ incomingNode.registerIncomingHandler({
910
+ tag: WA_NODE_TAGS.NOTIFICATION,
911
+ subtype: WA_NOTIFICATION_TYPES.ACCOUNT_SYNC,
912
+ prepend: true,
913
+ /**
914
+ * The stanza carries the changed `<category>` inline, but it is a
915
+ * trigger only: the payload is a partial view, so the whole set is
916
+ * refetched instead. Observer only - returning false keeps the
917
+ * generic notification handler responsible for the ack and the
918
+ * `debug_notification` emit, leaving wire behavior unchanged.
919
+ */
920
+ // eslint-disable-next-line @typescript-eslint/require-await
921
+ handler: async (node) => {
922
+ if (findNodeChild(node, WA_NODE_TAGS.PRIVACY)) {
923
+ privacyCoordinator.scheduleAccountSyncRefresh();
924
+ }
925
+ return false;
926
+ }
927
+ });
890
928
  incomingNode.registerIncomingHandler({
891
929
  tag: WA_NODE_TAGS.NOTIFICATION,
892
930
  subtype: WA_NOTIFICATION_TYPES.DEVICES,
@@ -1,8 +1,13 @@
1
+ import { resolveAbPropNameByCode, WA_ABPROPS } from '../../abprops-spec.js';
1
2
  import { parseAbPropsIqResult } from '../events/abprops.js';
2
- import { AB_PROP_CONFIGS, resolveAbPropNameByCode, WA_ABPROPS_REFRESH_BOUNDS } from '../../protocol/abprops.js';
3
+ import { WA_ABPROPS_REFRESH_BOUNDS } from '../../protocol/abprops.js';
3
4
  import { WA_DEFAULTS } from '../../protocol/constants.js';
4
5
  import { buildGetAbPropsIq } from '../../transport/node/builders/abprops.js';
5
- import { parseOptionalInt, toError } from '../../util/primitives.js';
6
+ import { toError } from '../../util/primitives.js';
7
+ // The generated table types every entry with literal `code` / `defaultValue`
8
+ // types. Reading through a widened view keeps a lookup by a non-literal name
9
+ // from expanding into a 2000-member union at every call site.
10
+ const AB_PROPS = WA_ABPROPS;
6
11
  export class WaAbPropsCoordinator {
7
12
  constructor(options) {
8
13
  this.logger = options.logger;
@@ -18,7 +23,7 @@ export class WaAbPropsCoordinator {
18
23
  if (cached !== undefined) {
19
24
  return cached;
20
25
  }
21
- return AB_PROP_CONFIGS[name].defaultValue;
26
+ return AB_PROPS[name].defaultValue;
22
27
  }
23
28
  sync() {
24
29
  if (this.syncPromise) {
@@ -105,7 +110,7 @@ export class WaAbPropsCoordinator {
105
110
  if (!name) {
106
111
  continue;
107
112
  }
108
- const config = AB_PROP_CONFIGS[name];
113
+ const config = AB_PROPS[name];
109
114
  const parsed = parseConfigValue(entry.configValue, config.type, config.defaultValue);
110
115
  this.cache.set(name, parsed);
111
116
  applied += 1;
@@ -119,6 +124,8 @@ export class WaAbPropsCoordinator {
119
124
  });
120
125
  }
121
126
  }
127
+ // parseInt, not a digits-only parse: negative values are in band, several props
128
+ // ship -1 as their default. Divergence: WA Web caches NaN, we keep the default.
122
129
  function parseConfigValue(value, type, defaultValue) {
123
130
  if (value === null) {
124
131
  return defaultValue;
@@ -127,7 +134,12 @@ function parseConfigValue(value, type, defaultValue) {
127
134
  return value === '1' || value === 'true' || value === 'True';
128
135
  }
129
136
  if (type === 'int') {
130
- return parseOptionalInt(value) ?? defaultValue;
137
+ const parsed = Number.parseInt(value, 10);
138
+ return Number.isNaN(parsed) ? defaultValue : parsed;
139
+ }
140
+ if (type === 'float') {
141
+ const parsed = Number.parseFloat(value);
142
+ return Number.isNaN(parsed) ? defaultValue : parsed;
131
143
  }
132
144
  return value;
133
145
  }
@@ -1,5 +1,6 @@
1
1
  import { parseChatstateNode } from '../events/chatstate.js';
2
2
  import { buildInboundAck, createIncomingBaseEvent, createIncomingBusinessNotificationHandler, createIncomingCallHandler, createIncomingFailureHandler, createIncomingGroupNotificationHandler, createIncomingNotificationHandler, createIncomingPictureNotificationHandler, createIncomingReceiptHandler, createIncomingRegistrationNotificationHandler, createInfoBulletinNotificationEvent, createUnhandledIncomingNodeEvent, sendSafeAck } from '../events/incoming.js';
3
+ import { parseOfflineThreadMetadata } from '../events/offline.js';
3
4
  import { parsePresenceNode } from '../events/presence.js';
4
5
  import { WA_IQ_TYPES, WA_MESSAGE_TAGS, WA_MESSAGE_TYPES, WA_NODE_TAGS, WA_NOTIFICATION_TYPES, WA_SIGNALING } from '../../protocol/constants.js';
5
6
  import { decodeNodeContentBase64OrBytes, findNodeChild, getNodeChildren, getNodeChildrenByTag } from '../../transport/node/helpers.js';
@@ -378,6 +379,9 @@ export class WaIncomingNodeCoordinator {
378
379
  if (child.tag === 'offline') {
379
380
  this.offlineResume.handleOfflineComplete(parseOptionalInt(child.attrs.count) ?? 0);
380
381
  }
382
+ if (child.tag === 'thread_metadata') {
383
+ this.runtime.emitOfflineThreadMetadata(parseOfflineThreadMetadata(child));
384
+ }
381
385
  handled = true;
382
386
  }
383
387
  }
@@ -6,12 +6,26 @@ import { uploadMedia } from '../messaging/messages.js';
6
6
  import { MEDIA_UPLOAD_PATHS } from '../../media/constants.js';
7
7
  import { buildAddonAdditionalData, decodeAddonPlaintext, decryptAddonPayload, identifyEncryptedAddon, resolveParentMessageSecret, resolvePollOptionNames, shouldUseAddonAdditionalData } from '../../message/crypto/addon-crypto.js';
8
8
  import { unwrapMessage } from '../../message/encode/content.js';
9
+ import { encodeGroupHistoryBundle } from '../../message/kinds/group-history.js';
9
10
  import { proto } from '../../proto.js';
10
- import { applyDeviceToJid, normalizeRecipientJid } from '../../protocol/jid.js';
11
+ import { applyDeviceToJid, isGroupJid, normalizeRecipientJid } from '../../protocol/jid.js';
11
12
  import { runMexQuery } from '../../transport/node/mex/client.js';
12
13
  import { readAllBytes } from '../../util/bytes.js';
13
- import { tryAsNumber, tryAsString } from '../../util/coercion.js';
14
- import { toError } from '../../util/primitives.js';
14
+ import { resolveOptionalPositive, resolvePositive, tryAsNumber, tryAsString } from '../../util/coercion.js';
15
+ import { longToNumber, toError } from '../../util/primitives.js';
16
+ /** MIME type the group-history bundle is uploaded and advertised under. */
17
+ const GROUP_HISTORY_BUNDLE_MIMETYPE = 'application/protobuf';
18
+ /** Oldest `messageTimestamp` across `messages`, in seconds; `undefined` when none carries one. */
19
+ function oldestTimestampSeconds(messages) {
20
+ let oldest;
21
+ for (let index = 0; index < messages.length; index += 1) {
22
+ const timestamp = longToNumber(messages[index].messageTimestamp);
23
+ if (timestamp > 0 && (oldest === undefined || timestamp < oldest)) {
24
+ oldest = timestamp;
25
+ }
26
+ }
27
+ return oldest;
28
+ }
15
29
  function parseReachoutTimelockMexResponse(data) {
16
30
  const root = data?.xwa2_fetch_account_reachout_timelock;
17
31
  return {
@@ -70,6 +84,8 @@ export class WaMessageCoordinator {
70
84
  this.emitAddon = deps.emitAddon;
71
85
  this.mexSocket = deps.mexSocket;
72
86
  this.peerDataOperation = deps.peerDataOperation;
87
+ this.isGroupHistorySendEnabled = deps.isGroupHistorySendEnabled;
88
+ this.getAbPropNumber = deps.getAbPropNumber;
73
89
  }
74
90
  /**
75
91
  * Asks the server to backfill older messages for `chatJid` beyond what
@@ -86,13 +102,7 @@ export class WaMessageCoordinator {
86
102
  */
87
103
  async requestHistorySync(input) {
88
104
  const chatJid = normalizeRecipientJid(input.chatJid);
89
- if (input.count !== undefined) {
90
- if (!Number.isFinite(input.count) ||
91
- !Number.isSafeInteger(input.count) ||
92
- input.count <= 0) {
93
- throw new Error(`invalid count: ${input.count}`);
94
- }
95
- }
105
+ const onDemandMsgCount = resolveOptionalPositive(input.count, 'count');
96
106
  if (input.oldestMsgTimestampMs !== undefined) {
97
107
  if (!Number.isFinite(input.oldestMsgTimestampMs) ||
98
108
  !Number.isSafeInteger(input.oldestMsgTimestampMs) ||
@@ -110,10 +120,155 @@ export class WaMessageCoordinator {
110
120
  ...(input.oldestMsgTimestampMs === undefined
111
121
  ? {}
112
122
  : { oldestMsgTimestampMs: input.oldestMsgTimestampMs }),
113
- ...(input.count === undefined ? {} : { onDemandMsgCount: input.count })
123
+ ...(onDemandMsgCount === undefined ? {} : { onDemandMsgCount })
114
124
  };
115
125
  return this.peerDataOperation.send(proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND, { historySyncOnDemandRequest });
116
126
  }
127
+ /**
128
+ * Shares recent group history with members who joined after the fact - the
129
+ * counterpart of the `group_history_bundle` event on the receiving side.
130
+ *
131
+ * Sends two messages: the bundle itself, encrypted per device and fanned
132
+ * out **only** to `toJids` (never to the whole group), followed by a hidden
133
+ * notice addressed to everyone so other clients can render the "history was
134
+ * shared" marker.
135
+ *
136
+ * WhatsApp gates the sender side per account through the `group_history_send`
137
+ * AB-prop and rejects the stanza with SMAX_INVALID when it is off, so this
138
+ * throws up front rather than spending an upload. Admin-only groups
139
+ * (`memberShareGroupHistoryMode: 'admin_share'`) reject a share from a
140
+ * regular member server-side - read `client.group.queryGroupMetadata()`
141
+ * first if you want to check that too.
142
+ *
143
+ * @throws when the account is not allowed to share group history, when
144
+ * `groupJid` is not a group, when any of `toJids` is not a current member
145
+ * (or is this account), or when there is nothing to bundle.
146
+ * @example
147
+ * ```ts
148
+ * await client.message.shareGroupHistory('12036@g.us', {
149
+ * toJids: ['5511999999999@s.whatsapp.net'],
150
+ * count: 50
151
+ * })
152
+ * ```
153
+ */
154
+ async shareGroupHistory(groupJid, input) {
155
+ const normalizedGroupJid = normalizeRecipientJid(groupJid);
156
+ if (!isGroupJid(normalizedGroupJid)) {
157
+ throw new Error(`shareGroupHistory requires a group jid: ${normalizedGroupJid}`);
158
+ }
159
+ if (input.toJids.length === 0) {
160
+ throw new Error('shareGroupHistory requires at least one recipient');
161
+ }
162
+ const messageLimit = resolvePositive(input.count, this.getAbPropNumber('group_history_message_count_limit'), 'count');
163
+ if (!this.isGroupHistorySendEnabled()) {
164
+ throw new Error('shareGroupHistory is disabled for this account (group_history_send is off)');
165
+ }
166
+ const audience = await this.messageDispatch.resolveGroupHistoryAudience(normalizedGroupJid, input.toJids);
167
+ if (audience.requestedSelf) {
168
+ throw new Error('shareGroupHistory cannot share history with this account itself');
169
+ }
170
+ if (audience.unknownJids.length > 0) {
171
+ throw new Error(`shareGroupHistory recipients are not members of the group (addressing mode: ${audience.addressingMode}): ${audience.unknownJids.join(', ')}`);
172
+ }
173
+ if (audience.historyReceivers.length === 0) {
174
+ throw new Error('shareGroupHistory resolved no recipients');
175
+ }
176
+ const messages = input.messages ??
177
+ (await this.loadGroupHistoryMessages(normalizedGroupJid, messageLimit, input.sinceMs));
178
+ if (messages.length === 0) {
179
+ throw new Error('shareGroupHistory found no messages to share');
180
+ }
181
+ const { compressed } = await encodeGroupHistoryBundle(messages, input.outOfWindowPinnedMessages);
182
+ const upload = await uploadMedia(this.mediaUploadOptions, {
183
+ source: compressed,
184
+ cryptoType: 'group-history',
185
+ uploadPath: MEDIA_UPLOAD_PATHS['group-history'],
186
+ contentType: GROUP_HISTORY_BUNDLE_MIMETYPE,
187
+ sidecar: false,
188
+ logLabel: 'group history bundle upload'
189
+ });
190
+ const oldestInWindow = oldestTimestampSeconds(messages);
191
+ const oldestPin = oldestTimestampSeconds(input.outOfWindowPinnedMessages ?? []);
192
+ const oldestInBundle = oldestPin === undefined
193
+ ? oldestInWindow
194
+ : oldestInWindow === undefined
195
+ ? oldestPin
196
+ : Math.min(oldestInWindow, oldestPin);
197
+ const metadata = {
198
+ historyReceivers: audience.historyReceivers,
199
+ nonHistoryReceivers: audience.nonHistoryReceivers,
200
+ messageCount: messages.length,
201
+ oldestMessageTimestampInWindow: oldestInWindow,
202
+ oldestMessageTimestampInBundle: oldestInBundle
203
+ };
204
+ const bundleResult = await this.send(normalizedGroupJid, {
205
+ messageHistoryBundle: {
206
+ mimetype: GROUP_HISTORY_BUNDLE_MIMETYPE,
207
+ fileSha256: upload.fileSha256,
208
+ fileEncSha256: upload.fileEncSha256,
209
+ mediaKey: upload.mediaKey,
210
+ directPath: upload.directPath,
211
+ mediaKeyTimestamp: this.mediaUploadOptions.serverClock.nowSeconds(),
212
+ messageHistoryMetadata: metadata
213
+ }
214
+ });
215
+ let noticeMessageId;
216
+ try {
217
+ noticeMessageId = (await this.send(normalizedGroupJid, {
218
+ messageHistoryNotice: { messageHistoryMetadata: metadata }
219
+ })).id;
220
+ }
221
+ catch (error) {
222
+ this.logger.warn('group history notice failed after the bundle was delivered', {
223
+ groupJid: normalizedGroupJid,
224
+ id: bundleResult.id,
225
+ receiverCount: audience.historyReceivers.length,
226
+ message: toError(error).message
227
+ });
228
+ }
229
+ this.logger.debug('shared group history', {
230
+ groupJid: normalizedGroupJid,
231
+ id: bundleResult.id,
232
+ messagesCount: messages.length,
233
+ receiverCount: audience.historyReceivers.length
234
+ });
235
+ return {
236
+ bundleMessageId: bundleResult.id,
237
+ noticeMessageId,
238
+ messagesCount: messages.length,
239
+ historyReceivers: audience.historyReceivers,
240
+ nonHistoryReceivers: audience.nonHistoryReceivers
241
+ };
242
+ }
243
+ /**
244
+ * Reads the most recent messages of a group back out of the mailbox store
245
+ * and rebuilds the `WebMessageInfo` shape a bundle carries. Yields nothing
246
+ * when the `messages` store domain is `'none'`.
247
+ */
248
+ async loadGroupHistoryMessages(groupJid, limit, sinceMs) {
249
+ const records = await this.messageStore.listByThread(groupJid, limit);
250
+ const messages = [];
251
+ for (let index = 0; index < records.length; index += 1) {
252
+ const record = records[index];
253
+ if (!record.messageBytes) {
254
+ continue;
255
+ }
256
+ if (sinceMs !== undefined && (record.timestampMs ?? 0) < sinceMs) {
257
+ continue;
258
+ }
259
+ messages[messages.length] = {
260
+ key: {
261
+ id: record.id,
262
+ remoteJid: groupJid,
263
+ fromMe: record.fromMe,
264
+ participant: record.participantJid ?? record.senderJid
265
+ },
266
+ message: proto.Message.decode(record.messageBytes),
267
+ messageTimestamp: Math.floor((record.timestampMs ?? 0) / 1000)
268
+ };
269
+ }
270
+ return messages;
271
+ }
117
272
  /**
118
273
  * Fetches the server-side "reachout" timelock that throttles cold outreach
119
274
  * to non-contacts, returning the active window when enforcement is on.