zapo-js 1.9.0 → 1.11.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/README.md +14 -13
- package/dist/auth/WaAuthClient.d.ts +5 -0
- package/dist/auth/WaAuthClient.js +5 -0
- package/dist/auth/pairing/WaPairingFlow.d.ts +11 -0
- package/dist/auth/pairing/WaPairingFlow.js +97 -40
- package/dist/client/WaClient.d.ts +4 -0
- package/dist/client/WaClient.js +7 -1
- package/dist/client/WaClientFactory.js +6 -5
- package/dist/client/connection/WaConnectionManager.d.ts +14 -2
- package/dist/client/connection/WaConnectionManager.js +55 -22
- package/dist/client/coordinators/WaMessageCoordinator.d.ts +27 -8
- package/dist/client/coordinators/WaMessageCoordinator.js +4 -8
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +13 -1
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +19 -6
- package/dist/client/coordinators/WaProfileCoordinator.d.ts +5 -1
- package/dist/client/coordinators/WaStreamControlCoordinator.d.ts +2 -0
- package/dist/client/coordinators/WaStreamControlCoordinator.js +6 -1
- package/dist/client/messaging/ignore-key.d.ts +2 -1
- package/dist/client/messaging/ignore-key.js +24 -3
- package/dist/client/messaging/messages.js +24 -8
- package/dist/client/persistence/history-sync.d.ts +5 -1
- package/dist/client/persistence/history-sync.js +172 -38
- package/dist/client/types.d.ts +65 -10
- package/dist/esm/auth/WaAuthClient.js +5 -0
- package/dist/esm/auth/pairing/WaPairingFlow.js +97 -40
- package/dist/esm/client/WaClient.js +7 -1
- package/dist/esm/client/WaClientFactory.js +6 -5
- package/dist/esm/client/connection/WaConnectionManager.js +55 -22
- package/dist/esm/client/coordinators/WaMessageCoordinator.js +4 -8
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +18 -6
- package/dist/esm/client/coordinators/WaStreamControlCoordinator.js +6 -1
- package/dist/esm/client/messaging/ignore-key.js +25 -4
- package/dist/esm/client/messaging/messages.js +25 -9
- package/dist/esm/client/persistence/history-sync.js +172 -38
- package/dist/esm/infra/log/types.js +2 -1
- package/dist/esm/message/context-info.js +2 -0
- package/dist/esm/message/encode/content.js +16 -0
- package/dist/esm/message/primitives/incoming.js +12 -5
- package/dist/esm/protocol/defaults.js +6 -0
- package/dist/esm/protocol/message.js +2 -1
- package/dist/esm/signal/api/SignalDeviceSyncApi.js +64 -32
- package/dist/esm/store/cache/identity.cache.js +30 -5
- package/dist/esm/store/cache/privacy-token.cache.js +30 -5
- package/dist/esm/store/cache/sender-key.cache.js +38 -7
- package/dist/esm/store/cache/session.cache.js +30 -4
- package/dist/esm/store/createStore.js +39 -4
- package/dist/esm/store/memory/identity.store.js +28 -4
- package/dist/esm/store/memory/privacy-token.store.js +24 -7
- package/dist/esm/store/memory/sender-key.store.js +36 -7
- package/dist/esm/store/memory/session.store.js +41 -7
- package/dist/esm/transport/WaComms.js +94 -16
- package/dist/esm/transport/WaWebSocket.js +79 -32
- package/dist/esm/transport/node/builders/message.js +1 -1
- package/dist/esm/transport/noise/WaNoiseSession.js +6 -5
- package/dist/esm/util/collections.js +94 -0
- package/dist/esm/util/index.js +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/infra/log/types.d.ts +1 -0
- package/dist/infra/log/types.js +2 -1
- package/dist/message/context-info.js +2 -0
- package/dist/message/encode/content.d.ts +3 -2
- package/dist/message/encode/content.js +17 -0
- package/dist/message/primitives/incoming.js +12 -5
- package/dist/message/primitives/media-retry.d.ts +11 -0
- package/dist/message/types.d.ts +21 -12
- package/dist/protocol/defaults.d.ts +6 -0
- package/dist/protocol/defaults.js +6 -0
- package/dist/protocol/message.d.ts +1 -0
- package/dist/protocol/message.js +2 -1
- package/dist/signal/api/SignalDeviceSyncApi.d.ts +27 -5
- package/dist/signal/api/SignalDeviceSyncApi.js +64 -32
- package/dist/store/cache/identity.cache.d.ts +6 -4
- package/dist/store/cache/identity.cache.js +30 -5
- package/dist/store/cache/privacy-token.cache.d.ts +6 -4
- package/dist/store/cache/privacy-token.cache.js +30 -5
- package/dist/store/cache/sender-key.cache.d.ts +5 -3
- package/dist/store/cache/sender-key.cache.js +38 -7
- package/dist/store/cache/session.cache.d.ts +6 -3
- package/dist/store/cache/session.cache.js +30 -4
- package/dist/store/createStore.js +39 -4
- package/dist/store/memory/identity.store.d.ts +7 -1
- package/dist/store/memory/identity.store.js +28 -4
- package/dist/store/memory/privacy-token.store.d.ts +7 -1
- package/dist/store/memory/privacy-token.store.js +24 -7
- package/dist/store/memory/sender-key.store.d.ts +12 -2
- package/dist/store/memory/sender-key.store.js +36 -7
- package/dist/store/memory/session.store.d.ts +7 -1
- package/dist/store/memory/session.store.js +41 -7
- package/dist/store/types.d.ts +43 -4
- package/dist/transport/WaComms.d.ts +9 -1
- package/dist/transport/WaComms.js +94 -16
- package/dist/transport/WaWebSocket.d.ts +8 -2
- package/dist/transport/WaWebSocket.js +79 -32
- package/dist/transport/node/builders/message.js +1 -1
- package/dist/transport/noise/WaNoiseSession.js +6 -5
- package/dist/util/collections.d.ts +46 -0
- package/dist/util/collections.js +98 -0
- package/dist/util/index.d.ts +2 -2
- package/dist/util/index.js +3 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WaMessageDispatchCoordinator = void 0;
|
|
4
4
|
exports.restrictGroupHistoryTargets = restrictGroupHistoryTargets;
|
|
5
|
+
exports.resolveOutboundStanzaShape = resolveOutboundStanzaShape;
|
|
5
6
|
const _crypto_1 = require("../../crypto");
|
|
6
7
|
const primitives_1 = require("../../crypto/core/primitives");
|
|
7
8
|
const PromiseDedup_1 = require("../../infra/perf/PromiseDedup");
|
|
@@ -73,6 +74,19 @@ function restrictGroupHistoryTargets(message, participantUserJids, senderJid, on
|
|
|
73
74
|
}
|
|
74
75
|
return targets;
|
|
75
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolves the stanza `type` and `enc.mediatype` of an outbound message. A
|
|
79
|
+
* `<biz>` companion rides a type=text stanza with no mediatype, except on a
|
|
80
|
+
* group list: there the server nacks with 479 (SMAX_INVALID) unless
|
|
81
|
+
* type=media, mediatype=list and `<biz><list>` travel together.
|
|
82
|
+
*/
|
|
83
|
+
function resolveOutboundStanzaShape(attrs, isGroup) {
|
|
84
|
+
const flattensForAddon = attrs.buttonAddonKind !== null && !(isGroup && attrs.buttonAddonKind === 'list');
|
|
85
|
+
if (flattensForAddon) {
|
|
86
|
+
return { type: 'text', mediatype: undefined };
|
|
87
|
+
}
|
|
88
|
+
return { type: attrs.typeAttr, mediatype: attrs.mediatype ?? undefined };
|
|
89
|
+
}
|
|
76
90
|
class WaMessageDispatchCoordinator {
|
|
77
91
|
constructor(options) {
|
|
78
92
|
this.icdcDedup = new PromiseDedup_1.PromiseDedup();
|
|
@@ -403,12 +417,8 @@ class WaMessageDispatchCoordinator {
|
|
|
403
417
|
const outboundAttrs = (0, content_1.resolveOutboundMessageAttrs)(messageWithIcdc);
|
|
404
418
|
const buttonAddonKind = outboundAttrs.buttonAddonKind;
|
|
405
419
|
const buttonAddonNode = buttonAddonKind ? (0, message_1.buildButtonAddonNode)(buttonAddonKind) : undefined;
|
|
406
|
-
|
|
407
|
-
// omit enc.mediatype; sending type=media + mediatype=list/button alongside the
|
|
408
|
-
// companion is rejected by the server as SMAX_INVALID (479).
|
|
409
|
-
const type = buttonAddonKind ? 'text' : outboundAttrs.typeAttr;
|
|
420
|
+
const { type, mediatype } = resolveOutboundStanzaShape(outboundAttrs, isGroup);
|
|
410
421
|
const edit = outboundAttrs.edit ?? undefined;
|
|
411
|
-
const mediatype = buttonAddonKind ? undefined : (outboundAttrs.mediatype ?? undefined);
|
|
412
422
|
const metaAttrs = outboundAttrs.metaAttrs;
|
|
413
423
|
const metaNode = metaAttrs ? (0, message_1.buildMetaNode)(metaAttrs) : undefined;
|
|
414
424
|
const customNodes = [];
|
|
@@ -473,11 +483,14 @@ class WaMessageDispatchCoordinator {
|
|
|
473
483
|
const pair = await this.deps.signalDeviceSync.resolveUserJidPair(directRecipientJid);
|
|
474
484
|
return pair.pnJid ?? undefined;
|
|
475
485
|
}
|
|
476
|
-
async syncSignalSession(jid, reasonIdentity = false) {
|
|
486
|
+
async syncSignalSession(jid, { force = false, reasonIdentity = false } = {}) {
|
|
477
487
|
const address = (0, jid_1.parseSignalAddressFromJid)(jid);
|
|
478
488
|
if (address.server === constants_1.WA_DEFAULTS.GROUP_SERVER) {
|
|
479
489
|
throw new Error('syncSignalSession supports only direct chats');
|
|
480
490
|
}
|
|
491
|
+
if (force || reasonIdentity) {
|
|
492
|
+
await this.deps.sessionStore.deleteSession(address);
|
|
493
|
+
}
|
|
481
494
|
await this.deps.sessionResolver.ensureSession(address, jid, undefined, reasonIdentity);
|
|
482
495
|
}
|
|
483
496
|
async sendReceipt(input) {
|
|
@@ -158,7 +158,11 @@ export interface WaProfileCoordinator {
|
|
|
158
158
|
* @throws when `pin` is not exactly 4 digits, before any round-trip.
|
|
159
159
|
*/
|
|
160
160
|
readonly setUsernameKey: (pin: string) => Promise<boolean>;
|
|
161
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Resolves LIDs for a list of phone numbers (handles normalization). Check
|
|
163
|
+
* `status`: `'out'` means no WhatsApp account, `'unanswered'` or `'error'` mean
|
|
164
|
+
* the server gave no verdict (see `error` for the reason).
|
|
165
|
+
*/
|
|
162
166
|
readonly getLidsByPhoneNumbers: (phoneNumbers: readonly string[]) => Promise<readonly SignalLidSyncResult[]>;
|
|
163
167
|
}
|
|
164
168
|
interface WaProfileCoordinatorOptions {
|
|
@@ -10,6 +10,8 @@ interface WaStreamControlCoordinatorOptions {
|
|
|
10
10
|
readonly disconnect: (reason: WaDisconnectReason, isLogout: boolean, code: WaConnectionCode | null) => Promise<void>;
|
|
11
11
|
readonly clearStoredCredentials: () => Promise<void>;
|
|
12
12
|
readonly connect: (reason: WaConnectionOpenReason) => Promise<void>;
|
|
13
|
+
/** Returns `true` when a pairing restart already owns the force-login `stream:error`. */
|
|
14
|
+
readonly handlePairingRestart: () => boolean;
|
|
13
15
|
}
|
|
14
16
|
export interface WaStreamControlHandler {
|
|
15
17
|
readonly handleStreamControlResult: (result: WaStreamControlNodeResult) => Promise<void>;
|
|
@@ -4,7 +4,7 @@ exports.createStreamControlHandler = createStreamControlHandler;
|
|
|
4
4
|
const constants_1 = require("../../protocol/constants");
|
|
5
5
|
const primitives_1 = require("../../util/primitives");
|
|
6
6
|
function createStreamControlHandler(options) {
|
|
7
|
-
const { logger, getComms, clearPendingQueries, clearMediaConnCache, disconnect, clearStoredCredentials, connect } = options;
|
|
7
|
+
const { logger, getComms, clearPendingQueries, clearMediaConnCache, disconnect, clearStoredCredentials, connect, handlePairingRestart } = options;
|
|
8
8
|
let lifecyclePromise = null;
|
|
9
9
|
const runStreamControlLifecycle = (reason, action) => {
|
|
10
10
|
if (lifecyclePromise) {
|
|
@@ -88,6 +88,11 @@ function createStreamControlHandler(options) {
|
|
|
88
88
|
logger.info('received xmlstreamend stanza');
|
|
89
89
|
return;
|
|
90
90
|
case 'stream_error_code':
|
|
91
|
+
if (result.code === constants_1.WA_STREAM_SIGNALING.FORCE_LOGIN_CODE &&
|
|
92
|
+
handlePairingRestart()) {
|
|
93
|
+
logger.info('stream:error force login claimed by the pairing restart');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
91
96
|
logger.warn('received stream:error with code', { code: result.code });
|
|
92
97
|
if (result.code >= 500 && result.code < 600) {
|
|
93
98
|
if (result.code === constants_1.WA_STREAM_SIGNALING.FORCE_LOGIN_CODE) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WaAuthCredentials } from '../../auth/types';
|
|
1
2
|
import type { WaIgnoreKey, WaIgnoreKeyContext, WaIgnoreKeyPredicate, WaIncomingStanzaFilter } from '../types';
|
|
2
3
|
import type { BinaryNode } from '../../transport/types';
|
|
3
4
|
export declare function validateIgnoreKey(d: WaIgnoreKey): void;
|
|
@@ -8,4 +9,4 @@ export declare function validateIgnoreKey(d: WaIgnoreKey): void;
|
|
|
8
9
|
export declare function extractIgnoreKeyContext(node: BinaryNode, meJid: string | null | undefined): WaIgnoreKeyContext | null;
|
|
9
10
|
/** Pure matcher. Exported for direct testing without a coordinator. */
|
|
10
11
|
export declare function matchesIgnoreKey(node: BinaryNode, d: WaIgnoreKey, meJid: string | null | undefined): boolean;
|
|
11
|
-
export declare function createIgnoreKeyFilter(input: WaIgnoreKey | WaIgnoreKeyPredicate,
|
|
12
|
+
export declare function createIgnoreKeyFilter(input: WaIgnoreKey | WaIgnoreKeyPredicate, getMe: () => Pick<WaAuthCredentials, 'meJid' | 'meLid'> | null | undefined): WaIncomingStanzaFilter;
|
|
@@ -123,12 +123,33 @@ function matchesIgnoreKey(node, d, meJid) {
|
|
|
123
123
|
return false;
|
|
124
124
|
return true;
|
|
125
125
|
}
|
|
126
|
-
|
|
126
|
+
/**
|
|
127
|
+
* A `category='peer'` message from our own account is traffic between our own
|
|
128
|
+
* devices (app-state key share, history sync, PDO responses), not a chat
|
|
129
|
+
* message, so it has no key to ignore by. Dropping it starves the protocol:
|
|
130
|
+
* app-state collections stay blocked on a key whose share never gets through.
|
|
131
|
+
* The own-account check keeps a foreign sender from dodging a filter by
|
|
132
|
+
* stamping the attribute.
|
|
133
|
+
*/
|
|
134
|
+
function isOwnPeerMessage(node, me) {
|
|
135
|
+
if (node.tag !== message_1.WA_MESSAGE_TAGS.MESSAGE || node.attrs.category !== 'peer' || !me) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const from = node.attrs.from;
|
|
139
|
+
return !!from && tryParseJid(from) !== null && (0, jid_1.isOwnAccountJid)(from, me.meJid, me.meLid);
|
|
140
|
+
}
|
|
141
|
+
function createIgnoreKeyFilter(input, getMe) {
|
|
127
142
|
if (typeof input === 'function') {
|
|
128
143
|
return (node) => {
|
|
129
|
-
const
|
|
144
|
+
const me = getMe();
|
|
145
|
+
if (isOwnPeerMessage(node, me))
|
|
146
|
+
return false;
|
|
147
|
+
const ctx = extractIgnoreKeyContext(node, me?.meJid);
|
|
130
148
|
return ctx !== null && input(ctx);
|
|
131
149
|
};
|
|
132
150
|
}
|
|
133
|
-
return (node) =>
|
|
151
|
+
return (node) => {
|
|
152
|
+
const me = getMe();
|
|
153
|
+
return !isOwnPeerMessage(node, me) && matchesIgnoreKey(node, input, me?.meJid);
|
|
154
|
+
};
|
|
134
155
|
}
|
|
@@ -228,6 +228,27 @@ async function buildMediaMessageContent(options, content, ctx) {
|
|
|
228
228
|
if (typeof content === 'string') {
|
|
229
229
|
return { message: { conversation: content } };
|
|
230
230
|
}
|
|
231
|
+
if ((0, content_1.isSendTypedMessage)(content)) {
|
|
232
|
+
return withMessageContextInfo(await buildTypedMessageContent(options, content, ctx), content.messageContextInfo);
|
|
233
|
+
}
|
|
234
|
+
if (!content || typeof content !== 'object') {
|
|
235
|
+
throw new Error('invalid message content');
|
|
236
|
+
}
|
|
237
|
+
return { message: content };
|
|
238
|
+
}
|
|
239
|
+
/** Adds the caller's `messageContextInfo` under the built one: fields the builder set win. */
|
|
240
|
+
function withMessageContextInfo(built, messageContextInfo) {
|
|
241
|
+
if (!messageContextInfo)
|
|
242
|
+
return built;
|
|
243
|
+
return {
|
|
244
|
+
...built,
|
|
245
|
+
message: {
|
|
246
|
+
...built.message,
|
|
247
|
+
messageContextInfo: { ...messageContextInfo, ...built.message.messageContextInfo }
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function buildTypedMessageContent(options, content, ctx) {
|
|
231
252
|
if ((0, content_1.isSendTextMessage)(content)) {
|
|
232
253
|
if (options.linkPreviewResolver) {
|
|
233
254
|
try {
|
|
@@ -267,13 +288,7 @@ async function buildMediaMessageContent(options, content, ctx) {
|
|
|
267
288
|
if ((0, content_1.isSendEventResponseMessage)(content)) {
|
|
268
289
|
return { message: await buildEventResponseMessage(content, options.serverClock, ctx) };
|
|
269
290
|
}
|
|
270
|
-
|
|
271
|
-
return buildMediaMessage(options, content);
|
|
272
|
-
}
|
|
273
|
-
if (!content || typeof content !== 'object') {
|
|
274
|
-
throw new Error('invalid message content');
|
|
275
|
-
}
|
|
276
|
-
return { message: content };
|
|
291
|
+
return buildMediaMessage(options, content);
|
|
277
292
|
}
|
|
278
293
|
async function getMediaConn(options, forceRefresh = false) {
|
|
279
294
|
const cached = options.getMediaConnCache();
|
|
@@ -394,7 +409,8 @@ async function buildMediaMessage(options, content) {
|
|
|
394
409
|
if (key !== 'type' &&
|
|
395
410
|
key !== 'media' &&
|
|
396
411
|
key !== 'fileLength' &&
|
|
397
|
-
key !== 'mimetype'
|
|
412
|
+
key !== 'mimetype' &&
|
|
413
|
+
key !== 'messageContextInfo') {
|
|
398
414
|
result[key] = c[key];
|
|
399
415
|
}
|
|
400
416
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WriteBehindPersistence } from '../persistence/WriteBehindPersistence';
|
|
2
|
-
import type { WaClientEventMap } from '../types';
|
|
2
|
+
import type { WaClientEventMap, WaHistorySyncSink } 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';
|
|
@@ -27,6 +27,9 @@ interface WaHistorySyncDeps {
|
|
|
27
27
|
readonly logger: Logger;
|
|
28
28
|
readonly mediaTransfer: WaMediaTransferClient;
|
|
29
29
|
readonly writeBehind: WriteBehindPersistence;
|
|
30
|
+
/** Id of the protocol message that carried the notification; empty when the caller has none. */
|
|
31
|
+
readonly chunkId?: string;
|
|
32
|
+
readonly sink?: WaHistorySyncSink;
|
|
30
33
|
readonly chatMetadataStore?: WaChatMetadataStore;
|
|
31
34
|
readonly emitEvent: <K extends keyof WaClientEventMap>(event: K, ...args: Parameters<WaClientEventMap[K]>) => void;
|
|
32
35
|
readonly onPrivacyTokens?: (conversations: readonly WaHistorySyncPrivacyTokenConversation[]) => Promise<void>;
|
|
@@ -41,6 +44,7 @@ export declare function runHistorySyncNotification(deps: WaHistorySyncDeps, noti
|
|
|
41
44
|
* Applies one history sync chunk while it downloads, descending into
|
|
42
45
|
* `conversations` so peak memory tracks the largest single message and not the
|
|
43
46
|
* chunk. Chunk-wide records settle after the stream, once every LID mapping is in.
|
|
47
|
+
* `INITIAL_STATUS_V3` is processed only for a sink and never emits `history_sync_chunk`.
|
|
44
48
|
*/
|
|
45
49
|
export declare function processHistorySyncNotification(deps: WaHistorySyncDeps, notification: Proto.Message.IHistorySyncNotification): Promise<void>;
|
|
46
50
|
export {};
|
|
@@ -40,6 +40,26 @@ exports.HISTORY_SYNC_FIELDS = Object.freeze({
|
|
|
40
40
|
NCT_SALT: 19,
|
|
41
41
|
INLINE_CONTACTS: 20
|
|
42
42
|
});
|
|
43
|
+
const HISTORY_SYNC_SINK_KEYS = new Map([
|
|
44
|
+
[3, 'statusV3Messages'],
|
|
45
|
+
[5, 'chunkOrder'],
|
|
46
|
+
[6, 'progress'],
|
|
47
|
+
[7, 'pushnames'],
|
|
48
|
+
[8, 'globalSettings'],
|
|
49
|
+
[9, 'threadIdUserSecret'],
|
|
50
|
+
[10, 'threadDsTimeframeOffset'],
|
|
51
|
+
[11, 'recentStickers'],
|
|
52
|
+
[12, 'pastParticipants'],
|
|
53
|
+
[13, 'callLogRecords'],
|
|
54
|
+
[14, 'aiWaitListState'],
|
|
55
|
+
[15, 'phoneNumberToLidMappings'],
|
|
56
|
+
[16, 'companionMetaNonce'],
|
|
57
|
+
[17, 'shareableChatIdentifierEncryptionKey'],
|
|
58
|
+
[18, 'accounts'],
|
|
59
|
+
[19, 'nctSalt'],
|
|
60
|
+
[20, 'inlineContacts'],
|
|
61
|
+
[21, 'inlineContactsProvided']
|
|
62
|
+
]);
|
|
43
63
|
exports.CONVERSATION_FIELDS = Object.freeze({
|
|
44
64
|
ID: 1,
|
|
45
65
|
MESSAGES: 2
|
|
@@ -60,6 +80,7 @@ async function runHistorySyncNotification(deps, notification) {
|
|
|
60
80
|
* Applies one history sync chunk while it downloads, descending into
|
|
61
81
|
* `conversations` so peak memory tracks the largest single message and not the
|
|
62
82
|
* chunk. Chunk-wide records settle after the stream, once every LID mapping is in.
|
|
83
|
+
* `INITIAL_STATUS_V3` is processed only for a sink and never emits `history_sync_chunk`.
|
|
63
84
|
*/
|
|
64
85
|
async function processHistorySyncNotification(deps, notification) {
|
|
65
86
|
const syncType = notification.syncType;
|
|
@@ -67,15 +88,17 @@ async function processHistorySyncNotification(deps, notification) {
|
|
|
67
88
|
deps.logger.debug('skipping history sync notification without syncType');
|
|
68
89
|
return;
|
|
69
90
|
}
|
|
70
|
-
|
|
91
|
+
const isStatusV3 = syncType === _proto_1.proto.Message.HistorySyncType.INITIAL_STATUS_V3;
|
|
92
|
+
if (!HANDLED_SYNC_TYPES.has(syncType) && !(isStatusV3 && deps.sink)) {
|
|
71
93
|
deps.logger.debug('skipping unhandled history sync type', { syncType });
|
|
72
|
-
if (
|
|
94
|
+
if (isStatusV3 && deps.onProcessed) {
|
|
73
95
|
await deps.onProcessed(syncType);
|
|
74
96
|
}
|
|
75
97
|
return;
|
|
76
98
|
}
|
|
77
99
|
const blob = await (0, history_blob_1.openHistoryBlobStream)(deps.mediaTransfer, notification, 'history', 'history sync', notification.initialHistBootstrapInlinePayload);
|
|
78
100
|
const state = {
|
|
101
|
+
chunk: { id: deps.chunkId ?? '', syncType },
|
|
79
102
|
nowMs: Date.now(),
|
|
80
103
|
pendingWrites: [],
|
|
81
104
|
pushnames: [],
|
|
@@ -88,6 +111,23 @@ async function processHistorySyncNotification(deps, notification) {
|
|
|
88
111
|
conversationsCount: 0,
|
|
89
112
|
messagesCount: 0
|
|
90
113
|
};
|
|
114
|
+
let event;
|
|
115
|
+
try {
|
|
116
|
+
event = await applyHistorySyncChunk(deps, blob, state);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const failure = (0, primitives_1.toError)(error);
|
|
120
|
+
await abortHistorySink(deps, state.chunk, failure);
|
|
121
|
+
throw failure;
|
|
122
|
+
}
|
|
123
|
+
if (!isStatusV3) {
|
|
124
|
+
deps.emitEvent('history_sync_chunk', event);
|
|
125
|
+
}
|
|
126
|
+
if (deps.onProcessed) {
|
|
127
|
+
await deps.onProcessed(syncType);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function applyHistorySyncChunk(deps, blob, state) {
|
|
91
131
|
try {
|
|
92
132
|
await consumeHistorySyncStream(deps, blob.inflated, state);
|
|
93
133
|
}
|
|
@@ -100,7 +140,7 @@ async function processHistorySyncNotification(deps, notification) {
|
|
|
100
140
|
await blob.verified;
|
|
101
141
|
await settleHistorySyncChunk(deps, state);
|
|
102
142
|
deps.logger.info('decoded history sync chunk', {
|
|
103
|
-
syncType,
|
|
143
|
+
syncType: state.chunk.syncType,
|
|
104
144
|
chunkOrder: state.chunkOrder,
|
|
105
145
|
progress: state.progress,
|
|
106
146
|
conversations: state.conversationsCount,
|
|
@@ -109,7 +149,8 @@ async function processHistorySyncNotification(deps, notification) {
|
|
|
109
149
|
inlineContacts: state.inlineContacts.length
|
|
110
150
|
});
|
|
111
151
|
const event = {
|
|
112
|
-
|
|
152
|
+
id: state.chunk.id,
|
|
153
|
+
syncType: state.chunk.syncType,
|
|
113
154
|
messagesCount: state.messagesCount,
|
|
114
155
|
conversationsCount: state.conversationsCount,
|
|
115
156
|
pushnamesCount: state.pushnames.length,
|
|
@@ -118,15 +159,53 @@ async function processHistorySyncNotification(deps, notification) {
|
|
|
118
159
|
progress: state.progress ?? undefined
|
|
119
160
|
};
|
|
120
161
|
await (0, history_blob_1.flushPendingWrites)(state.pendingWrites);
|
|
121
|
-
deps.
|
|
122
|
-
|
|
123
|
-
await deps.onProcessed(syncType);
|
|
162
|
+
if (deps.sink?.commit) {
|
|
163
|
+
await deps.sink.commit(event);
|
|
124
164
|
}
|
|
165
|
+
return event;
|
|
166
|
+
}
|
|
167
|
+
/** A throwing `abort` is logged so it never hides the failure that triggered it. */
|
|
168
|
+
async function abortHistorySink(deps, chunk, error) {
|
|
169
|
+
if (!deps.sink?.abort) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
await deps.sink.abort(chunk, error);
|
|
174
|
+
}
|
|
175
|
+
catch (abortError) {
|
|
176
|
+
deps.logger.warn('history sync sink abort failed', {
|
|
177
|
+
id: chunk.id,
|
|
178
|
+
message: (0, primitives_1.toError)(abortError).message
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Only a thenable result is surfaced, so a synchronous sink never adds an await. */
|
|
183
|
+
function deliverHistoryItem(sink, chunk, item) {
|
|
184
|
+
const result = sink.item(item, chunk);
|
|
185
|
+
return typeof result?.then === 'function'
|
|
186
|
+
? Promise.resolve(result)
|
|
187
|
+
: undefined;
|
|
188
|
+
}
|
|
189
|
+
function deliverHistoryValue(sink, chunk, key, value) {
|
|
190
|
+
return deliverHistoryItem(sink, chunk, { kind: key, value });
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Delivers a `HistorySync` field the stores do not consume. The field is decoded
|
|
194
|
+
* from a copy because the reader buffer is reused and `bytes` values are views of
|
|
195
|
+
* their input; one occurrence of a repeated field yields a single element.
|
|
196
|
+
*/
|
|
197
|
+
function deliverHistoryField(sink, chunk, key, event) {
|
|
198
|
+
const decoded = _proto_1.proto.HistorySync.decode(event.raw.slice())[key];
|
|
199
|
+
const value = Array.isArray(decoded) ? decoded[0] : decoded;
|
|
200
|
+
return value === null || value === undefined
|
|
201
|
+
? undefined
|
|
202
|
+
: deliverHistoryValue(sink, chunk, key, value);
|
|
125
203
|
}
|
|
126
204
|
async function consumeHistorySyncStream(deps, inflated, state) {
|
|
127
205
|
let headerParts = null;
|
|
128
206
|
let threadJid = null;
|
|
129
207
|
let parked = null;
|
|
208
|
+
const sink = deps.sink;
|
|
130
209
|
const onEvent = (event) => {
|
|
131
210
|
if (event.kind === proto_stream_1.PROTO_STREAM_EVENT_KINDS.ENTER) {
|
|
132
211
|
headerParts = [];
|
|
@@ -147,20 +226,37 @@ async function consumeHistorySyncStream(deps, inflated, state) {
|
|
|
147
226
|
if (event.depth === 1) {
|
|
148
227
|
if (event.fieldNumber === exports.CONVERSATION_FIELDS.MESSAGES &&
|
|
149
228
|
event.wireType === protoscan_1.PROTO_WIRE_TYPES.LEN) {
|
|
150
|
-
const
|
|
151
|
-
|
|
229
|
+
const record = _proto_1.proto.HistorySyncMsg.decode(sink ? event.value.slice() : event.value);
|
|
230
|
+
const webMsg = record.message;
|
|
231
|
+
if (!webMsg && !sink) {
|
|
152
232
|
return undefined;
|
|
153
233
|
}
|
|
234
|
+
const stored = webMsg ? toStoredHistoryMessage(webMsg) : null;
|
|
154
235
|
if (!threadJid) {
|
|
236
|
+
if (!stored && !sink) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
155
239
|
parked ?? (parked = []);
|
|
156
240
|
if (parked.length >= HISTORY_SYNC_MAX_PARKED_MESSAGES) {
|
|
157
241
|
throw new Error(`history sync parked ${parked.length} messages with no thread jid, exceeding the ${HISTORY_SYNC_MAX_PARKED_MESSAGES} limit`);
|
|
158
242
|
}
|
|
159
|
-
parked[parked.length] =
|
|
243
|
+
parked[parked.length] = { stored, record: sink ? record : null };
|
|
160
244
|
return undefined;
|
|
161
245
|
}
|
|
162
|
-
|
|
163
|
-
|
|
246
|
+
if (stored) {
|
|
247
|
+
persistHistoryMessage(deps, state, stored, threadJid);
|
|
248
|
+
}
|
|
249
|
+
const delivery = sink
|
|
250
|
+
? deliverHistoryItem(sink, state.chunk, {
|
|
251
|
+
kind: 'conversations.messages',
|
|
252
|
+
conversationId: threadJid,
|
|
253
|
+
value: record
|
|
254
|
+
})
|
|
255
|
+
: undefined;
|
|
256
|
+
const flush = maybeFlush(state);
|
|
257
|
+
return delivery && flush
|
|
258
|
+
? Promise.all([delivery, flush]).then(() => undefined)
|
|
259
|
+
: (delivery ?? flush);
|
|
164
260
|
}
|
|
165
261
|
if (headerParts) {
|
|
166
262
|
if (event.fieldNumber === exports.CONVERSATION_FIELDS.ID &&
|
|
@@ -177,35 +273,52 @@ async function consumeHistorySyncStream(deps, inflated, state) {
|
|
|
177
273
|
if (event.wireType === protoscan_1.PROTO_WIRE_TYPES.VARINT) {
|
|
178
274
|
if (event.fieldNumber === exports.HISTORY_SYNC_FIELDS.CHUNK_ORDER) {
|
|
179
275
|
state.chunkOrder = event.varintValue;
|
|
276
|
+
return sink
|
|
277
|
+
? deliverHistoryValue(sink, state.chunk, 'chunkOrder', event.varintValue)
|
|
278
|
+
: undefined;
|
|
180
279
|
}
|
|
181
280
|
else if (event.fieldNumber === exports.HISTORY_SYNC_FIELDS.PROGRESS) {
|
|
182
281
|
state.progress = event.varintValue;
|
|
282
|
+
return sink
|
|
283
|
+
? deliverHistoryValue(sink, state.chunk, 'progress', event.varintValue)
|
|
284
|
+
: undefined;
|
|
183
285
|
}
|
|
184
|
-
return undefined;
|
|
185
286
|
}
|
|
186
|
-
if (event.wireType
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const map = _proto_1.proto.PhoneNumberToLIDMapping.decode(event.value);
|
|
194
|
-
if (map.pnJid && map.lidJid) {
|
|
195
|
-
state.pnToLid.set(map.pnJid, map.lidJid);
|
|
287
|
+
else if (event.wireType === protoscan_1.PROTO_WIRE_TYPES.LEN) {
|
|
288
|
+
if (event.fieldNumber === exports.HISTORY_SYNC_FIELDS.PUSHNAMES) {
|
|
289
|
+
const pushname = _proto_1.proto.Pushname.decode(sink ? event.value.slice() : event.value);
|
|
290
|
+
state.pushnames[state.pushnames.length] = pushname;
|
|
291
|
+
return sink
|
|
292
|
+
? deliverHistoryValue(sink, state.chunk, 'pushnames', pushname)
|
|
293
|
+
: undefined;
|
|
196
294
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
295
|
+
else if (event.fieldNumber === exports.HISTORY_SYNC_FIELDS.PHONE_NUMBER_TO_LID_MAPPINGS) {
|
|
296
|
+
const mapping = _proto_1.proto.PhoneNumberToLIDMapping.decode(sink ? event.value.slice() : event.value);
|
|
297
|
+
if (mapping.pnJid && mapping.lidJid) {
|
|
298
|
+
state.pnToLid.set(mapping.pnJid, mapping.lidJid);
|
|
299
|
+
}
|
|
300
|
+
return sink
|
|
301
|
+
? deliverHistoryValue(sink, state.chunk, 'phoneNumberToLidMappings', mapping)
|
|
302
|
+
: undefined;
|
|
303
|
+
}
|
|
304
|
+
else if (event.fieldNumber === exports.HISTORY_SYNC_FIELDS.INLINE_CONTACTS) {
|
|
305
|
+
const contact = _proto_1.proto.InlineContact.decode(sink ? event.value.slice() : event.value);
|
|
306
|
+
state.inlineContacts[state.inlineContacts.length] = contact;
|
|
307
|
+
if (contact.pnJid && contact.lidJid) {
|
|
308
|
+
state.pnToLid.set(contact.pnJid, contact.lidJid);
|
|
309
|
+
}
|
|
310
|
+
return sink
|
|
311
|
+
? deliverHistoryValue(sink, state.chunk, 'inlineContacts', contact)
|
|
312
|
+
: undefined;
|
|
313
|
+
}
|
|
314
|
+
else if (event.fieldNumber === exports.HISTORY_SYNC_FIELDS.NCT_SALT) {
|
|
315
|
+
const salt = event.value.slice();
|
|
316
|
+
state.nctSalt = salt;
|
|
317
|
+
return sink ? deliverHistoryValue(sink, state.chunk, 'nctSalt', salt) : undefined;
|
|
203
318
|
}
|
|
204
319
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
return undefined;
|
|
320
|
+
const key = sink ? HISTORY_SYNC_SINK_KEYS.get(event.fieldNumber) : undefined;
|
|
321
|
+
return sink && key ? deliverHistoryField(sink, state.chunk, key, event) : undefined;
|
|
209
322
|
};
|
|
210
323
|
await (0, proto_stream_1.streamProtoFields)(inflated, onEvent, {
|
|
211
324
|
shouldDescend: (fieldNumber, depth) => depth === 0 && fieldNumber === exports.HISTORY_SYNC_FIELDS.CONVERSATIONS,
|
|
@@ -284,8 +397,30 @@ async function closeConversation(deps, state, headerParts, threadJid, parked) {
|
|
|
284
397
|
}));
|
|
285
398
|
}
|
|
286
399
|
}
|
|
287
|
-
|
|
288
|
-
|
|
400
|
+
const sink = deps.sink;
|
|
401
|
+
for (const entry of parked ?? []) {
|
|
402
|
+
if (entry.stored) {
|
|
403
|
+
persistHistoryMessage(deps, state, entry.stored, resolvedJid);
|
|
404
|
+
}
|
|
405
|
+
if (sink && entry.record) {
|
|
406
|
+
const delivery = deliverHistoryItem(sink, state.chunk, {
|
|
407
|
+
kind: 'conversations.messages',
|
|
408
|
+
conversationId: resolvedJid,
|
|
409
|
+
value: entry.record
|
|
410
|
+
});
|
|
411
|
+
if (delivery) {
|
|
412
|
+
await delivery;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (sink) {
|
|
417
|
+
const delivery = deliverHistoryItem(sink, state.chunk, {
|
|
418
|
+
kind: 'conversations',
|
|
419
|
+
value: conversation
|
|
420
|
+
});
|
|
421
|
+
if (delivery) {
|
|
422
|
+
await delivery;
|
|
423
|
+
}
|
|
289
424
|
}
|
|
290
425
|
await maybeFlush(state);
|
|
291
426
|
}
|
|
@@ -367,9 +502,8 @@ async function settleHistorySyncChunk(deps, state) {
|
|
|
367
502
|
}
|
|
368
503
|
}
|
|
369
504
|
/** `null` for content-less stubs, which duplicate live notification stanzas. */
|
|
370
|
-
function
|
|
371
|
-
|
|
372
|
-
if (!webMsg?.key?.id || !webMsg.message) {
|
|
505
|
+
function toStoredHistoryMessage(webMsg) {
|
|
506
|
+
if (!webMsg.key?.id || !webMsg.message) {
|
|
373
507
|
return null;
|
|
374
508
|
}
|
|
375
509
|
const timestampMs = (0, primitives_1.longToNumber)(webMsg.messageTimestamp) * 1000;
|
package/dist/client/types.d.ts
CHANGED
|
@@ -148,11 +148,12 @@ export interface WaClientOptions extends WaAuthClientOptions, WaAuthSocketOption
|
|
|
148
148
|
* History-sync behavior. By default chunks pushed via
|
|
149
149
|
* `historySyncNotification` (both the initial bootstrap and the
|
|
150
150
|
* on-demand backfill triggered by `message.requestHistorySync`) are
|
|
151
|
-
* downloaded and emitted as `history_sync_chunk`
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* `
|
|
155
|
-
*
|
|
151
|
+
* downloaded, written to the stores and emitted as `history_sync_chunk`
|
|
152
|
+
* events. `sink` streams the decoded content to your own callback, with
|
|
153
|
+
* or without stores. Set `enabled: false` to drop the chunks;
|
|
154
|
+
* `requireFullSync: true` asks the primary device for a full history
|
|
155
|
+
* download instead of just recent; `groupBundles: true` opts into the
|
|
156
|
+
* group-history bundle a member may share after somebody joins a group.
|
|
156
157
|
*/
|
|
157
158
|
readonly history?: WaHistorySyncOptions;
|
|
158
159
|
/**
|
|
@@ -313,11 +314,22 @@ export interface WaHistorySyncOptions {
|
|
|
313
314
|
* Whether to process the `historySyncNotification` protocol messages
|
|
314
315
|
* that WhatsApp pushes on first connect (initial bootstrap) and via
|
|
315
316
|
* `message.requestHistorySync` (on-demand). On by default - set to
|
|
316
|
-
* `false` to drop the chunks silently (
|
|
317
|
-
*
|
|
318
|
-
* burn bandwidth).
|
|
317
|
+
* `false` to drop the chunks silently (and skip `sink`): the primary is
|
|
318
|
+
* still acked, but nothing is downloaded or delivered.
|
|
319
319
|
*/
|
|
320
320
|
readonly enabled?: boolean;
|
|
321
|
+
/**
|
|
322
|
+
* Receives every field of the decoded history as it streams, awaited per item,
|
|
323
|
+
* even with every store off; it alone receives the `INITIAL_STATUS_V3` chunk,
|
|
324
|
+
* which is otherwise only acked and never emits `history_sync_chunk`. A slow
|
|
325
|
+
* sink pauses the download: the media timeout ends at the headers.
|
|
326
|
+
*/
|
|
327
|
+
readonly sink?: WaHistorySyncSink;
|
|
328
|
+
/**
|
|
329
|
+
* When `true`, request a full history download from the primary device on
|
|
330
|
+
* pairing instead of just recent messages. Takes precedence over the
|
|
331
|
+
* top-level `requireFullSync`; has no effect on an already-paired session.
|
|
332
|
+
*/
|
|
321
333
|
readonly requireFullSync?: boolean;
|
|
322
334
|
/**
|
|
323
335
|
* Whether to download and process the group-history bundle a member may
|
|
@@ -1101,9 +1113,51 @@ export interface WaGroupEvent extends WaIncomingBaseEvent {
|
|
|
1101
1113
|
/** Decoded payload bag for actions without dedicated fields above. */
|
|
1102
1114
|
readonly details?: Readonly<Record<string, unknown>>;
|
|
1103
1115
|
}
|
|
1104
|
-
|
|
1116
|
+
/** Identifies one history-sync chunk; chunks can be processed concurrently. */
|
|
1117
|
+
export interface WaHistorySyncChunkRef {
|
|
1118
|
+
/** Id of the protocol message that carried the notification. */
|
|
1119
|
+
readonly id: string;
|
|
1105
1120
|
/** Numeric value of `proto.Message.HistorySyncType` (`RECENT`, `ON_DEMAND`, …). */
|
|
1106
1121
|
readonly syncType: number;
|
|
1122
|
+
}
|
|
1123
|
+
type WaHistorySyncElement<T> = T extends readonly (infer E)[] ? E : T;
|
|
1124
|
+
type WaHistorySyncKey = Exclude<keyof Proto.IHistorySync, 'syncType' | 'conversations' | '$unknowns'>;
|
|
1125
|
+
/**
|
|
1126
|
+
* One decoded record of a history-sync chunk. `kind` is the `HistorySync` field it
|
|
1127
|
+
* came from, one element per item for repeated fields; `syncType` rides on the chunk ref.
|
|
1128
|
+
* Conversation messages precede their metadata on the wire, so they stream out first as
|
|
1129
|
+
* `conversations.messages` and the conversation arrives at its close, without `messages`.
|
|
1130
|
+
*/
|
|
1131
|
+
export type WaHistorySyncItem = {
|
|
1132
|
+
[K in WaHistorySyncKey]-?: {
|
|
1133
|
+
readonly kind: K;
|
|
1134
|
+
readonly value: NonNullable<WaHistorySyncElement<NonNullable<Proto.IHistorySync[K]>>>;
|
|
1135
|
+
};
|
|
1136
|
+
}[WaHistorySyncKey] | {
|
|
1137
|
+
readonly kind: 'conversations';
|
|
1138
|
+
readonly value: Omit<Proto.IConversation, 'messages'>;
|
|
1139
|
+
} | {
|
|
1140
|
+
readonly kind: 'conversations.messages';
|
|
1141
|
+
readonly conversationId: string;
|
|
1142
|
+
/** Includes stubs and records without content, which the stores skip. */
|
|
1143
|
+
readonly value: Proto.IHistorySyncMsg;
|
|
1144
|
+
};
|
|
1145
|
+
export interface WaHistorySyncSink {
|
|
1146
|
+
/**
|
|
1147
|
+
* Called per record in wire order. A returned promise pauses the stream until
|
|
1148
|
+
* it settles; a throw or rejection aborts the chunk. Items arrive before the
|
|
1149
|
+
* chunk MAC is verified.
|
|
1150
|
+
*/
|
|
1151
|
+
item(item: WaHistorySyncItem, chunk: WaHistorySyncChunkRef): void | Promise<void>;
|
|
1152
|
+
/** Called once the chunk verified and applied; the receipt goes out after it resolves. */
|
|
1153
|
+
commit?(chunk: WaHistorySyncChunkEvent): void | Promise<void>;
|
|
1154
|
+
/**
|
|
1155
|
+
* Called when the chunk fails after items may have been delivered. The primary
|
|
1156
|
+
* resends it, so items repeat: dedupe by `key.id`.
|
|
1157
|
+
*/
|
|
1158
|
+
abort?(chunk: WaHistorySyncChunkRef, error: Error): void | Promise<void>;
|
|
1159
|
+
}
|
|
1160
|
+
export interface WaHistorySyncChunkEvent extends WaHistorySyncChunkRef {
|
|
1107
1161
|
readonly messagesCount: number;
|
|
1108
1162
|
readonly conversationsCount: number;
|
|
1109
1163
|
readonly pushnamesCount: number;
|
|
@@ -1209,7 +1263,8 @@ export interface WaClientEventMap {
|
|
|
1209
1263
|
}) => void;
|
|
1210
1264
|
/**
|
|
1211
1265
|
* The server is ready to receive pairing input. `forceManual: true` means
|
|
1212
|
-
* the QR refresh budget was exhausted
|
|
1266
|
+
* the QR refresh budget was exhausted, or a pending pairing code could not
|
|
1267
|
+
* be registered again after a reconnect, and the user must request a fresh
|
|
1213
1268
|
* one (e.g. via the link-code flow).
|
|
1214
1269
|
*/
|
|
1215
1270
|
readonly auth_pairing_required: (event: {
|