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.
- package/dist/abprops-spec.d.ts +10 -0
- package/dist/abprops-spec.js +25 -0
- package/dist/client/WaClient.d.ts +11 -0
- package/dist/client/WaClient.js +60 -0
- package/dist/client/WaClientFactory.d.ts +2 -2
- package/dist/client/WaClientFactory.js +50 -12
- package/dist/client/coordinators/WaAbPropsCoordinator.d.ts +2 -2
- package/dist/client/coordinators/WaAbPropsCoordinator.js +16 -4
- package/dist/client/coordinators/WaIncomingNodeCoordinator.d.ts +2 -1
- package/dist/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
- package/dist/client/coordinators/WaMessageCoordinator.d.ts +96 -0
- package/dist/client/coordinators/WaMessageCoordinator.js +163 -8
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +64 -0
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +217 -30
- package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +97 -19
- package/dist/client/coordinators/WaPrivacyCoordinator.js +152 -101
- package/dist/client/coordinators/WaRetryCoordinator.d.ts +22 -0
- package/dist/client/coordinators/WaRetryCoordinator.js +74 -11
- package/dist/client/events/dirty.d.ts +3 -0
- package/dist/client/events/dirty.js +12 -7
- package/dist/client/events/offline.d.ts +9 -0
- package/dist/client/events/offline.js +95 -0
- package/dist/client/events/privacy.d.ts +45 -0
- package/dist/client/events/privacy.js +130 -0
- package/dist/client/messaging/group-metadata.d.ts +12 -0
- package/dist/client/messaging/group-metadata.js +24 -8
- package/dist/client/messaging/link-preview.d.ts +7 -0
- package/dist/client/messaging/link-preview.js +27 -0
- package/dist/client/newsletter/content.d.ts +4 -1
- package/dist/client/newsletter/content.js +43 -18
- package/dist/client/newsletter/messaging.d.ts +2 -0
- package/dist/client/newsletter/messaging.js +2 -1
- package/dist/client/newsletter/mex.d.ts +2 -2
- package/dist/client/persistence/WriteBehindPersistence.js +2 -1
- package/dist/client/persistence/ephemeral-setting.d.ts +42 -0
- package/dist/client/persistence/ephemeral-setting.js +120 -0
- package/dist/client/persistence/group-history.d.ts +29 -0
- package/dist/client/persistence/group-history.js +149 -0
- package/dist/client/persistence/history-blob.d.ts +25 -0
- package/dist/client/persistence/history-blob.js +38 -0
- package/dist/client/persistence/history-sync.d.ts +2 -0
- package/dist/client/persistence/history-sync.js +31 -29
- package/dist/client/types.d.ts +150 -16
- package/dist/crypto/core/xeddsa.js +16 -0
- package/dist/crypto/curves/X25519.js +13 -0
- package/dist/crypto/nativeBackend.d.ts +20 -0
- package/dist/crypto/nativeBackend.js +95 -0
- package/dist/esm/abprops-spec.js +15 -0
- package/dist/esm/client/WaClient.js +60 -0
- package/dist/esm/client/WaClientFactory.js +51 -13
- package/dist/esm/client/coordinators/WaAbPropsCoordinator.js +17 -5
- package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
- package/dist/esm/client/coordinators/WaMessageCoordinator.js +166 -11
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +218 -32
- package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +145 -94
- package/dist/esm/client/coordinators/WaRetryCoordinator.js +74 -11
- package/dist/esm/client/events/dirty.js +12 -7
- package/dist/esm/client/events/offline.js +92 -0
- package/dist/esm/client/events/privacy.js +123 -0
- package/dist/esm/client/messaging/group-metadata.js +25 -9
- package/dist/esm/client/messaging/link-preview.js +29 -2
- package/dist/esm/client/newsletter/content.js +44 -19
- package/dist/esm/client/newsletter/messaging.js +2 -1
- package/dist/esm/client/persistence/WriteBehindPersistence.js +2 -1
- package/dist/esm/client/persistence/ephemeral-setting.js +116 -0
- package/dist/esm/client/persistence/group-history.js +145 -0
- package/dist/esm/client/persistence/history-blob.js +34 -0
- package/dist/esm/client/persistence/history-sync.js +24 -22
- package/dist/esm/crypto/core/xeddsa.js +17 -1
- package/dist/esm/crypto/curves/X25519.js +13 -0
- package/dist/esm/crypto/nativeBackend.js +93 -0
- package/dist/esm/index.js +3 -1
- package/dist/esm/message/context-info.js +39 -0
- package/dist/esm/message/encode/content.js +20 -4
- package/dist/esm/message/encode/media-payload.js +3 -1
- package/dist/esm/message/kinds/group-history.js +36 -0
- package/dist/esm/message/primitives/incoming.js +18 -6
- package/dist/esm/protocol/abprops.js +18 -162
- package/dist/esm/protocol/constants.js +4 -3
- package/dist/esm/protocol/defaults.js +6 -0
- package/dist/esm/protocol/media.js +1 -0
- package/dist/esm/protocol/message.js +8 -0
- package/dist/esm/protocol/notification.js +1 -0
- package/dist/esm/protocol/privacy.js +70 -4
- package/dist/esm/store/contracts/chat-metadata.store.js +1 -0
- package/dist/esm/store/createStore.js +19 -2
- package/dist/esm/store/index.js +1 -0
- package/dist/esm/store/memory/chat-metadata.store.js +66 -0
- package/dist/esm/store/noop.store.js +8 -0
- package/dist/esm/transport/node/builders/privacy.js +63 -4
- package/dist/esm/util/coercion.js +12 -4
- package/dist/esm/util/index.js +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.js +15 -2
- package/dist/media/types.d.ts +1 -1
- package/dist/message/context-info.d.ts +10 -0
- package/dist/message/context-info.js +40 -0
- package/dist/message/encode/content.d.ts +7 -3
- package/dist/message/encode/content.js +20 -4
- package/dist/message/encode/media-payload.d.ts +5 -2
- package/dist/message/encode/media-payload.js +3 -1
- package/dist/message/kinds/group-history.d.ts +30 -0
- package/dist/message/kinds/group-history.js +40 -0
- package/dist/message/primitives/incoming.d.ts +5 -0
- package/dist/message/primitives/incoming.js +18 -6
- package/dist/protocol/abprops.d.ts +27 -140
- package/dist/protocol/abprops.js +18 -163
- package/dist/protocol/constants.d.ts +4 -3
- package/dist/protocol/constants.js +10 -3
- package/dist/protocol/defaults.d.ts +6 -0
- package/dist/protocol/defaults.js +6 -0
- package/dist/protocol/media.d.ts +1 -0
- package/dist/protocol/media.js +1 -0
- package/dist/protocol/message.d.ts +6 -0
- package/dist/protocol/message.js +9 -0
- package/dist/protocol/notification.d.ts +1 -0
- package/dist/protocol/notification.js +1 -0
- package/dist/protocol/privacy.d.ts +52 -0
- package/dist/protocol/privacy.js +71 -5
- package/dist/store/contracts/chat-metadata.store.d.ts +34 -0
- package/dist/store/contracts/chat-metadata.store.js +2 -0
- package/dist/store/contracts/group-metadata.store.d.ts +7 -0
- package/dist/store/contracts/thread.store.d.ts +6 -0
- package/dist/store/createStore.d.ts +10 -2
- package/dist/store/createStore.js +18 -1
- package/dist/store/index.d.ts +3 -1
- package/dist/store/index.js +3 -1
- package/dist/store/memory/chat-metadata.store.d.ts +26 -0
- package/dist/store/memory/chat-metadata.store.js +70 -0
- package/dist/store/noop.store.d.ts +2 -0
- package/dist/store/noop.store.js +9 -1
- package/dist/store/types.d.ts +190 -39
- package/dist/transport/node/builders/privacy.d.ts +26 -2
- package/dist/transport/node/builders/privacy.js +63 -3
- package/dist/util/coercion.d.ts +6 -0
- package/dist/util/coercion.js +13 -4
- package/dist/util/index.d.ts +1 -1
- package/dist/util/index.js +2 -1
- package/package.json +3 -1
- package/spec/abprops/index.d.ts +4402 -0
- 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
|
|
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
|
-
|
|
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
|
}
|