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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { PromiseDedup } from '../../infra/perf/PromiseDedup.js';
|
|
2
|
+
import { pickIncomingEphemeralSettingTimestamp } from '../../message/context-info.js';
|
|
3
|
+
import { isGroupJid } from '../../protocol/jid.js';
|
|
4
|
+
import { normalizeEphemeralSettingSeconds } from '../../protocol/message.js';
|
|
5
|
+
import { longToNumber, toError } from '../../util/primitives.js';
|
|
6
|
+
/**
|
|
7
|
+
* Persists a 1:1 `EPHEMERAL_SETTING` protocol message so outgoing sends pick up
|
|
8
|
+
* the peer's change without waiting for history sync. Groups are skipped –
|
|
9
|
+
* their timer comes from group notifications, not protocol messages.
|
|
10
|
+
*/
|
|
11
|
+
export function persistIncomingEphemeralSetting(options) {
|
|
12
|
+
const { logger, writeBehind, event, protocolMessage, chatMetadataStore } = options;
|
|
13
|
+
const chatJid = event.key.remoteJid;
|
|
14
|
+
if (!chatJid || event.key.isGroup || isGroupJid(chatJid)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const expiration = protocolMessage.ephemeralExpiration ?? 0;
|
|
18
|
+
const rawTimestamp = protocolMessage.ephemeralSettingTimestamp !== undefined &&
|
|
19
|
+
protocolMessage.ephemeralSettingTimestamp !== null
|
|
20
|
+
? longToNumber(protocolMessage.ephemeralSettingTimestamp)
|
|
21
|
+
: event.timestampSeconds;
|
|
22
|
+
const settingTimestamp = rawTimestamp !== undefined ? normalizeEphemeralSettingSeconds(rawTimestamp) : undefined;
|
|
23
|
+
void chatMetadataStore
|
|
24
|
+
.upsertChatMetadata({
|
|
25
|
+
chatJid,
|
|
26
|
+
ephemeralExpiration: expiration,
|
|
27
|
+
...(settingTimestamp !== undefined
|
|
28
|
+
? { ephemeralSettingTimestamp: settingTimestamp }
|
|
29
|
+
: {}),
|
|
30
|
+
updatedAtMs: Date.now()
|
|
31
|
+
})
|
|
32
|
+
.catch((error) => {
|
|
33
|
+
logger.debug('failed to cache incoming ephemeral setting', {
|
|
34
|
+
jid: chatJid,
|
|
35
|
+
message: toError(error).message
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
writeBehind.persistThread({
|
|
40
|
+
jid: chatJid,
|
|
41
|
+
ephemeralExpiration: expiration,
|
|
42
|
+
...(settingTimestamp !== undefined
|
|
43
|
+
? { ephemeralSettingTimestamp: settingTimestamp }
|
|
44
|
+
: {})
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
logger.warn('failed to persist incoming ephemeral setting', {
|
|
49
|
+
jid: chatJid,
|
|
50
|
+
expiration,
|
|
51
|
+
message: toError(error).message
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Builds the inbound observer that refreshes the chat-metadata cache from
|
|
57
|
+
* ordinary traffic. A peer stamps `contextInfo.ephemeralSettingTimestamp` on
|
|
58
|
+
* every message in a disappearing chat, making it the only continuously
|
|
59
|
+
* refreshed source - history sync and `EPHEMERAL_SETTING` are point-in-time.
|
|
60
|
+
*
|
|
61
|
+
* Never blocks and never rejects: the store round-trip is fire-and-forget.
|
|
62
|
+
* Messages repeating a value share one round-trip, so the batch an offline
|
|
63
|
+
* resume delivers costs a single read instead of one per message, while a
|
|
64
|
+
* message carrying a *different* value still gets its own round-trip rather
|
|
65
|
+
* than being swallowed by the one in flight.
|
|
66
|
+
*
|
|
67
|
+
* A write skips a setting older than the cached one, mirroring how the peer
|
|
68
|
+
* resolves it: highest timestamp wins. That check is not atomic with the
|
|
69
|
+
* write, so two round-trips that both read a cold cache can still land out of
|
|
70
|
+
* order and leave the older value behind. The next message from the chat
|
|
71
|
+
* corrects it, and a stale-but-older stamp loses to the peer's own value
|
|
72
|
+
* anyway, so this is not worth serializing the receive path over.
|
|
73
|
+
*/
|
|
74
|
+
export function createEphemeralObserver(options) {
|
|
75
|
+
const { logger, chatMetadataStore } = options;
|
|
76
|
+
const dedup = new PromiseDedup();
|
|
77
|
+
return (event) => {
|
|
78
|
+
const expiration = event.expirationSeconds;
|
|
79
|
+
if (expiration === undefined || expiration <= 0) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const chatJid = event.key.remoteJid;
|
|
83
|
+
if (!chatJid || event.key.isGroup || event.key.isNewsletter || isGroupJid(chatJid)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const raw = pickIncomingEphemeralSettingTimestamp(event.message);
|
|
87
|
+
if (raw === undefined) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const settingTimestamp = normalizeEphemeralSettingSeconds(raw);
|
|
91
|
+
void dedup
|
|
92
|
+
.run(`observe:${chatJid}:${expiration}:${settingTimestamp}`, async () => {
|
|
93
|
+
const cached = await chatMetadataStore.getChatMetadata(chatJid);
|
|
94
|
+
if (cached?.ephemeralSettingTimestamp === settingTimestamp &&
|
|
95
|
+
cached.ephemeralExpiration === expiration) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (cached?.ephemeralSettingTimestamp !== undefined &&
|
|
99
|
+
cached.ephemeralSettingTimestamp > settingTimestamp) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await chatMetadataStore.upsertChatMetadata({
|
|
103
|
+
chatJid,
|
|
104
|
+
ephemeralExpiration: expiration,
|
|
105
|
+
ephemeralSettingTimestamp: settingTimestamp,
|
|
106
|
+
updatedAtMs: Date.now()
|
|
107
|
+
});
|
|
108
|
+
})
|
|
109
|
+
.catch((error) => {
|
|
110
|
+
logger.debug('failed to cache observed ephemeral setting', {
|
|
111
|
+
jid: chatJid,
|
|
112
|
+
message: toError(error).message
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { downloadHistoryBlob, flushPendingWrites } from '../persistence/history-blob.js';
|
|
2
|
+
import { decodeGroupHistoryBundle } from '../../message/kinds/group-history.js';
|
|
3
|
+
import { proto } from '../../proto.js';
|
|
4
|
+
import { isGroupJid, toUserJid } from '../../protocol/jid.js';
|
|
5
|
+
import { longToNumber, toError } from '../../util/primitives.js';
|
|
6
|
+
const GROUP_HISTORY_MAX_PENDING_WRITES = 1024;
|
|
7
|
+
export async function runGroupHistoryBundle(deps, input) {
|
|
8
|
+
try {
|
|
9
|
+
await processGroupHistoryBundle(deps, input);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
deps.logger.warn('failed to process group history bundle', {
|
|
13
|
+
groupJid: input.groupJid,
|
|
14
|
+
id: input.bundleMessageId,
|
|
15
|
+
message: toError(error).message
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function processGroupHistoryBundle(deps, input) {
|
|
20
|
+
const logger = deps.logger.child({
|
|
21
|
+
groupJid: input.groupJid,
|
|
22
|
+
id: input.bundleMessageId
|
|
23
|
+
});
|
|
24
|
+
if (!isGroupJid(input.groupJid)) {
|
|
25
|
+
logger.debug('skipping group history bundle outside a group');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!isHistoryReceiver(deps, input.bundle)) {
|
|
29
|
+
logger.debug('skipping group history bundle not addressed to this account');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const nowMs = Date.now();
|
|
33
|
+
const bundleTtlSeconds = deps.getAbPropNumber('group_history_bundle_time_limit_receiver_enforcement_secs');
|
|
34
|
+
if (input.sentAtSeconds !== undefined &&
|
|
35
|
+
nowMs > (input.sentAtSeconds + bundleTtlSeconds) * 1000) {
|
|
36
|
+
logger.debug('skipping expired group history bundle', {
|
|
37
|
+
sentAtSeconds: input.sentAtSeconds,
|
|
38
|
+
bundleTtlSeconds
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const blob = await downloadHistoryBlob(deps.mediaTransfer, input.bundle, 'group-history', 'group history bundle');
|
|
43
|
+
const history = await decodeGroupHistoryBundle(blob);
|
|
44
|
+
const messageTtlSeconds = deps.getAbPropNumber('group_history_messages_time_limit_receiver_enforcement_secs');
|
|
45
|
+
const pendingWrites = [];
|
|
46
|
+
const dropped = { foreignChat: 0, ephemeralExpired: 0, tooOld: 0, stub: 0 };
|
|
47
|
+
let messagesCount = 0;
|
|
48
|
+
let oldestTimestampMs;
|
|
49
|
+
for (const source of [
|
|
50
|
+
{ messages: history.messages, skipAgeCheck: false },
|
|
51
|
+
{ messages: history.outOfWindowPinnedMessages, skipAgeCheck: true }
|
|
52
|
+
]) {
|
|
53
|
+
for (const webMsg of source.messages) {
|
|
54
|
+
if (!webMsg.key?.id || !webMsg.message) {
|
|
55
|
+
dropped.stub += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (webMsg.key.remoteJid && webMsg.key.remoteJid !== input.groupJid) {
|
|
59
|
+
dropped.foreignChat += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const timestampSeconds = longToNumber(webMsg.messageTimestamp);
|
|
63
|
+
if (isEphemeralExpired(webMsg, timestampSeconds, nowMs)) {
|
|
64
|
+
dropped.ephemeralExpired += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!source.skipAgeCheck &&
|
|
68
|
+
timestampSeconds > 0 &&
|
|
69
|
+
(timestampSeconds + 2 * messageTtlSeconds) * 1000 < nowMs) {
|
|
70
|
+
dropped.tooOld += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const timestampMs = timestampSeconds * 1000;
|
|
74
|
+
if (timestampMs > 0 &&
|
|
75
|
+
(oldestTimestampMs === undefined || timestampMs < oldestTimestampMs)) {
|
|
76
|
+
oldestTimestampMs = timestampMs;
|
|
77
|
+
}
|
|
78
|
+
pendingWrites[pendingWrites.length] = deps.writeBehind.persistMessageAsync({
|
|
79
|
+
id: webMsg.key.id,
|
|
80
|
+
threadJid: input.groupJid,
|
|
81
|
+
senderJid: webMsg.key.participant ?? undefined,
|
|
82
|
+
fromMe: webMsg.key.fromMe === true,
|
|
83
|
+
timestampMs: timestampMs || undefined,
|
|
84
|
+
messageBytes: proto.Message.encode(webMsg.message).finish()
|
|
85
|
+
});
|
|
86
|
+
if (pendingWrites.length >= GROUP_HISTORY_MAX_PENDING_WRITES) {
|
|
87
|
+
await flushPendingWrites(pendingWrites);
|
|
88
|
+
}
|
|
89
|
+
messagesCount += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await flushPendingWrites(pendingWrites);
|
|
93
|
+
const droppedCount = dropped.foreignChat + dropped.ephemeralExpired + dropped.tooOld + dropped.stub;
|
|
94
|
+
logger.debug('processed group history bundle', {
|
|
95
|
+
messagesCount,
|
|
96
|
+
outOfWindowPinsCount: history.outOfWindowPinnedMessages.length,
|
|
97
|
+
droppedCount,
|
|
98
|
+
dropped
|
|
99
|
+
});
|
|
100
|
+
const event = {
|
|
101
|
+
groupJid: input.groupJid,
|
|
102
|
+
senderJid: input.senderJid,
|
|
103
|
+
bundleMessageId: input.bundleMessageId,
|
|
104
|
+
messagesCount,
|
|
105
|
+
outOfWindowPinsCount: history.outOfWindowPinnedMessages.length,
|
|
106
|
+
droppedCount,
|
|
107
|
+
oldestTimestampMs,
|
|
108
|
+
historyReceivers: [...(input.bundle.messageHistoryMetadata?.historyReceivers ?? [])]
|
|
109
|
+
};
|
|
110
|
+
deps.emitEvent('group_history_bundle', event);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A bundle is only ours to process when this account is listed in
|
|
114
|
+
* `historyReceivers`. The list may address us by either PN or LID, so both
|
|
115
|
+
* forms of the local identity are compared.
|
|
116
|
+
*/
|
|
117
|
+
function isHistoryReceiver(deps, bundle) {
|
|
118
|
+
const receivers = bundle.messageHistoryMetadata?.historyReceivers;
|
|
119
|
+
if (!receivers || receivers.length === 0) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const meUserJid = deps.meJid ? toUserJid(deps.meJid) : null;
|
|
123
|
+
const meUserLid = deps.meLid ? toUserJid(deps.meLid) : null;
|
|
124
|
+
if (!meUserJid && !meUserLid) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
for (let index = 0; index < receivers.length; index += 1) {
|
|
128
|
+
const receiver = toUserJid(receivers[index]);
|
|
129
|
+
if (receiver === meUserJid || receiver === meUserLid) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
function isEphemeralExpired(webMsg, timestampSeconds, nowMs) {
|
|
136
|
+
const duration = webMsg.ephemeralDuration;
|
|
137
|
+
if (!duration) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const startSeconds = longToNumber(webMsg.ephemeralStartTimestamp) || timestampSeconds;
|
|
141
|
+
if (startSeconds <= 0) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return (startSeconds + duration) * 1000 <= nowMs;
|
|
145
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { decodeProtoBytes } from '../../util/bytes.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fetches and decrypts a history blob. `label` prefixes the field errors so a
|
|
4
|
+
* failure names which payload was malformed.
|
|
5
|
+
*
|
|
6
|
+
* @throws when `directPath` is absent or a key/hash field is missing.
|
|
7
|
+
*/
|
|
8
|
+
export async function downloadHistoryBlob(mediaTransfer, source, mediaType, label) {
|
|
9
|
+
if (!source.directPath) {
|
|
10
|
+
throw new Error(`${label} missing directPath`);
|
|
11
|
+
}
|
|
12
|
+
const mediaKey = decodeProtoBytes(source.mediaKey, `${label} mediaKey`);
|
|
13
|
+
const fileSha256 = decodeProtoBytes(source.fileSha256, `${label} fileSha256`);
|
|
14
|
+
const fileEncSha256 = decodeProtoBytes(source.fileEncSha256, `${label} fileEncSha256`);
|
|
15
|
+
return mediaTransfer.downloadAndDecrypt({
|
|
16
|
+
directPath: source.directPath,
|
|
17
|
+
mediaType,
|
|
18
|
+
mediaKey,
|
|
19
|
+
fileSha256,
|
|
20
|
+
fileEncSha256
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Awaits an accumulating write batch and empties it in place, so the caller can
|
|
25
|
+
* keep filling the same array instead of allocating a new one per chunk.
|
|
26
|
+
*/
|
|
27
|
+
export async function flushPendingWrites(pendingWrites) {
|
|
28
|
+
if (pendingWrites.length === 0) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const settled = Promise.all(pendingWrites);
|
|
32
|
+
pendingWrites.length = 0;
|
|
33
|
+
await settled;
|
|
34
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { promisify } from 'node:util';
|
|
2
2
|
import { unzip } from 'node:zlib';
|
|
3
|
+
import { downloadHistoryBlob, flushPendingWrites } from '../persistence/history-blob.js';
|
|
3
4
|
import { proto } from '../../proto.js';
|
|
4
5
|
import { isUserJid } from '../../protocol/jid.js';
|
|
6
|
+
import { normalizeEphemeralSettingSeconds } from '../../protocol/message.js';
|
|
5
7
|
import { decodeProtoBytes, toBytesView } from '../../util/bytes.js';
|
|
6
8
|
import { longToNumber, toError } from '../../util/primitives.js';
|
|
7
9
|
const unzipAsync = promisify(unzip);
|
|
@@ -123,6 +125,25 @@ export async function processHistorySyncNotification(deps, notification) {
|
|
|
123
125
|
deps.logger.debug('skipping history sync conversation without thread jid');
|
|
124
126
|
continue;
|
|
125
127
|
}
|
|
128
|
+
const ephemeralExpiration = conversation.ephemeralExpiration ?? undefined;
|
|
129
|
+
const ephemeralSettingTimestamp = normalizeEphemeralSettingSeconds(longToNumber(conversation.ephemeralSettingTimestamp)) || undefined;
|
|
130
|
+
if (deps.chatMetadataStore && ephemeralExpiration !== undefined) {
|
|
131
|
+
pendingWrites[pendingWrites.length] = deps.chatMetadataStore
|
|
132
|
+
.upsertChatMetadata({
|
|
133
|
+
chatJid: threadJid,
|
|
134
|
+
ephemeralExpiration,
|
|
135
|
+
...(ephemeralSettingTimestamp !== undefined
|
|
136
|
+
? { ephemeralSettingTimestamp }
|
|
137
|
+
: {}),
|
|
138
|
+
updatedAtMs: nowMs
|
|
139
|
+
})
|
|
140
|
+
.catch((error) => {
|
|
141
|
+
deps.logger.debug('failed to cache history sync chat metadata', {
|
|
142
|
+
jid: threadJid,
|
|
143
|
+
message: toError(error).message
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
126
147
|
pendingWrites[pendingWrites.length] = deps.writeBehind.persistThreadAsync({
|
|
127
148
|
jid: threadJid,
|
|
128
149
|
name: conversation.name ?? undefined,
|
|
@@ -131,7 +152,8 @@ export async function processHistorySyncNotification(deps, notification) {
|
|
|
131
152
|
pinned: conversation.pinned ?? undefined,
|
|
132
153
|
muteEndMs: longToNumber(conversation.muteEndTime) || undefined,
|
|
133
154
|
markedAsUnread: conversation.markedAsUnread ?? undefined,
|
|
134
|
-
ephemeralExpiration
|
|
155
|
+
ephemeralExpiration,
|
|
156
|
+
ephemeralSettingTimestamp
|
|
135
157
|
});
|
|
136
158
|
if (pendingWrites.length >= HISTORY_SYNC_MAX_PENDING_WRITES) {
|
|
137
159
|
await flushPendingWrites(pendingWrites);
|
|
@@ -228,29 +250,9 @@ export async function processHistorySyncNotification(deps, notification) {
|
|
|
228
250
|
await deps.onProcessed(syncType);
|
|
229
251
|
}
|
|
230
252
|
}
|
|
231
|
-
async function flushPendingWrites(pendingWrites) {
|
|
232
|
-
if (pendingWrites.length === 0) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const settled = Promise.all(pendingWrites);
|
|
236
|
-
pendingWrites.length = 0;
|
|
237
|
-
await settled;
|
|
238
|
-
}
|
|
239
253
|
async function downloadHistorySyncBlob(deps, notification) {
|
|
240
254
|
if (notification.initialHistBootstrapInlinePayload) {
|
|
241
255
|
return decodeProtoBytes(notification.initialHistBootstrapInlinePayload, 'initialHistBootstrapInlinePayload');
|
|
242
256
|
}
|
|
243
|
-
|
|
244
|
-
throw new Error('history sync notification missing directPath');
|
|
245
|
-
}
|
|
246
|
-
const mediaKey = decodeProtoBytes(notification.mediaKey, 'history sync mediaKey');
|
|
247
|
-
const fileSha256 = decodeProtoBytes(notification.fileSha256, 'history sync fileSha256');
|
|
248
|
-
const fileEncSha256 = decodeProtoBytes(notification.fileEncSha256, 'history sync fileEncSha256');
|
|
249
|
-
return deps.mediaTransfer.downloadAndDecrypt({
|
|
250
|
-
directPath: notification.directPath,
|
|
251
|
-
mediaType: 'history',
|
|
252
|
-
mediaKey,
|
|
253
|
-
fileSha256,
|
|
254
|
-
fileEncSha256
|
|
255
|
-
});
|
|
257
|
+
return downloadHistoryBlob(deps.mediaTransfer, notification, 'history', 'history sync');
|
|
256
258
|
}
|
|
@@ -5,17 +5,30 @@ import { clampCurvePrivateKeyInPlace, montgomeryToEdwardsPublic } from '../curve
|
|
|
5
5
|
import { encodeExtendedPoint, scalarMultBase } from '../math/edwards.js';
|
|
6
6
|
import { bigIntToBytesLE, bytesToBigIntLE } from '../math/le.js';
|
|
7
7
|
import { modGroup } from '../math/mod.js';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveNativeCryptoBackend } from '../nativeBackend.js';
|
|
9
|
+
import { assertByteLength, concatBytes, toBytesView } from '../../util/bytes.js';
|
|
9
10
|
const PREFIX_SIGNATURE_RANDOM = new Uint8Array([
|
|
10
11
|
0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
11
12
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
|
|
12
13
|
]);
|
|
14
|
+
const nativeBinding = (() => {
|
|
15
|
+
if (process.env.ZAPO_XEDDSA_FORCE_JS)
|
|
16
|
+
return null;
|
|
17
|
+
const mod = resolveNativeCryptoBackend();
|
|
18
|
+
if (mod && typeof mod.xeddsaSign === 'function' && typeof mod.xeddsaVerify === 'function') {
|
|
19
|
+
return { xeddsaSign: mod.xeddsaSign, xeddsaVerify: mod.xeddsaVerify };
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
})();
|
|
13
23
|
/**
|
|
14
24
|
* Verifies an XEdDSA signature over `message` against an X25519 (Montgomery)
|
|
15
25
|
* public key, converting to the Edwards form internally. Returns `false`
|
|
16
26
|
* on shape mismatch instead of throwing.
|
|
17
27
|
*/
|
|
18
28
|
export async function xeddsaVerify(curvePublicKey, message, signature) {
|
|
29
|
+
if (nativeBinding) {
|
|
30
|
+
return nativeBinding.xeddsaVerify(curvePublicKey, message, signature);
|
|
31
|
+
}
|
|
19
32
|
if (signature.length !== 64) {
|
|
20
33
|
return false;
|
|
21
34
|
}
|
|
@@ -40,6 +53,9 @@ export async function xeddsaVerify(curvePublicKey, message, signature) {
|
|
|
40
53
|
*/
|
|
41
54
|
export async function xeddsaSign(privateKey, message) {
|
|
42
55
|
assertByteLength(privateKey, 32, `invalid curve25519 private key length ${privateKey.length}`);
|
|
56
|
+
if (nativeBinding) {
|
|
57
|
+
return toBytesView(nativeBinding.xeddsaSign(privateKey, message));
|
|
58
|
+
}
|
|
43
59
|
const clampedPrivateKey = clampCurvePrivateKeyInPlace(privateKey);
|
|
44
60
|
const privateScalar = bytesToBigIntLE(clampedPrivateKey);
|
|
45
61
|
const encodedPublic = encodeExtendedPoint(scalarMultBase(privateScalar));
|
|
@@ -5,7 +5,17 @@ import { X25519_PKCS8_PREFIX, X25519_SPKI_PREFIX } from '../curves/constants.js'
|
|
|
5
5
|
import { pkcs8FromRawPrivate } from '../curves/types.js';
|
|
6
6
|
import { FE_ONE } from '../math/constants.js';
|
|
7
7
|
import { fe, feAdd, feFromBytes, feInv, feMul, fePack, feSub } from '../math/fe.js';
|
|
8
|
+
import { resolveNativeCryptoBackend } from '../nativeBackend.js';
|
|
8
9
|
import { assertByteLength, concatBytes, decodeBase64Url, toBytesView } from '../../util/bytes.js';
|
|
10
|
+
const nativeX25519ScalarMult = (() => {
|
|
11
|
+
if (process.env.ZAPO_X25519_FORCE_JS)
|
|
12
|
+
return null;
|
|
13
|
+
const mod = resolveNativeCryptoBackend();
|
|
14
|
+
if (mod && typeof mod.x25519ScalarMult === 'function') {
|
|
15
|
+
return mod.x25519ScalarMult;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
})();
|
|
9
19
|
const diffieHellmanWithCallback = diffieHellman;
|
|
10
20
|
let diffieHellmanAsync = null;
|
|
11
21
|
let diffieHellmanAsyncProbed = false;
|
|
@@ -115,6 +125,9 @@ export class X25519 {
|
|
|
115
125
|
static async scalarMult(privKey, pubKey) {
|
|
116
126
|
assertByteLength(privKey, 32, 'x25519 private key must be 32 bytes');
|
|
117
127
|
assertByteLength(pubKey, 32, 'x25519 public key must be 32 bytes');
|
|
128
|
+
if (nativeX25519ScalarMult) {
|
|
129
|
+
return toBytesView(nativeX25519ScalarMult(privKey, pubKey));
|
|
130
|
+
}
|
|
118
131
|
const opts = {
|
|
119
132
|
privateKey: x25519PrivateKeyObject(privKey),
|
|
120
133
|
publicKey: x25519PublicKeyObject(pubKey)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
const require = createRequire(import.meta.url)
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the optional native crypto accelerator backend shared by the
|
|
5
|
+
* X25519 and XEdDSA paths. Selected via `ZAPO_NATIVE_BACKEND`:
|
|
6
|
+
*
|
|
7
|
+
* - `auto` (default) – try the NAPI addon first, then the WASM build,
|
|
8
|
+
* then no accelerator.
|
|
9
|
+
* - `napi` – only the compiled Rust NAPI addon (`@zapo-js/native`).
|
|
10
|
+
* - `wasm` – only the wasm-bindgen build of the same Rust core.
|
|
11
|
+
* The shipped artifact targets the web (ESM glue +
|
|
12
|
+
* explicit init) so one build serves browsers and
|
|
13
|
+
* Node; loading it here relies on `require(esm)`,
|
|
14
|
+
* available unflagged on Node 20.19+ / 22.12+.
|
|
15
|
+
* - `js` / `none` – no accelerator; callers fall back to the pure-JS
|
|
16
|
+
* (and `node:crypto`) implementations.
|
|
17
|
+
*
|
|
18
|
+
* The module is resolved once and memoised. A failed backend resolves to
|
|
19
|
+
* `null` (or, under `auto`, falls through to the next one) so callers
|
|
20
|
+
* transparently fall back to JS. An absent backend stays silent - that is
|
|
21
|
+
* the expected shape of an optional accelerator - but a present-yet-broken
|
|
22
|
+
* one (corrupt binary, ABI mismatch, unsupported runtime) emits a process
|
|
23
|
+
* warning with code `ZAPO_NATIVE_LOAD_FAILED` so the degradation is
|
|
24
|
+
* visible. The per-primitive `ZAPO_X25519_FORCE_JS` / `ZAPO_XEDDSA_FORCE_JS`
|
|
25
|
+
* escape hatches are honoured by the callers, not here.
|
|
26
|
+
*/
|
|
27
|
+
import { readFileSync } from 'node:fs';
|
|
28
|
+
import { toError } from '../util/primitives.js';
|
|
29
|
+
const WASM_MODULE_SPECIFIER = '@zapo-js/native/wasm/pkg/zapo_native_wasm.js';
|
|
30
|
+
const WASM_BINARY_SPECIFIER = '@zapo-js/native/wasm/pkg/zapo_native_wasm_bg.wasm';
|
|
31
|
+
function loadWasmBackend() {
|
|
32
|
+
const mod = require(WASM_MODULE_SPECIFIER);
|
|
33
|
+
const wasmBytes = readFileSync(require.resolve(WASM_BINARY_SPECIFIER));
|
|
34
|
+
mod.initSync({ module: wasmBytes });
|
|
35
|
+
return mod;
|
|
36
|
+
}
|
|
37
|
+
let cached;
|
|
38
|
+
const ABSENCE_TOKENS = {
|
|
39
|
+
napi: ["'@zapo-js/native'", 'binding.js', 'Cannot find native binding'],
|
|
40
|
+
wasm: ["'@zapo-js/native'", 'zapo_native_wasm.js']
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Classifies a backend load failure: `true` means the backend is simply not
|
|
44
|
+
* available on this install (missing optional package, entry file absent,
|
|
45
|
+
* no prebuilt binary, ESM build without `require`) and the fallback should
|
|
46
|
+
* stay silent; `false` means the backend is present but broken and the
|
|
47
|
+
* degradation deserves a process warning.
|
|
48
|
+
*/
|
|
49
|
+
export function isBackendAbsence(backend, error) {
|
|
50
|
+
if (error instanceof ReferenceError) {
|
|
51
|
+
return error.message.includes('require is not defined');
|
|
52
|
+
}
|
|
53
|
+
const { code, message } = (error ?? {});
|
|
54
|
+
if (typeof message !== 'string')
|
|
55
|
+
return false;
|
|
56
|
+
const notFound = code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND';
|
|
57
|
+
if (!notFound && !message.includes('Cannot find native binding'))
|
|
58
|
+
return false;
|
|
59
|
+
return ABSENCE_TOKENS[backend].some((token) => message.includes(token));
|
|
60
|
+
}
|
|
61
|
+
function warnLoadFailure(backend, error) {
|
|
62
|
+
process.emitWarning('failed to load native crypto accelerator, falling back', {
|
|
63
|
+
code: 'ZAPO_NATIVE_LOAD_FAILED',
|
|
64
|
+
detail: `${backend}: ${toError(error).message}`
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function resolveNativeCryptoBackend() {
|
|
68
|
+
if (cached !== undefined)
|
|
69
|
+
return cached;
|
|
70
|
+
const backend = (process.env.ZAPO_NATIVE_BACKEND ?? 'auto').toLowerCase();
|
|
71
|
+
cached = null;
|
|
72
|
+
if (backend === 'js' || backend === 'none')
|
|
73
|
+
return cached;
|
|
74
|
+
if (backend === 'napi' || backend === 'auto') {
|
|
75
|
+
try {
|
|
76
|
+
cached = require('@zapo-js/native');
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (!isBackendAbsence('napi', error))
|
|
80
|
+
warnLoadFailure('napi', error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (cached === null && (backend === 'wasm' || backend === 'auto')) {
|
|
84
|
+
try {
|
|
85
|
+
cached = loadWasmBackend();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (!isBackendAbsence('wasm', error))
|
|
89
|
+
warnLoadFailure('wasm', error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return cached;
|
|
93
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { createFileCompanionHostPersistence } from './client/persistence/compani
|
|
|
7
7
|
export { parseUsyncResultEnvelope } from './transport/node/builders/usync.js';
|
|
8
8
|
export { getContentType, resolveEncMediaType, resolveMessageTarget, unwrapMessage } from './message/encode/content.js';
|
|
9
9
|
export { getContextInfo } from './message/context-info.js';
|
|
10
|
+
export { decodeGroupHistoryBundle, encodeGroupHistoryBundle } from './message/kinds/group-history.js';
|
|
10
11
|
export { resolveMediaPayload } from './message/encode/media-payload.js';
|
|
11
12
|
export { unpadPkcs7, writeRandomPadMax16 } from './message/encode/padding.js';
|
|
12
13
|
export { fetchLatestWaWebVersion, fetchLatestWaMobileVersion } from './transport/wa-version-fetcher.js';
|
|
@@ -14,7 +15,8 @@ export { ConsoleLogger } from './infra/log/ConsoleLogger.js';
|
|
|
14
15
|
export { PinoLogger, createPinoLogger } from './infra/log/PinoLogger.js';
|
|
15
16
|
export { createNoopLogger } from './infra/log/types.js';
|
|
16
17
|
export { WA_VERSION } from './version-spec.js';
|
|
18
|
+
export { resolveAbPropNameByCode, WA_ABPROPS, WA_ABPROPS_BY_CODE, WA_ABPROPS_SPECIAL_EARLY, WA_ABPROPS_USED_BEFORE_INIT, WA_GROUP_ABPROPS, WA_GROUP_ABPROPS_BY_CODE } from './abprops-spec.js';
|
|
17
19
|
export { createStore, WaAuthMemoryStore } from './store/index.js';
|
|
18
20
|
export { delay } from './util/async.js';
|
|
19
|
-
export { buildDeviceJid, canonicalizeSignalJid, canonicalizeSignalServer, getLoginIdentity, getWaCompanionPlatformId, getWaMediaHkdfInfo, isBotJid, isBroadcastJid, isGroupJid, isGroupOrBroadcastJid, isHostedDeviceId, isHostedDeviceJid, isHostedServer, isLidJid, isNewsletterJid, isStatusBroadcastJid, isUserJid, normalizeDeviceJid, normalizeRecipientJid, parseJidFull, parsePhoneJid, parseSignalAddressFromJid, signalAddressKey, splitJid, toUserJid, WA_ACCOUNT_SYNC_PROTOCOLS, WA_APP_STATE_COLLECTIONS, WA_APP_STATE_COLLECTION_STATES, WA_APP_STATE_ERROR_CODES, WA_APP_STATE_KDF_INFO, WA_APP_STATE_KEY_TYPES, WA_APP_STATE_SYNC_DATA_TYPE, WA_BROWSERS, WA_BUSINESS_HOURS_DAYS, WA_BUSINESS_HOURS_MODES, WA_COMPANION_PLATFORM_IDS, WA_DEFAULTS, WA_DIRTY_PROTOCOLS, WA_DIRTY_TYPES, WA_DISCONNECT_REASONS, WA_IQ_TYPES, WA_LOGOUT_REASONS, WA_MESSAGE_TAGS, WA_MESSAGE_TYPES, WA_MEDIA_HKDF_INFO, WA_NODE_TAGS, WA_PAIRING_KDF_INFO, WA_PREVIEW_MEDIA_HKDF_INFO, WA_READY_STATES, WA_RETRYABLE_ACK_CODES, WA_SIGNALING, WA_STREAM_SIGNALING, WA_SUPPORTED_DIRTY_TYPES, WA_PRIVACY_CATEGORIES, WA_PRIVACY_CATEGORY_TO_SETTING, WA_PRIVACY_DISALLOWED_LIST_CATEGORIES, WA_PRIVACY_SETTING_TO_CATEGORY, WA_PRIVACY_TAGS, WA_PRIVACY_VALUES, WA_XMLNS } from './protocol/index.js';
|
|
21
|
+
export { buildDeviceJid, canonicalizeSignalJid, canonicalizeSignalServer, getLoginIdentity, getWaCompanionPlatformId, getWaMediaHkdfInfo, isBotJid, isBroadcastJid, isGroupJid, isGroupOrBroadcastJid, isHostedDeviceId, isHostedDeviceJid, isHostedServer, isLidJid, isNewsletterJid, isStatusBroadcastJid, isUserJid, normalizeDeviceJid, normalizeRecipientJid, parseJidFull, parsePhoneJid, parseSignalAddressFromJid, signalAddressKey, splitJid, toUserJid, WA_ACCOUNT_SYNC_PROTOCOLS, WA_APP_STATE_COLLECTIONS, WA_APP_STATE_COLLECTION_STATES, WA_APP_STATE_ERROR_CODES, WA_APP_STATE_KDF_INFO, WA_APP_STATE_KEY_TYPES, WA_APP_STATE_SYNC_DATA_TYPE, WA_BROWSERS, WA_BUSINESS_HOURS_DAYS, WA_BUSINESS_HOURS_MODES, WA_COMPANION_PLATFORM_IDS, WA_DEFAULTS, WA_DIRTY_PROTOCOLS, WA_DIRTY_TYPES, WA_DISCONNECT_REASONS, WA_IQ_TYPES, WA_LOGOUT_REASONS, WA_MESSAGE_TAGS, WA_MESSAGE_TYPES, WA_MEDIA_HKDF_INFO, WA_NODE_TAGS, WA_PAIRING_KDF_INFO, WA_PREVIEW_MEDIA_HKDF_INFO, WA_READY_STATES, WA_RETRYABLE_ACK_CODES, WA_SIGNALING, WA_STREAM_SIGNALING, WA_SUPPORTED_DIRTY_TYPES, WA_PRIVACY_ACCOUNT_SYNC_DISALLOWED_LISTS, WA_PRIVACY_CATEGORIES, WA_PRIVACY_CATEGORY_TO_SETTING, WA_PRIVACY_DISALLOWED_LIST_CATEGORIES, WA_PRIVACY_SETTING_TO_CATEGORY, WA_PRIVACY_SETTING_VALUES, WA_PRIVACY_TAGS, WA_PRIVACY_VALUES, WA_XMLNS } from './protocol/index.js';
|
|
20
22
|
export { proto } from './proto.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isGroupOrBroadcastJid, toUserJid } from '../protocol/jid.js';
|
|
2
|
+
import { longToNumber } from '../util/primitives.js';
|
|
2
3
|
export function buildContextInfoProto(input) {
|
|
3
4
|
const ctx = {};
|
|
4
5
|
if (input.quotedMessageId !== undefined)
|
|
@@ -20,6 +21,20 @@ export function buildContextInfoProto(input) {
|
|
|
20
21
|
ctx.isSpoiler = input.isSpoiler;
|
|
21
22
|
if (input.expirationSeconds !== undefined)
|
|
22
23
|
ctx.expiration = input.expirationSeconds;
|
|
24
|
+
if (input.ephemeralSettingTimestamp !== undefined) {
|
|
25
|
+
ctx.ephemeralSettingTimestamp = input.ephemeralSettingTimestamp;
|
|
26
|
+
}
|
|
27
|
+
if (input.disappearingModeInitiator !== undefined ||
|
|
28
|
+
input.disappearingModeTrigger !== undefined) {
|
|
29
|
+
ctx.disappearingMode = {
|
|
30
|
+
...(input.disappearingModeInitiator !== undefined
|
|
31
|
+
? { initiator: input.disappearingModeInitiator }
|
|
32
|
+
: {}),
|
|
33
|
+
...(input.disappearingModeTrigger !== undefined
|
|
34
|
+
? { trigger: input.disappearingModeTrigger }
|
|
35
|
+
: {})
|
|
36
|
+
};
|
|
37
|
+
}
|
|
23
38
|
if (input.groupSubject !== undefined)
|
|
24
39
|
ctx.groupSubject = input.groupSubject;
|
|
25
40
|
if (input.parentGroupJid !== undefined)
|
|
@@ -73,6 +88,30 @@ export function pickIncomingExpirationSeconds(message) {
|
|
|
73
88
|
}
|
|
74
89
|
return undefined;
|
|
75
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Reads `contextInfo.ephemeralSettingTimestamp` (Unix seconds) from the first
|
|
93
|
+
* submessage that carries it. Peers stamp it on every message in a disappearing
|
|
94
|
+
* chat, which makes it the only continuously-refreshed source for the setting -
|
|
95
|
+
* history sync and `EPHEMERAL_SETTING` are point-in-time.
|
|
96
|
+
*/
|
|
97
|
+
export function pickIncomingEphemeralSettingTimestamp(message) {
|
|
98
|
+
if (!message)
|
|
99
|
+
return undefined;
|
|
100
|
+
const inner = message.ephemeralMessage?.message ?? message;
|
|
101
|
+
for (const key of Object.keys(inner)) {
|
|
102
|
+
const value = inner[key];
|
|
103
|
+
if (value &&
|
|
104
|
+
typeof value === 'object' &&
|
|
105
|
+
!Array.isArray(value) &&
|
|
106
|
+
!(value instanceof Uint8Array)) {
|
|
107
|
+
const stamp = value.contextInfo?.ephemeralSettingTimestamp;
|
|
108
|
+
if (stamp !== undefined && stamp !== null) {
|
|
109
|
+
return longToNumber(stamp);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
76
115
|
function pickContextInfoTarget(message) {
|
|
77
116
|
for (const key of Object.keys(message)) {
|
|
78
117
|
const value = message[key];
|
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import { proto } from '../../proto.js';
|
|
2
2
|
import { WA_EDIT_ATTRS, WA_ENC_MEDIA_TYPES, WA_EVENT_META_TYPES, WA_POLL_META_TYPES, WA_STANZA_MSG_TYPES } from '../../protocol/constants.js';
|
|
3
|
+
/**
|
|
4
|
+
* Content keys that carry a payload but do not end in `Message`, so the
|
|
5
|
+
* substring match below would miss them.
|
|
6
|
+
*/
|
|
7
|
+
const UNSUFFIXED_CONTENT_KEYS = new Set([
|
|
8
|
+
'conversation',
|
|
9
|
+
'messageHistoryBundle',
|
|
10
|
+
'messageHistoryNotice'
|
|
11
|
+
]);
|
|
3
12
|
/**
|
|
4
13
|
* Returns the content-type key of a message - `'conversation'`,
|
|
5
|
-
* `'imageMessage'`, `'extendedTextMessage'`, etc. -
|
|
6
|
-
*
|
|
7
|
-
* their real payload type rather than the
|
|
14
|
+
* `'imageMessage'`, `'extendedTextMessage'`, `'messageHistoryBundle'`, etc. -
|
|
15
|
+
* or `undefined` for an empty message. `senderKeyDistributionMessage` is
|
|
16
|
+
* skipped so group messages report their real payload type rather than the
|
|
17
|
+
* piggy-backed sender-key.
|
|
18
|
+
*
|
|
19
|
+
* A key that is present but holds `null`/`undefined` carries no payload and is
|
|
20
|
+
* skipped, so an explicitly-cleared field cannot mask the real content.
|
|
8
21
|
*/
|
|
9
22
|
export function getContentType(content) {
|
|
10
23
|
if (!content)
|
|
11
24
|
return undefined;
|
|
12
|
-
const key = Object.keys(content).find((k) =>
|
|
25
|
+
const key = Object.keys(content).find((k) => content[k] !== null &&
|
|
26
|
+
content[k] !== undefined &&
|
|
27
|
+
(UNSUFFIXED_CONTENT_KEYS.has(k) || k.includes('Message')) &&
|
|
28
|
+
k !== 'senderKeyDistributionMessage');
|
|
13
29
|
return key;
|
|
14
30
|
}
|
|
15
31
|
/**
|
|
@@ -28,7 +28,7 @@ function buildPayload(mediaType, fields) {
|
|
|
28
28
|
* lacks the `directPath` / `mediaKey` needed to fetch and decrypt the blob (a
|
|
29
29
|
* non-`Uint8Array` `mediaKey` also yields `null`). Supported kinds: image,
|
|
30
30
|
* video (gif when `gifPlayback`), audio (ptt when `ptt`), document, sticker,
|
|
31
|
-
* ptv.
|
|
31
|
+
* ptv, and the group-history bundle shared with a member after they join.
|
|
32
32
|
*
|
|
33
33
|
* @example
|
|
34
34
|
* ```ts
|
|
@@ -56,5 +56,7 @@ export function resolveMediaPayload(message) {
|
|
|
56
56
|
return buildPayload('sticker', msg.stickerMessage);
|
|
57
57
|
if (msg.ptvMessage)
|
|
58
58
|
return buildPayload('ptv', msg.ptvMessage);
|
|
59
|
+
if (msg.messageHistoryBundle)
|
|
60
|
+
return buildPayload('group-history', msg.messageHistoryBundle);
|
|
59
61
|
return null;
|
|
60
62
|
}
|