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
@@ -12,7 +12,8 @@ function mergeThread(previous, incoming) {
12
12
  pinned: incoming.pinned ?? previous.pinned,
13
13
  muteEndMs: incoming.muteEndMs ?? previous.muteEndMs,
14
14
  markedAsUnread: incoming.markedAsUnread ?? previous.markedAsUnread,
15
- ephemeralExpiration: incoming.ephemeralExpiration ?? previous.ephemeralExpiration
15
+ ephemeralExpiration: incoming.ephemeralExpiration ?? previous.ephemeralExpiration,
16
+ ephemeralSettingTimestamp: incoming.ephemeralSettingTimestamp ?? previous.ephemeralSettingTimestamp
16
17
  };
17
18
  }
18
19
  function mergeContact(previous, incoming) {
@@ -0,0 +1,42 @@
1
+ import type { WriteBehindPersistence } from '../persistence/WriteBehindPersistence';
2
+ import type { WaIncomingMessageEvent } from '../types';
3
+ import type { Logger } from '../../infra/log/types';
4
+ import type { Proto } from '../../proto';
5
+ import type { WaChatMetadataStore } from '../../store/contracts/chat-metadata.store';
6
+ export interface WaPersistIncomingEphemeralSettingOptions {
7
+ readonly logger: Logger;
8
+ readonly writeBehind: WriteBehindPersistence;
9
+ readonly event: WaIncomingMessageEvent;
10
+ readonly protocolMessage: Proto.Message.IProtocolMessage;
11
+ readonly chatMetadataStore: WaChatMetadataStore;
12
+ }
13
+ /**
14
+ * Persists a 1:1 `EPHEMERAL_SETTING` protocol message so outgoing sends pick up
15
+ * the peer's change without waiting for history sync. Groups are skipped –
16
+ * their timer comes from group notifications, not protocol messages.
17
+ */
18
+ export declare function persistIncomingEphemeralSetting(options: WaPersistIncomingEphemeralSettingOptions): void;
19
+ export interface WaEphemeralObserverOptions {
20
+ readonly logger: Logger;
21
+ readonly chatMetadataStore: WaChatMetadataStore;
22
+ }
23
+ /**
24
+ * Builds the inbound observer that refreshes the chat-metadata cache from
25
+ * ordinary traffic. A peer stamps `contextInfo.ephemeralSettingTimestamp` on
26
+ * every message in a disappearing chat, making it the only continuously
27
+ * refreshed source - history sync and `EPHEMERAL_SETTING` are point-in-time.
28
+ *
29
+ * Never blocks and never rejects: the store round-trip is fire-and-forget.
30
+ * Messages repeating a value share one round-trip, so the batch an offline
31
+ * resume delivers costs a single read instead of one per message, while a
32
+ * message carrying a *different* value still gets its own round-trip rather
33
+ * than being swallowed by the one in flight.
34
+ *
35
+ * A write skips a setting older than the cached one, mirroring how the peer
36
+ * resolves it: highest timestamp wins. That check is not atomic with the
37
+ * write, so two round-trips that both read a cold cache can still land out of
38
+ * order and leave the older value behind. The next message from the chat
39
+ * corrects it, and a stale-but-older stamp loses to the peer's own value
40
+ * anyway, so this is not worth serializing the receive path over.
41
+ */
42
+ export declare function createEphemeralObserver(options: WaEphemeralObserverOptions): (event: WaIncomingMessageEvent) => void;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.persistIncomingEphemeralSetting = persistIncomingEphemeralSetting;
4
+ exports.createEphemeralObserver = createEphemeralObserver;
5
+ const PromiseDedup_1 = require("../../infra/perf/PromiseDedup");
6
+ const context_info_1 = require("../../message/context-info");
7
+ const jid_1 = require("../../protocol/jid");
8
+ const message_1 = require("../../protocol/message");
9
+ const primitives_1 = require("../../util/primitives");
10
+ /**
11
+ * Persists a 1:1 `EPHEMERAL_SETTING` protocol message so outgoing sends pick up
12
+ * the peer's change without waiting for history sync. Groups are skipped –
13
+ * their timer comes from group notifications, not protocol messages.
14
+ */
15
+ function persistIncomingEphemeralSetting(options) {
16
+ const { logger, writeBehind, event, protocolMessage, chatMetadataStore } = options;
17
+ const chatJid = event.key.remoteJid;
18
+ if (!chatJid || event.key.isGroup || (0, jid_1.isGroupJid)(chatJid)) {
19
+ return;
20
+ }
21
+ const expiration = protocolMessage.ephemeralExpiration ?? 0;
22
+ const rawTimestamp = protocolMessage.ephemeralSettingTimestamp !== undefined &&
23
+ protocolMessage.ephemeralSettingTimestamp !== null
24
+ ? (0, primitives_1.longToNumber)(protocolMessage.ephemeralSettingTimestamp)
25
+ : event.timestampSeconds;
26
+ const settingTimestamp = rawTimestamp !== undefined ? (0, message_1.normalizeEphemeralSettingSeconds)(rawTimestamp) : undefined;
27
+ void chatMetadataStore
28
+ .upsertChatMetadata({
29
+ chatJid,
30
+ ephemeralExpiration: expiration,
31
+ ...(settingTimestamp !== undefined
32
+ ? { ephemeralSettingTimestamp: settingTimestamp }
33
+ : {}),
34
+ updatedAtMs: Date.now()
35
+ })
36
+ .catch((error) => {
37
+ logger.debug('failed to cache incoming ephemeral setting', {
38
+ jid: chatJid,
39
+ message: (0, primitives_1.toError)(error).message
40
+ });
41
+ });
42
+ try {
43
+ writeBehind.persistThread({
44
+ jid: chatJid,
45
+ ephemeralExpiration: expiration,
46
+ ...(settingTimestamp !== undefined
47
+ ? { ephemeralSettingTimestamp: settingTimestamp }
48
+ : {})
49
+ });
50
+ }
51
+ catch (error) {
52
+ logger.warn('failed to persist incoming ephemeral setting', {
53
+ jid: chatJid,
54
+ expiration,
55
+ message: (0, primitives_1.toError)(error).message
56
+ });
57
+ }
58
+ }
59
+ /**
60
+ * Builds the inbound observer that refreshes the chat-metadata cache from
61
+ * ordinary traffic. A peer stamps `contextInfo.ephemeralSettingTimestamp` on
62
+ * every message in a disappearing chat, making it the only continuously
63
+ * refreshed source - history sync and `EPHEMERAL_SETTING` are point-in-time.
64
+ *
65
+ * Never blocks and never rejects: the store round-trip is fire-and-forget.
66
+ * Messages repeating a value share one round-trip, so the batch an offline
67
+ * resume delivers costs a single read instead of one per message, while a
68
+ * message carrying a *different* value still gets its own round-trip rather
69
+ * than being swallowed by the one in flight.
70
+ *
71
+ * A write skips a setting older than the cached one, mirroring how the peer
72
+ * resolves it: highest timestamp wins. That check is not atomic with the
73
+ * write, so two round-trips that both read a cold cache can still land out of
74
+ * order and leave the older value behind. The next message from the chat
75
+ * corrects it, and a stale-but-older stamp loses to the peer's own value
76
+ * anyway, so this is not worth serializing the receive path over.
77
+ */
78
+ function createEphemeralObserver(options) {
79
+ const { logger, chatMetadataStore } = options;
80
+ const dedup = new PromiseDedup_1.PromiseDedup();
81
+ return (event) => {
82
+ const expiration = event.expirationSeconds;
83
+ if (expiration === undefined || expiration <= 0) {
84
+ return;
85
+ }
86
+ const chatJid = event.key.remoteJid;
87
+ if (!chatJid || event.key.isGroup || event.key.isNewsletter || (0, jid_1.isGroupJid)(chatJid)) {
88
+ return;
89
+ }
90
+ const raw = (0, context_info_1.pickIncomingEphemeralSettingTimestamp)(event.message);
91
+ if (raw === undefined) {
92
+ return;
93
+ }
94
+ const settingTimestamp = (0, message_1.normalizeEphemeralSettingSeconds)(raw);
95
+ void dedup
96
+ .run(`observe:${chatJid}:${expiration}:${settingTimestamp}`, async () => {
97
+ const cached = await chatMetadataStore.getChatMetadata(chatJid);
98
+ if (cached?.ephemeralSettingTimestamp === settingTimestamp &&
99
+ cached.ephemeralExpiration === expiration) {
100
+ return;
101
+ }
102
+ if (cached?.ephemeralSettingTimestamp !== undefined &&
103
+ cached.ephemeralSettingTimestamp > settingTimestamp) {
104
+ return;
105
+ }
106
+ await chatMetadataStore.upsertChatMetadata({
107
+ chatJid,
108
+ ephemeralExpiration: expiration,
109
+ ephemeralSettingTimestamp: settingTimestamp,
110
+ updatedAtMs: Date.now()
111
+ });
112
+ })
113
+ .catch((error) => {
114
+ logger.debug('failed to cache observed ephemeral setting', {
115
+ jid: chatJid,
116
+ message: (0, primitives_1.toError)(error).message
117
+ });
118
+ });
119
+ };
120
+ }
@@ -0,0 +1,29 @@
1
+ import type { WaAbPropName } from '../../abprops-spec';
2
+ import type { WriteBehindPersistence } from '../persistence/WriteBehindPersistence';
3
+ import type { WaClientEventMap } from '../types';
4
+ import type { Logger } from '../../infra/log/types';
5
+ import type { WaMediaTransferClient } from '../../media/transfer/WaMediaTransferClient';
6
+ import { type Proto } from '../../proto';
7
+ export interface WaGroupHistoryBundleInput {
8
+ /** The `messageHistoryBundle` payload carried by the incoming message. */
9
+ readonly bundle: Proto.Message.IMessageHistoryBundle;
10
+ /** Group the bundle was shared in - the `remoteJid` of the carrier message. */
11
+ readonly groupJid: string;
12
+ /** Member who shared the history (carrier message `participant`). */
13
+ readonly senderJid?: string;
14
+ /** Stanza id of the carrier message. */
15
+ readonly bundleMessageId?: string;
16
+ /** Carrier message `t` attr, in seconds - drives the bundle expiry check. */
17
+ readonly sentAtSeconds?: number;
18
+ }
19
+ export interface WaGroupHistoryDeps {
20
+ readonly logger: Logger;
21
+ readonly mediaTransfer: WaMediaTransferClient;
22
+ readonly writeBehind: WriteBehindPersistence;
23
+ readonly emitEvent: <K extends keyof WaClientEventMap>(event: K, ...args: Parameters<WaClientEventMap[K]>) => void;
24
+ readonly meJid?: string | null;
25
+ readonly meLid?: string | null;
26
+ readonly getAbPropNumber: (name: WaAbPropName) => number;
27
+ }
28
+ export declare function runGroupHistoryBundle(deps: WaGroupHistoryDeps, input: WaGroupHistoryBundleInput): Promise<void>;
29
+ export declare function processGroupHistoryBundle(deps: WaGroupHistoryDeps, input: WaGroupHistoryBundleInput): Promise<void>;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runGroupHistoryBundle = runGroupHistoryBundle;
4
+ exports.processGroupHistoryBundle = processGroupHistoryBundle;
5
+ const history_blob_1 = require("../persistence/history-blob");
6
+ const group_history_1 = require("../../message/kinds/group-history");
7
+ const _proto_1 = require("../../proto");
8
+ const jid_1 = require("../../protocol/jid");
9
+ const primitives_1 = require("../../util/primitives");
10
+ const GROUP_HISTORY_MAX_PENDING_WRITES = 1024;
11
+ async function runGroupHistoryBundle(deps, input) {
12
+ try {
13
+ await processGroupHistoryBundle(deps, input);
14
+ }
15
+ catch (error) {
16
+ deps.logger.warn('failed to process group history bundle', {
17
+ groupJid: input.groupJid,
18
+ id: input.bundleMessageId,
19
+ message: (0, primitives_1.toError)(error).message
20
+ });
21
+ }
22
+ }
23
+ async function processGroupHistoryBundle(deps, input) {
24
+ const logger = deps.logger.child({
25
+ groupJid: input.groupJid,
26
+ id: input.bundleMessageId
27
+ });
28
+ if (!(0, jid_1.isGroupJid)(input.groupJid)) {
29
+ logger.debug('skipping group history bundle outside a group');
30
+ return;
31
+ }
32
+ if (!isHistoryReceiver(deps, input.bundle)) {
33
+ logger.debug('skipping group history bundle not addressed to this account');
34
+ return;
35
+ }
36
+ const nowMs = Date.now();
37
+ const bundleTtlSeconds = deps.getAbPropNumber('group_history_bundle_time_limit_receiver_enforcement_secs');
38
+ if (input.sentAtSeconds !== undefined &&
39
+ nowMs > (input.sentAtSeconds + bundleTtlSeconds) * 1000) {
40
+ logger.debug('skipping expired group history bundle', {
41
+ sentAtSeconds: input.sentAtSeconds,
42
+ bundleTtlSeconds
43
+ });
44
+ return;
45
+ }
46
+ const blob = await (0, history_blob_1.downloadHistoryBlob)(deps.mediaTransfer, input.bundle, 'group-history', 'group history bundle');
47
+ const history = await (0, group_history_1.decodeGroupHistoryBundle)(blob);
48
+ const messageTtlSeconds = deps.getAbPropNumber('group_history_messages_time_limit_receiver_enforcement_secs');
49
+ const pendingWrites = [];
50
+ const dropped = { foreignChat: 0, ephemeralExpired: 0, tooOld: 0, stub: 0 };
51
+ let messagesCount = 0;
52
+ let oldestTimestampMs;
53
+ for (const source of [
54
+ { messages: history.messages, skipAgeCheck: false },
55
+ { messages: history.outOfWindowPinnedMessages, skipAgeCheck: true }
56
+ ]) {
57
+ for (const webMsg of source.messages) {
58
+ if (!webMsg.key?.id || !webMsg.message) {
59
+ dropped.stub += 1;
60
+ continue;
61
+ }
62
+ if (webMsg.key.remoteJid && webMsg.key.remoteJid !== input.groupJid) {
63
+ dropped.foreignChat += 1;
64
+ continue;
65
+ }
66
+ const timestampSeconds = (0, primitives_1.longToNumber)(webMsg.messageTimestamp);
67
+ if (isEphemeralExpired(webMsg, timestampSeconds, nowMs)) {
68
+ dropped.ephemeralExpired += 1;
69
+ continue;
70
+ }
71
+ if (!source.skipAgeCheck &&
72
+ timestampSeconds > 0 &&
73
+ (timestampSeconds + 2 * messageTtlSeconds) * 1000 < nowMs) {
74
+ dropped.tooOld += 1;
75
+ continue;
76
+ }
77
+ const timestampMs = timestampSeconds * 1000;
78
+ if (timestampMs > 0 &&
79
+ (oldestTimestampMs === undefined || timestampMs < oldestTimestampMs)) {
80
+ oldestTimestampMs = timestampMs;
81
+ }
82
+ pendingWrites[pendingWrites.length] = deps.writeBehind.persistMessageAsync({
83
+ id: webMsg.key.id,
84
+ threadJid: input.groupJid,
85
+ senderJid: webMsg.key.participant ?? undefined,
86
+ fromMe: webMsg.key.fromMe === true,
87
+ timestampMs: timestampMs || undefined,
88
+ messageBytes: _proto_1.proto.Message.encode(webMsg.message).finish()
89
+ });
90
+ if (pendingWrites.length >= GROUP_HISTORY_MAX_PENDING_WRITES) {
91
+ await (0, history_blob_1.flushPendingWrites)(pendingWrites);
92
+ }
93
+ messagesCount += 1;
94
+ }
95
+ }
96
+ await (0, history_blob_1.flushPendingWrites)(pendingWrites);
97
+ const droppedCount = dropped.foreignChat + dropped.ephemeralExpired + dropped.tooOld + dropped.stub;
98
+ logger.debug('processed group history bundle', {
99
+ messagesCount,
100
+ outOfWindowPinsCount: history.outOfWindowPinnedMessages.length,
101
+ droppedCount,
102
+ dropped
103
+ });
104
+ const event = {
105
+ groupJid: input.groupJid,
106
+ senderJid: input.senderJid,
107
+ bundleMessageId: input.bundleMessageId,
108
+ messagesCount,
109
+ outOfWindowPinsCount: history.outOfWindowPinnedMessages.length,
110
+ droppedCount,
111
+ oldestTimestampMs,
112
+ historyReceivers: [...(input.bundle.messageHistoryMetadata?.historyReceivers ?? [])]
113
+ };
114
+ deps.emitEvent('group_history_bundle', event);
115
+ }
116
+ /**
117
+ * A bundle is only ours to process when this account is listed in
118
+ * `historyReceivers`. The list may address us by either PN or LID, so both
119
+ * forms of the local identity are compared.
120
+ */
121
+ function isHistoryReceiver(deps, bundle) {
122
+ const receivers = bundle.messageHistoryMetadata?.historyReceivers;
123
+ if (!receivers || receivers.length === 0) {
124
+ return false;
125
+ }
126
+ const meUserJid = deps.meJid ? (0, jid_1.toUserJid)(deps.meJid) : null;
127
+ const meUserLid = deps.meLid ? (0, jid_1.toUserJid)(deps.meLid) : null;
128
+ if (!meUserJid && !meUserLid) {
129
+ return false;
130
+ }
131
+ for (let index = 0; index < receivers.length; index += 1) {
132
+ const receiver = (0, jid_1.toUserJid)(receivers[index]);
133
+ if (receiver === meUserJid || receiver === meUserLid) {
134
+ return true;
135
+ }
136
+ }
137
+ return false;
138
+ }
139
+ function isEphemeralExpired(webMsg, timestampSeconds, nowMs) {
140
+ const duration = webMsg.ephemeralDuration;
141
+ if (!duration) {
142
+ return false;
143
+ }
144
+ const startSeconds = (0, primitives_1.longToNumber)(webMsg.ephemeralStartTimestamp) || timestampSeconds;
145
+ if (startSeconds <= 0) {
146
+ return false;
147
+ }
148
+ return (startSeconds + duration) * 1000 <= nowMs;
149
+ }
@@ -0,0 +1,25 @@
1
+ import type { WaMediaTransferClient } from '../../media/transfer/WaMediaTransferClient';
2
+ import type { MediaCryptoType } from '../../media/types';
3
+ /**
4
+ * Encrypted-blob fields shared by the history-sync notification and the
5
+ * group-history bundle: both point at a CDN object plus the key and hashes
6
+ * needed to decrypt and verify it.
7
+ */
8
+ export interface WaHistoryBlobSource {
9
+ readonly directPath?: string | null;
10
+ readonly mediaKey?: Uint8Array | string | null;
11
+ readonly fileSha256?: Uint8Array | string | null;
12
+ readonly fileEncSha256?: Uint8Array | string | null;
13
+ }
14
+ /**
15
+ * Fetches and decrypts a history blob. `label` prefixes the field errors so a
16
+ * failure names which payload was malformed.
17
+ *
18
+ * @throws when `directPath` is absent or a key/hash field is missing.
19
+ */
20
+ export declare function downloadHistoryBlob(mediaTransfer: WaMediaTransferClient, source: WaHistoryBlobSource, mediaType: MediaCryptoType, label: string): Promise<Uint8Array>;
21
+ /**
22
+ * Awaits an accumulating write batch and empties it in place, so the caller can
23
+ * keep filling the same array instead of allocating a new one per chunk.
24
+ */
25
+ export declare function flushPendingWrites(pendingWrites: Promise<void>[]): Promise<void>;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.downloadHistoryBlob = downloadHistoryBlob;
4
+ exports.flushPendingWrites = flushPendingWrites;
5
+ const bytes_1 = require("../../util/bytes");
6
+ /**
7
+ * Fetches and decrypts a history blob. `label` prefixes the field errors so a
8
+ * failure names which payload was malformed.
9
+ *
10
+ * @throws when `directPath` is absent or a key/hash field is missing.
11
+ */
12
+ async function downloadHistoryBlob(mediaTransfer, source, mediaType, label) {
13
+ if (!source.directPath) {
14
+ throw new Error(`${label} missing directPath`);
15
+ }
16
+ const mediaKey = (0, bytes_1.decodeProtoBytes)(source.mediaKey, `${label} mediaKey`);
17
+ const fileSha256 = (0, bytes_1.decodeProtoBytes)(source.fileSha256, `${label} fileSha256`);
18
+ const fileEncSha256 = (0, bytes_1.decodeProtoBytes)(source.fileEncSha256, `${label} fileEncSha256`);
19
+ return mediaTransfer.downloadAndDecrypt({
20
+ directPath: source.directPath,
21
+ mediaType,
22
+ mediaKey,
23
+ fileSha256,
24
+ fileEncSha256
25
+ });
26
+ }
27
+ /**
28
+ * Awaits an accumulating write batch and empties it in place, so the caller can
29
+ * keep filling the same array instead of allocating a new one per chunk.
30
+ */
31
+ async function flushPendingWrites(pendingWrites) {
32
+ if (pendingWrites.length === 0) {
33
+ return;
34
+ }
35
+ const settled = Promise.all(pendingWrites);
36
+ pendingWrites.length = 0;
37
+ await settled;
38
+ }
@@ -3,10 +3,12 @@ import type { WaClientEventMap } from '../types';
3
3
  import type { Logger } from '../../infra/log/types';
4
4
  import type { WaMediaTransferClient } from '../../media/transfer/WaMediaTransferClient';
5
5
  import { type Proto } from '../../proto';
6
+ import type { WaChatMetadataStore } from '../../store/contracts/chat-metadata.store';
6
7
  interface WaHistorySyncDeps {
7
8
  readonly logger: Logger;
8
9
  readonly mediaTransfer: WaMediaTransferClient;
9
10
  readonly writeBehind: WriteBehindPersistence;
11
+ readonly chatMetadataStore?: WaChatMetadataStore;
10
12
  readonly emitEvent: <K extends keyof WaClientEventMap>(event: K, ...args: Parameters<WaClientEventMap[K]>) => void;
11
13
  readonly onPrivacyTokens?: (conversations: readonly {
12
14
  readonly jid: string;
@@ -4,8 +4,10 @@ exports.runHistorySyncNotification = runHistorySyncNotification;
4
4
  exports.processHistorySyncNotification = processHistorySyncNotification;
5
5
  const node_util_1 = require("node:util");
6
6
  const node_zlib_1 = require("node:zlib");
7
+ const history_blob_1 = require("../persistence/history-blob");
7
8
  const _proto_1 = require("../../proto");
8
9
  const jid_1 = require("../../protocol/jid");
10
+ const message_1 = require("../../protocol/message");
9
11
  const bytes_1 = require("../../util/bytes");
10
12
  const primitives_1 = require("../../util/primitives");
11
13
  const unzipAsync = (0, node_util_1.promisify)(node_zlib_1.unzip);
@@ -98,7 +100,7 @@ async function processHistorySyncNotification(deps, notification) {
98
100
  lastUpdatedMs: nowMs
99
101
  });
100
102
  if (pendingWrites.length >= HISTORY_SYNC_MAX_PENDING_WRITES) {
101
- await flushPendingWrites(pendingWrites);
103
+ await (0, history_blob_1.flushPendingWrites)(pendingWrites);
102
104
  }
103
105
  }
104
106
  for (const c of historySync.inlineContacts ?? []) {
@@ -117,7 +119,7 @@ async function processHistorySyncNotification(deps, notification) {
117
119
  lastUpdatedMs: nowMs
118
120
  });
119
121
  if (pendingWrites.length >= HISTORY_SYNC_MAX_PENDING_WRITES) {
120
- await flushPendingWrites(pendingWrites);
122
+ await (0, history_blob_1.flushPendingWrites)(pendingWrites);
121
123
  }
122
124
  }
123
125
  let messagesCount = 0;
@@ -127,6 +129,25 @@ async function processHistorySyncNotification(deps, notification) {
127
129
  deps.logger.debug('skipping history sync conversation without thread jid');
128
130
  continue;
129
131
  }
132
+ const ephemeralExpiration = conversation.ephemeralExpiration ?? undefined;
133
+ const ephemeralSettingTimestamp = (0, message_1.normalizeEphemeralSettingSeconds)((0, primitives_1.longToNumber)(conversation.ephemeralSettingTimestamp)) || undefined;
134
+ if (deps.chatMetadataStore && ephemeralExpiration !== undefined) {
135
+ pendingWrites[pendingWrites.length] = deps.chatMetadataStore
136
+ .upsertChatMetadata({
137
+ chatJid: threadJid,
138
+ ephemeralExpiration,
139
+ ...(ephemeralSettingTimestamp !== undefined
140
+ ? { ephemeralSettingTimestamp }
141
+ : {}),
142
+ updatedAtMs: nowMs
143
+ })
144
+ .catch((error) => {
145
+ deps.logger.debug('failed to cache history sync chat metadata', {
146
+ jid: threadJid,
147
+ message: (0, primitives_1.toError)(error).message
148
+ });
149
+ });
150
+ }
130
151
  pendingWrites[pendingWrites.length] = deps.writeBehind.persistThreadAsync({
131
152
  jid: threadJid,
132
153
  name: conversation.name ?? undefined,
@@ -135,10 +156,11 @@ async function processHistorySyncNotification(deps, notification) {
135
156
  pinned: conversation.pinned ?? undefined,
136
157
  muteEndMs: (0, primitives_1.longToNumber)(conversation.muteEndTime) || undefined,
137
158
  markedAsUnread: conversation.markedAsUnread ?? undefined,
138
- ephemeralExpiration: conversation.ephemeralExpiration ?? undefined
159
+ ephemeralExpiration,
160
+ ephemeralSettingTimestamp
139
161
  });
140
162
  if (pendingWrites.length >= HISTORY_SYNC_MAX_PENDING_WRITES) {
141
- await flushPendingWrites(pendingWrites);
163
+ await (0, history_blob_1.flushPendingWrites)(pendingWrites);
142
164
  }
143
165
  if ((0, jid_1.isUserJid)(threadJid) || (conversation.lidJid ?? conversation.accountLid)) {
144
166
  const contactDisplay = conversation.displayName || conversation.username || undefined;
@@ -153,7 +175,7 @@ async function processHistorySyncNotification(deps, notification) {
153
175
  lastUpdatedMs: nowMs
154
176
  });
155
177
  if (pendingWrites.length >= HISTORY_SYNC_MAX_PENDING_WRITES) {
156
- await flushPendingWrites(pendingWrites);
178
+ await (0, history_blob_1.flushPendingWrites)(pendingWrites);
157
179
  }
158
180
  }
159
181
  }
@@ -176,7 +198,7 @@ async function processHistorySyncNotification(deps, notification) {
176
198
  messageBytes: _proto_1.proto.Message.encode(webMsg.message).finish()
177
199
  });
178
200
  if (pendingWrites.length >= HISTORY_SYNC_MAX_PENDING_WRITES) {
179
- await flushPendingWrites(pendingWrites);
201
+ await (0, history_blob_1.flushPendingWrites)(pendingWrites);
180
202
  }
181
203
  messagesCount += 1;
182
204
  }
@@ -191,7 +213,7 @@ async function processHistorySyncNotification(deps, notification) {
191
213
  lastUpdatedMs: nowMs
192
214
  });
193
215
  if (pendingWrites.length >= HISTORY_SYNC_MAX_PENDING_WRITES) {
194
- await flushPendingWrites(pendingWrites);
216
+ await (0, history_blob_1.flushPendingWrites)(pendingWrites);
195
217
  }
196
218
  }
197
219
  if (deps.onPrivacyTokens) {
@@ -226,35 +248,15 @@ async function processHistorySyncNotification(deps, notification) {
226
248
  chunkOrder: historySync.chunkOrder ?? undefined,
227
249
  progress: historySync.progress ?? undefined
228
250
  };
229
- await flushPendingWrites(pendingWrites);
251
+ await (0, history_blob_1.flushPendingWrites)(pendingWrites);
230
252
  deps.emitEvent('history_sync_chunk', event);
231
253
  if (deps.onProcessed) {
232
254
  await deps.onProcessed(syncType);
233
255
  }
234
256
  }
235
- async function flushPendingWrites(pendingWrites) {
236
- if (pendingWrites.length === 0) {
237
- return;
238
- }
239
- const settled = Promise.all(pendingWrites);
240
- pendingWrites.length = 0;
241
- await settled;
242
- }
243
257
  async function downloadHistorySyncBlob(deps, notification) {
244
258
  if (notification.initialHistBootstrapInlinePayload) {
245
259
  return (0, bytes_1.decodeProtoBytes)(notification.initialHistBootstrapInlinePayload, 'initialHistBootstrapInlinePayload');
246
260
  }
247
- if (!notification.directPath) {
248
- throw new Error('history sync notification missing directPath');
249
- }
250
- const mediaKey = (0, bytes_1.decodeProtoBytes)(notification.mediaKey, 'history sync mediaKey');
251
- const fileSha256 = (0, bytes_1.decodeProtoBytes)(notification.fileSha256, 'history sync fileSha256');
252
- const fileEncSha256 = (0, bytes_1.decodeProtoBytes)(notification.fileEncSha256, 'history sync fileEncSha256');
253
- return deps.mediaTransfer.downloadAndDecrypt({
254
- directPath: notification.directPath,
255
- mediaType: 'history',
256
- mediaKey,
257
- fileSha256,
258
- fileEncSha256
259
- });
261
+ return (0, history_blob_1.downloadHistoryBlob)(deps.mediaTransfer, notification, 'history', 'history sync');
260
262
  }