zapo-js 1.5.0 → 1.6.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/client/WaClient.d.ts +1 -1
- package/dist/client/WaClient.js +1 -1
- package/dist/client/WaClientFactory.js +1 -0
- package/dist/client/coordinators/WaAppStateMutationCoordinator.js +2 -2
- package/dist/client/coordinators/WaMessageCoordinator.d.ts +69 -1
- package/dist/client/coordinators/WaMessageCoordinator.js +76 -1
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +1 -1
- package/dist/client/media.d.ts +1 -0
- package/dist/client/media.js +1 -0
- package/dist/client/messaging/messages.d.ts +45 -1
- package/dist/client/messaging/messages.js +57 -45
- package/dist/esm/client/WaClient.js +1 -1
- package/dist/esm/client/WaClientFactory.js +1 -0
- package/dist/esm/client/coordinators/WaAppStateMutationCoordinator.js +2 -2
- package/dist/esm/client/coordinators/WaMessageCoordinator.js +78 -3
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +1 -1
- package/dist/esm/client/media.js +1 -1
- package/dist/esm/client/messaging/messages.js +56 -45
- package/dist/esm/index.js +2 -0
- package/dist/esm/signal/api/SignalDeviceSyncApi.js +133 -63
- package/dist/index.d.ts +4 -1
- package/dist/index.js +6 -2
- package/dist/signal/api/SignalDeviceSyncApi.d.ts +29 -2
- package/dist/signal/api/SignalDeviceSyncApi.js +131 -61
- package/package.json +1 -1
- package/spec/appstate/index.d.ts +5 -4
- package/spec/appstate/index.js +21 -8
- package/spec/mex/index.d.ts +335 -73
- package/spec/mex/index.js +29 -21
- package/spec/proto/index.d.ts +833 -69
- package/spec/proto/index.js +1 -1
- package/spec/version/index.d.ts +1 -1
- package/spec/version/index.js +1 -1
- package/spec/version/version.json +1 -1
|
@@ -131,7 +131,7 @@ declare class WaClientImpl extends EventEmitter {
|
|
|
131
131
|
ignoreKey(input: WaIgnoreKey | WaIgnoreKeyPredicate): () => void;
|
|
132
132
|
/** Auth client: pairing, credentials, registration state. */
|
|
133
133
|
get auth(): WaAuthClient;
|
|
134
|
-
/** Message coordinator: send/receive, receipts, addons, media download. */
|
|
134
|
+
/** Message coordinator: send/receive, receipts, addons, media upload/download. */
|
|
135
135
|
get message(): WaMessageCoordinator;
|
|
136
136
|
/** Presence coordinator: own/peer presence subscriptions. */
|
|
137
137
|
get presence(): WaPresenceCoordinator;
|
package/dist/client/WaClient.js
CHANGED
|
@@ -414,7 +414,7 @@ class WaClientImpl extends node_events_1.EventEmitter {
|
|
|
414
414
|
get auth() {
|
|
415
415
|
return this.deps.authClient;
|
|
416
416
|
}
|
|
417
|
-
/** Message coordinator: send/receive, receipts, addons, media download. */
|
|
417
|
+
/** Message coordinator: send/receive, receipts, addons, media upload/download. */
|
|
418
418
|
get message() {
|
|
419
419
|
return this.deps.messageCoordinator;
|
|
420
420
|
}
|
|
@@ -504,6 +504,7 @@ function buildWaClientDependencies(input) {
|
|
|
504
504
|
const messageCoordinator = new WaMessageCoordinator_1.WaMessageCoordinator({
|
|
505
505
|
messageDispatch,
|
|
506
506
|
mediaTransfer,
|
|
507
|
+
mediaUploadOptions: mediaMessageBuildOptions,
|
|
507
508
|
logger,
|
|
508
509
|
messageStore: sessionStore.messages,
|
|
509
510
|
messageSecretStore: sessionStore.messageSecret,
|
|
@@ -761,8 +761,8 @@ class WaAppStateMutationCoordinator {
|
|
|
761
761
|
statusPrivacy: {
|
|
762
762
|
mode: modeValue,
|
|
763
763
|
userJid,
|
|
764
|
-
...(input.shareToFB === undefined ? {} : {
|
|
765
|
-
...(input.shareToIG === undefined ? {} : {
|
|
764
|
+
...(input.shareToFB === undefined ? {} : { shareToFb: input.shareToFB }),
|
|
765
|
+
...(input.shareToIG === undefined ? {} : { shareToIg: input.shareToIG })
|
|
766
766
|
}
|
|
767
767
|
};
|
|
768
768
|
const timestamp = this.serverClock.nowMs();
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Readable } from 'node:stream';
|
|
2
2
|
import type { WaMessageDispatchCoordinator } from '../coordinators/WaMessageDispatchCoordinator';
|
|
3
3
|
import type { WaTrustedContactTokenCoordinator } from '../coordinators/WaTrustedContactTokenCoordinator';
|
|
4
|
+
import { type WaUploadMediaSource } from '../media';
|
|
5
|
+
import { type UploadResult, type WaMediaMessageOptions } from '../messaging/messages';
|
|
4
6
|
import type { WaDownloadMediaOptions, WaIncomingAddonEvent, WaIncomingMessageEvent, WaSendMessageOptions } from '../types';
|
|
5
7
|
import type { Logger } from '../../infra/log/types';
|
|
6
8
|
import type { WaMediaTransferClient } from '../../media/transfer/WaMediaTransferClient';
|
|
9
|
+
import type { MediaKind } from '../../media/types';
|
|
7
10
|
import type { PeerDataOperationRequester } from '../../message/primitives/peer-data-operation';
|
|
8
11
|
import type { WaMessagePublishResult, WaSendMessageContent, WaSendReceiptEventOptions, WaSendReceiptOptions } from '../../message/types';
|
|
9
12
|
import { type Proto } from '../../proto';
|
|
@@ -13,6 +16,8 @@ import { type WaMexQuerySocket } from '../../transport/node/mex/client';
|
|
|
13
16
|
export interface WaMessageCoordinatorDeps {
|
|
14
17
|
readonly messageDispatch: WaMessageDispatchCoordinator;
|
|
15
18
|
readonly mediaTransfer: WaMediaTransferClient;
|
|
19
|
+
/** Media upload wiring shared with the send path; backs {@link WaMessageCoordinator.upload}. */
|
|
20
|
+
readonly mediaUploadOptions: WaMediaMessageOptions;
|
|
16
21
|
readonly logger: Logger;
|
|
17
22
|
readonly messageStore: WaMessageStore;
|
|
18
23
|
readonly messageSecretStore: WaMessageSecretStore;
|
|
@@ -58,14 +63,47 @@ export interface WaMessageCappingInfo {
|
|
|
58
63
|
readonly mvStatus: string | null;
|
|
59
64
|
readonly cappingStatus: string | null;
|
|
60
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Media kinds accepted by {@link WaMessageCoordinator.upload}. `gif` is a
|
|
68
|
+
* GIF-playback video, `ptt` a voice note, `ptv` a round video note.
|
|
69
|
+
*/
|
|
70
|
+
export type WaUploadMediaType = MediaKind | 'gif' | 'ptt';
|
|
71
|
+
/** Options for {@link WaMessageCoordinator.upload}. */
|
|
72
|
+
export interface WaUploadMediaOptions {
|
|
73
|
+
/** Media type: sets the encryption context and CDN upload path. */
|
|
74
|
+
readonly type: WaUploadMediaType;
|
|
75
|
+
/** `Content-Type` for the upload and the mimetype for the message proto. */
|
|
76
|
+
readonly mimetype?: string;
|
|
77
|
+
/** Reuse a 32-byte media key instead of generating one. */
|
|
78
|
+
readonly mediaKey?: Uint8Array;
|
|
79
|
+
/** Override the streaming sidecar (default on for video/ptv/audio/gif/ptt). */
|
|
80
|
+
readonly sidecar?: boolean;
|
|
81
|
+
/** Animated-sticker first-frame length for the first-frame sidecar. */
|
|
82
|
+
readonly firstFrameLength?: number;
|
|
83
|
+
/** Per-upload transfer timeout override (ms). */
|
|
84
|
+
readonly timeoutMs?: number;
|
|
85
|
+
/** Cancellation signal forwarded to the CDN request. */
|
|
86
|
+
readonly signal?: AbortSignal;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Reusable descriptor returned by {@link WaMessageCoordinator.upload}: the
|
|
90
|
+
* {@link UploadResult} fields plus the media-key timestamp and echoed mimetype.
|
|
91
|
+
*/
|
|
92
|
+
export interface WaMediaUploadResult extends UploadResult {
|
|
93
|
+
/** Unix seconds the media key was minted; belongs on the message proto. */
|
|
94
|
+
readonly mediaKeyTimestamp: number;
|
|
95
|
+
/** Echo of the `mimetype` option when provided. */
|
|
96
|
+
readonly mimetype?: string;
|
|
97
|
+
}
|
|
61
98
|
/**
|
|
62
99
|
* Coordinates outbound message sending, receipts, addon decryption, media
|
|
63
|
-
* download, and the related MEX account queries. Accessed via
|
|
100
|
+
* upload/download, and the related MEX account queries. Accessed via
|
|
64
101
|
* {@link WaClient.message}.
|
|
65
102
|
*/
|
|
66
103
|
export declare class WaMessageCoordinator {
|
|
67
104
|
private readonly messageDispatch;
|
|
68
105
|
private readonly mediaTransfer;
|
|
106
|
+
private readonly mediaUploadOptions;
|
|
69
107
|
private readonly logger;
|
|
70
108
|
private readonly messageStore;
|
|
71
109
|
private readonly messageSecretStore;
|
|
@@ -183,6 +221,36 @@ export declare class WaMessageCoordinator {
|
|
|
183
221
|
*/
|
|
184
222
|
sendReceipt(target: WaIncomingMessageEvent | readonly WaIncomingMessageEvent[], options?: WaSendReceiptEventOptions): Promise<void>;
|
|
185
223
|
sendReceipt(jid: string, ids: string | readonly string[], options?: WaSendReceiptOptions): Promise<void>;
|
|
224
|
+
/**
|
|
225
|
+
* Encrypts and uploads standalone media to the WhatsApp CDN and returns the
|
|
226
|
+
* reusable descriptor, without sending a message - pre-upload once and
|
|
227
|
+
* reference it across sends, or build custom protos. Needs a connected
|
|
228
|
+
* session (the host token comes from a `media_conn` IQ). `source` is bytes,
|
|
229
|
+
* a file path, or a `Readable`. To send the result, spread its fields onto
|
|
230
|
+
* the matching proto message and pass that to {@link send}.
|
|
231
|
+
*
|
|
232
|
+
* @throws when `source` is unsupported, the file is unreadable, or the upload fails.
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* const media = await client.message.upload(await readFile('photo.jpg'), {
|
|
236
|
+
* type: 'image',
|
|
237
|
+
* mimetype: 'image/jpeg'
|
|
238
|
+
* })
|
|
239
|
+
* await client.message.send(jid, {
|
|
240
|
+
* imageMessage: {
|
|
241
|
+
* url: media.url,
|
|
242
|
+
* directPath: media.directPath,
|
|
243
|
+
* mediaKey: media.mediaKey,
|
|
244
|
+
* fileSha256: media.fileSha256,
|
|
245
|
+
* fileEncSha256: media.fileEncSha256,
|
|
246
|
+
* fileLength: media.fileLength,
|
|
247
|
+
* mediaKeyTimestamp: media.mediaKeyTimestamp,
|
|
248
|
+
* mimetype: media.mimetype
|
|
249
|
+
* }
|
|
250
|
+
* })
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
upload(source: WaUploadMediaSource, options: WaUploadMediaOptions): Promise<WaMediaUploadResult>;
|
|
186
254
|
/**
|
|
187
255
|
* Resolves the media payload inside `source` and returns a `Readable`
|
|
188
256
|
* stream of the decrypted bytes. Throws when the message has no
|
|
@@ -5,6 +5,8 @@ const node_fs_1 = require("node:fs");
|
|
|
5
5
|
const promises_1 = require("node:stream/promises");
|
|
6
6
|
const receipt_1 = require("../events/receipt");
|
|
7
7
|
const media_1 = require("../media");
|
|
8
|
+
const messages_1 = require("../messaging/messages");
|
|
9
|
+
const constants_1 = require("../../media/constants");
|
|
8
10
|
const addon_crypto_1 = require("../../message/crypto/addon-crypto");
|
|
9
11
|
const content_1 = require("../../message/encode/content");
|
|
10
12
|
const _proto_1 = require("../../proto");
|
|
@@ -34,15 +36,36 @@ function parseMessageCappingMexResponse(data) {
|
|
|
34
36
|
cappingStatus: (0, coercion_1.tryAsString)(root?.capping_status)
|
|
35
37
|
};
|
|
36
38
|
}
|
|
39
|
+
const SIDECAR_UPLOAD_TYPES = new Set([
|
|
40
|
+
'video',
|
|
41
|
+
'ptv',
|
|
42
|
+
'audio',
|
|
43
|
+
'gif',
|
|
44
|
+
'ptt'
|
|
45
|
+
]);
|
|
46
|
+
async function normalizeUploadSource(source) {
|
|
47
|
+
if (source instanceof Uint8Array) {
|
|
48
|
+
return source;
|
|
49
|
+
}
|
|
50
|
+
if (typeof source === 'string') {
|
|
51
|
+
await (0, media_1.assertReadableFile)(source);
|
|
52
|
+
return (0, node_fs_1.createReadStream)(source);
|
|
53
|
+
}
|
|
54
|
+
if ((0, media_1.isReadableStream)(source)) {
|
|
55
|
+
return source;
|
|
56
|
+
}
|
|
57
|
+
throw new Error('media upload received unsupported source type');
|
|
58
|
+
}
|
|
37
59
|
/**
|
|
38
60
|
* Coordinates outbound message sending, receipts, addon decryption, media
|
|
39
|
-
* download, and the related MEX account queries. Accessed via
|
|
61
|
+
* upload/download, and the related MEX account queries. Accessed via
|
|
40
62
|
* {@link WaClient.message}.
|
|
41
63
|
*/
|
|
42
64
|
class WaMessageCoordinator {
|
|
43
65
|
constructor(deps) {
|
|
44
66
|
this.messageDispatch = deps.messageDispatch;
|
|
45
67
|
this.mediaTransfer = deps.mediaTransfer;
|
|
68
|
+
this.mediaUploadOptions = deps.mediaUploadOptions;
|
|
46
69
|
this.logger = deps.logger;
|
|
47
70
|
this.messageStore = deps.messageStore;
|
|
48
71
|
this.messageSecretStore = deps.messageSecretStore;
|
|
@@ -222,6 +245,58 @@ class WaMessageCoordinator {
|
|
|
222
245
|
});
|
|
223
246
|
}
|
|
224
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Encrypts and uploads standalone media to the WhatsApp CDN and returns the
|
|
250
|
+
* reusable descriptor, without sending a message - pre-upload once and
|
|
251
|
+
* reference it across sends, or build custom protos. Needs a connected
|
|
252
|
+
* session (the host token comes from a `media_conn` IQ). `source` is bytes,
|
|
253
|
+
* a file path, or a `Readable`. To send the result, spread its fields onto
|
|
254
|
+
* the matching proto message and pass that to {@link send}.
|
|
255
|
+
*
|
|
256
|
+
* @throws when `source` is unsupported, the file is unreadable, or the upload fails.
|
|
257
|
+
* @example
|
|
258
|
+
* ```ts
|
|
259
|
+
* const media = await client.message.upload(await readFile('photo.jpg'), {
|
|
260
|
+
* type: 'image',
|
|
261
|
+
* mimetype: 'image/jpeg'
|
|
262
|
+
* })
|
|
263
|
+
* await client.message.send(jid, {
|
|
264
|
+
* imageMessage: {
|
|
265
|
+
* url: media.url,
|
|
266
|
+
* directPath: media.directPath,
|
|
267
|
+
* mediaKey: media.mediaKey,
|
|
268
|
+
* fileSha256: media.fileSha256,
|
|
269
|
+
* fileEncSha256: media.fileEncSha256,
|
|
270
|
+
* fileLength: media.fileLength,
|
|
271
|
+
* mediaKeyTimestamp: media.mediaKeyTimestamp,
|
|
272
|
+
* mimetype: media.mimetype
|
|
273
|
+
* }
|
|
274
|
+
* })
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
async upload(source, options) {
|
|
278
|
+
const uploadPath = constants_1.MEDIA_UPLOAD_PATHS[options.type];
|
|
279
|
+
if (!uploadPath) {
|
|
280
|
+
throw new Error(`unknown media upload type: ${String(options.type)}`);
|
|
281
|
+
}
|
|
282
|
+
const result = await (0, messages_1.uploadMedia)(this.mediaUploadOptions, {
|
|
283
|
+
source: await normalizeUploadSource(source),
|
|
284
|
+
cryptoType: options.type,
|
|
285
|
+
uploadPath,
|
|
286
|
+
contentType: options.mimetype,
|
|
287
|
+
mediaKey: options.mediaKey,
|
|
288
|
+
sidecar: options.sidecar ?? SIDECAR_UPLOAD_TYPES.has(options.type),
|
|
289
|
+
firstFrameLength: options.firstFrameLength,
|
|
290
|
+
timeoutMs: options.timeoutMs,
|
|
291
|
+
signal: options.signal,
|
|
292
|
+
logLabel: 'user media upload'
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
...result,
|
|
296
|
+
mediaKeyTimestamp: this.mediaUploadOptions.serverClock.nowSeconds(),
|
|
297
|
+
mimetype: options.mimetype
|
|
298
|
+
};
|
|
299
|
+
}
|
|
225
300
|
/**
|
|
226
301
|
* Resolves the media payload inside `source` and returns a `Readable`
|
|
227
302
|
* stream of the decrypted bytes. Throws when the message has no
|
|
@@ -318,7 +318,7 @@ class WaMessageDispatchCoordinator {
|
|
|
318
318
|
}
|
|
319
319
|
try {
|
|
320
320
|
const results = await this.deps.signalDeviceSync.queryLidsByPhoneJids([pnUserJid]);
|
|
321
|
-
const match = results.find((entry) => entry.
|
|
321
|
+
const match = results.find((entry) => entry.queriedJid === pnUserJid);
|
|
322
322
|
if (match?.lidJid)
|
|
323
323
|
return match.lidJid;
|
|
324
324
|
}
|
package/dist/client/media.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ export interface ResolvedMediaInputs {
|
|
|
57
57
|
readonly uploadMedia: Uint8Array | Readable;
|
|
58
58
|
readonly tempFilePath?: string;
|
|
59
59
|
}
|
|
60
|
+
export declare function assertReadableFile(filePath: string): Promise<void>;
|
|
60
61
|
export declare function resolveMediaInputs(shouldProcess: boolean, raw: Uint8Array | ArrayBuffer | Readable | string): Promise<ResolvedMediaInputs>;
|
|
61
62
|
export declare function shouldNormalizeVoiceNote(media: WaMediaOptions | undefined, content: WaSendMediaMessage): content is WaSendMediaMessage & {
|
|
62
63
|
type: 'audio';
|
package/dist/client/media.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.buildMediaUploadUrl = buildMediaUploadUrl;
|
|
|
9
9
|
exports.assertMediaUploadStatus = assertMediaUploadStatus;
|
|
10
10
|
exports.parseMediaUploadJsonBody = parseMediaUploadJsonBody;
|
|
11
11
|
exports.performPlaintextMediaUpload = performPlaintextMediaUpload;
|
|
12
|
+
exports.assertReadableFile = assertReadableFile;
|
|
12
13
|
exports.resolveMediaInputs = resolveMediaInputs;
|
|
13
14
|
exports.shouldNormalizeVoiceNote = shouldNormalizeVoiceNote;
|
|
14
15
|
exports.hasMediaProcessingTasks = hasMediaProcessingTasks;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
1
2
|
import type { ResolvedLinkPreviewResult } from '../messaging/link-preview';
|
|
2
3
|
import type { WaMediaOptions } from '../types';
|
|
3
4
|
import type { Logger } from '../../infra/log/types';
|
|
4
5
|
import type { WaMediaTransferClient } from '../../media/transfer/WaMediaTransferClient';
|
|
5
|
-
import type { WaMediaConn } from '../../media/types';
|
|
6
|
+
import type { MediaCryptoType, WaMediaConn } from '../../media/types';
|
|
6
7
|
import type { WaMessageBuildResult, WaSendMessageContent, WaSendTextMessage } from '../../message/types';
|
|
7
8
|
import type { BinaryNode } from '../../transport/types';
|
|
8
9
|
import type { ServerClock } from '../../util/clock';
|
|
@@ -26,3 +27,46 @@ export interface WaBuildMessageContext {
|
|
|
26
27
|
}
|
|
27
28
|
export declare function buildMediaMessageContent(options: WaMediaMessageOptions, content: WaSendMessageContent, ctx?: WaBuildMessageContext): Promise<WaMessageBuildResult>;
|
|
28
29
|
export declare function getMediaConn(options: WaMediaMessageOptions, forceRefresh?: boolean): Promise<WaMediaConn>;
|
|
30
|
+
/** Descriptor produced by {@link uploadMedia}: CDN location, media key, hashes, and optional sidecars. */
|
|
31
|
+
export interface UploadResult {
|
|
32
|
+
/** Absolute CDN URL of the uploaded ciphertext. */
|
|
33
|
+
readonly url: string;
|
|
34
|
+
/** Host-relative path; pairs with a media host or feeds `downloadMediaMessage`. */
|
|
35
|
+
readonly directPath: string;
|
|
36
|
+
/** 32-byte media key used to encrypt (and later decrypt) the payload. */
|
|
37
|
+
readonly mediaKey: Uint8Array;
|
|
38
|
+
/** SHA-256 of the plaintext. */
|
|
39
|
+
readonly fileSha256: Uint8Array;
|
|
40
|
+
/** SHA-256 of the encrypted `ciphertext||mac`. */
|
|
41
|
+
readonly fileEncSha256: Uint8Array;
|
|
42
|
+
/** Plaintext byte length. */
|
|
43
|
+
readonly fileLength: number;
|
|
44
|
+
/** Server metadata URL, when the CDN returns one (video). */
|
|
45
|
+
readonly metadataUrl?: string;
|
|
46
|
+
/** Streaming sidecar for seekable playback, when computed. */
|
|
47
|
+
readonly streamingSidecar?: Uint8Array;
|
|
48
|
+
/** First-frame sidecar for animated stickers, when computed. */
|
|
49
|
+
readonly firstFrameSidecar?: Uint8Array;
|
|
50
|
+
/** First-frame length echoed back for animated stickers. */
|
|
51
|
+
readonly firstFrameLength?: number;
|
|
52
|
+
}
|
|
53
|
+
/** Fully-resolved input for {@link uploadMedia} (crypto type, path, sidecar already decided). */
|
|
54
|
+
export interface WaMediaUploadInput {
|
|
55
|
+
readonly source: Uint8Array | Readable;
|
|
56
|
+
readonly cryptoType: MediaCryptoType;
|
|
57
|
+
readonly uploadPath: string;
|
|
58
|
+
readonly contentType?: string;
|
|
59
|
+
readonly mediaKey?: Uint8Array;
|
|
60
|
+
readonly sidecar: boolean;
|
|
61
|
+
readonly firstFrameLength?: number;
|
|
62
|
+
readonly logLabel?: string;
|
|
63
|
+
readonly timeoutMs?: number;
|
|
64
|
+
readonly signal?: AbortSignal;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Shared media upload primitive: derives (or reuses) a media key, encrypts,
|
|
68
|
+
* fetches a media connection, and POSTs the ciphertext. Bytes take a
|
|
69
|
+
* zero-temp-file fast path; streams stage to a temp file first. Backs both the
|
|
70
|
+
* send path and `WaMessageCoordinator.upload`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function uploadMedia(options: WaMediaMessageOptions, input: WaMediaUploadInput): Promise<UploadResult>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildMediaMessageContent = buildMediaMessageContent;
|
|
4
4
|
exports.getMediaConn = getMediaConn;
|
|
5
|
+
exports.uploadMedia = uploadMedia;
|
|
5
6
|
const node_fs_1 = require("node:fs");
|
|
6
7
|
const node_stream_1 = require("node:stream");
|
|
7
8
|
const media_1 = require("../media");
|
|
@@ -345,9 +346,15 @@ async function buildMediaMessage(options, content) {
|
|
|
345
346
|
const firstFrameLength = content.type === 'sticker'
|
|
346
347
|
? (content.firstFrameLength ?? detectedFirstFrameLength)
|
|
347
348
|
: undefined;
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
:
|
|
349
|
+
const uploadType = resolveUploadType(content);
|
|
350
|
+
const uploadPromise = uploadMedia(options, {
|
|
351
|
+
source: resolved.uploadMedia,
|
|
352
|
+
cryptoType: uploadType,
|
|
353
|
+
uploadPath: resolveUploadPath(uploadType),
|
|
354
|
+
contentType: mimetype,
|
|
355
|
+
sidecar: needsSidecar(content),
|
|
356
|
+
firstFrameLength
|
|
357
|
+
});
|
|
351
358
|
const processPromise = (0, media_1.runMediaProcessor)(options.media, resolved.processorInput, content, options.logger);
|
|
352
359
|
const [uploadResult, processResult] = await Promise.allSettled([
|
|
353
360
|
uploadPromise,
|
|
@@ -508,29 +515,42 @@ function parseUploadResponse(body, status) {
|
|
|
508
515
|
...(parsed.metadata_url ? { metadataUrl: parsed.metadata_url } : {})
|
|
509
516
|
};
|
|
510
517
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
518
|
+
/**
|
|
519
|
+
* Shared media upload primitive: derives (or reuses) a media key, encrypts,
|
|
520
|
+
* fetches a media connection, and POSTs the ciphertext. Bytes take a
|
|
521
|
+
* zero-temp-file fast path; streams stage to a temp file first. Backs both the
|
|
522
|
+
* send path and `WaMessageCoordinator.upload`.
|
|
523
|
+
*/
|
|
524
|
+
async function uploadMedia(options, input) {
|
|
525
|
+
const mediaKey = input.mediaKey ?? (await WaMediaCrypto_1.WaMediaCrypto.generateMediaKey());
|
|
526
|
+
if (input.source instanceof Uint8Array) {
|
|
527
|
+
return uploadMediaBytes(options, input, mediaKey, input.source);
|
|
528
|
+
}
|
|
529
|
+
return uploadMediaStream(options, input, mediaKey, input.source);
|
|
530
|
+
}
|
|
531
|
+
async function uploadMediaBytes(options, input, mediaKey, mediaBytes) {
|
|
514
532
|
const [encrypted, mediaConn] = await Promise.all([
|
|
515
|
-
WaMediaCrypto_1.WaMediaCrypto.encryptBytes(
|
|
516
|
-
sidecar:
|
|
517
|
-
firstFrameLength
|
|
533
|
+
WaMediaCrypto_1.WaMediaCrypto.encryptBytes(input.cryptoType, mediaKey, mediaBytes, {
|
|
534
|
+
sidecar: input.sidecar,
|
|
535
|
+
firstFrameLength: input.firstFrameLength
|
|
518
536
|
}),
|
|
519
537
|
getMediaConn(options)
|
|
520
538
|
]);
|
|
521
539
|
const selectedHost = (0, media_1.selectMediaUploadHost)(mediaConn);
|
|
522
|
-
const uploadUrl = (0, media_1.buildMediaUploadUrl)(selectedHost,
|
|
523
|
-
options.logger.debug('sending media upload request', {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
540
|
+
const uploadUrl = (0, media_1.buildMediaUploadUrl)(selectedHost, input.uploadPath, mediaConn.auth, encrypted.fileEncSha256);
|
|
541
|
+
options.logger.debug(input.logLabel ?? 'sending media upload request', {
|
|
542
|
+
cryptoType: input.cryptoType,
|
|
543
|
+
host: selectedHost,
|
|
544
|
+
size: mediaBytes.byteLength
|
|
527
545
|
});
|
|
528
546
|
const uploadResponse = await options.mediaTransfer.uploadStream({
|
|
529
547
|
url: uploadUrl,
|
|
530
548
|
method: 'POST',
|
|
531
549
|
body: encrypted.ciphertextHmac,
|
|
532
550
|
contentLength: encrypted.ciphertextHmac.byteLength,
|
|
533
|
-
contentType:
|
|
551
|
+
contentType: input.contentType,
|
|
552
|
+
timeoutMs: input.timeoutMs,
|
|
553
|
+
signal: input.signal
|
|
534
554
|
});
|
|
535
555
|
const responseBody = await options.mediaTransfer.readResponseBytes(uploadResponse);
|
|
536
556
|
const parsed = parseUploadResponse(responseBody, uploadResponse.status);
|
|
@@ -542,19 +562,22 @@ async function uploadMediaBytes(options, content, mediaBytes, firstFrameLength,
|
|
|
542
562
|
fileLength: mediaBytes.byteLength,
|
|
543
563
|
streamingSidecar: encrypted.streamingSidecar,
|
|
544
564
|
firstFrameSidecar: encrypted.firstFrameSidecar,
|
|
545
|
-
firstFrameLength
|
|
565
|
+
firstFrameLength: input.firstFrameLength
|
|
546
566
|
};
|
|
547
567
|
}
|
|
548
|
-
async function
|
|
549
|
-
const encResult = await WaMediaCrypto_1.WaMediaCrypto.encryptToFile(input.cryptoType,
|
|
568
|
+
async function uploadMediaStream(options, input, mediaKey, plaintext) {
|
|
569
|
+
const encResult = await WaMediaCrypto_1.WaMediaCrypto.encryptToFile(input.cryptoType, mediaKey, plaintext, {
|
|
570
|
+
sidecar: input.sidecar,
|
|
571
|
+
firstFrameLength: input.firstFrameLength
|
|
572
|
+
});
|
|
550
573
|
let readStream;
|
|
551
574
|
try {
|
|
552
575
|
const mediaConn = await getMediaConn(options);
|
|
553
576
|
const selectedHost = (0, media_1.selectMediaUploadHost)(mediaConn);
|
|
554
577
|
const uploadUrl = (0, media_1.buildMediaUploadUrl)(selectedHost, input.uploadPath, mediaConn.auth, encResult.fileEncSha256);
|
|
555
|
-
options.logger.debug(input.logLabel, {
|
|
578
|
+
options.logger.debug(input.logLabel ?? 'sending media stream upload request', {
|
|
579
|
+
cryptoType: input.cryptoType,
|
|
556
580
|
host: selectedHost,
|
|
557
|
-
uploadPath: input.uploadPath,
|
|
558
581
|
plaintextLength: encResult.plaintextLength,
|
|
559
582
|
encryptedSize: encResult.fileSize
|
|
560
583
|
});
|
|
@@ -564,13 +587,15 @@ async function uploadEncryptedStream(options, input) {
|
|
|
564
587
|
method: 'POST',
|
|
565
588
|
body: readStream,
|
|
566
589
|
contentLength: encResult.fileSize,
|
|
567
|
-
contentType: input.contentType
|
|
590
|
+
contentType: input.contentType,
|
|
591
|
+
timeoutMs: input.timeoutMs,
|
|
592
|
+
signal: input.signal
|
|
568
593
|
});
|
|
569
594
|
const responseBody = await options.mediaTransfer.readResponseBytes(uploadResponse);
|
|
570
595
|
const parsed = parseUploadResponse(responseBody, uploadResponse.status);
|
|
571
596
|
return {
|
|
572
597
|
...parsed,
|
|
573
|
-
mediaKey
|
|
598
|
+
mediaKey,
|
|
574
599
|
fileSha256: encResult.fileSha256,
|
|
575
600
|
fileEncSha256: encResult.fileEncSha256,
|
|
576
601
|
fileLength: encResult.plaintextLength,
|
|
@@ -589,19 +614,6 @@ async function uploadEncryptedStream(options, input) {
|
|
|
589
614
|
await WaMediaCrypto_1.WaMediaCrypto.cleanupEncryptedFile(encResult.filePath);
|
|
590
615
|
}
|
|
591
616
|
}
|
|
592
|
-
async function uploadMediaStream(options, content, stream, firstFrameLength, mimetype) {
|
|
593
|
-
const cryptoType = resolveUploadType(content);
|
|
594
|
-
return uploadEncryptedStream(options, {
|
|
595
|
-
plaintext: stream,
|
|
596
|
-
mediaKey: await WaMediaCrypto_1.WaMediaCrypto.generateMediaKey(),
|
|
597
|
-
cryptoType,
|
|
598
|
-
uploadPath: resolveUploadPath(cryptoType),
|
|
599
|
-
contentType: mimetype,
|
|
600
|
-
logLabel: 'sending media stream upload request',
|
|
601
|
-
sidecar: needsSidecar(content),
|
|
602
|
-
firstFrameLength
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
617
|
function openStickerPackInputStream(media) {
|
|
606
618
|
return typeof media === 'string' ? (0, node_fs_1.createReadStream)(media) : node_stream_1.Readable.from([media]);
|
|
607
619
|
}
|
|
@@ -613,23 +625,23 @@ async function buildStickerPackMediaMessage(options, content) {
|
|
|
613
625
|
const coverThumbnail = content.coverThumbnail;
|
|
614
626
|
const mediaKey = await WaMediaCrypto_1.WaMediaCrypto.generateMediaKey();
|
|
615
627
|
const [bundle, cover] = await Promise.all([
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
mediaKey,
|
|
628
|
+
uploadMedia(options, {
|
|
629
|
+
source: (0, sticker_pack_1.createStickerPackZipStream)((0, sticker_pack_2.toStickerPackZipEntries)(content)),
|
|
619
630
|
cryptoType: 'sticker-pack',
|
|
620
631
|
uploadPath: constants_1.MEDIA_UPLOAD_PATHS['sticker-pack'],
|
|
621
632
|
contentType: 'application/zip',
|
|
622
|
-
logLabel: 'sending sticker pack bundle upload',
|
|
623
|
-
sidecar: false
|
|
624
|
-
}),
|
|
625
|
-
uploadEncryptedStream(options, {
|
|
626
|
-
plaintext: openStickerPackInputStream(coverThumbnail),
|
|
627
633
|
mediaKey,
|
|
634
|
+
sidecar: false,
|
|
635
|
+
logLabel: 'sending sticker pack bundle upload'
|
|
636
|
+
}),
|
|
637
|
+
uploadMedia(options, {
|
|
638
|
+
source: openStickerPackInputStream(coverThumbnail),
|
|
628
639
|
cryptoType: 'thumbnail-sticker-pack',
|
|
629
640
|
uploadPath: constants_1.MEDIA_UPLOAD_PATHS['thumbnail-sticker-pack'],
|
|
630
641
|
contentType: 'image/jpeg',
|
|
631
|
-
|
|
632
|
-
sidecar: false
|
|
642
|
+
mediaKey,
|
|
643
|
+
sidecar: false,
|
|
644
|
+
logLabel: 'sending sticker pack thumbnail upload'
|
|
633
645
|
})
|
|
634
646
|
]);
|
|
635
647
|
if (cover.fileLength === 0) {
|
|
@@ -411,7 +411,7 @@ class WaClientImpl extends EventEmitter {
|
|
|
411
411
|
get auth() {
|
|
412
412
|
return this.deps.authClient;
|
|
413
413
|
}
|
|
414
|
-
/** Message coordinator: send/receive, receipts, addons, media download. */
|
|
414
|
+
/** Message coordinator: send/receive, receipts, addons, media upload/download. */
|
|
415
415
|
get message() {
|
|
416
416
|
return this.deps.messageCoordinator;
|
|
417
417
|
}
|
|
@@ -500,6 +500,7 @@ export function buildWaClientDependencies(input) {
|
|
|
500
500
|
const messageCoordinator = new WaMessageCoordinator({
|
|
501
501
|
messageDispatch,
|
|
502
502
|
mediaTransfer,
|
|
503
|
+
mediaUploadOptions: mediaMessageBuildOptions,
|
|
503
504
|
logger,
|
|
504
505
|
messageStore: sessionStore.messages,
|
|
505
506
|
messageSecretStore: sessionStore.messageSecret,
|
|
@@ -758,8 +758,8 @@ export class WaAppStateMutationCoordinator {
|
|
|
758
758
|
statusPrivacy: {
|
|
759
759
|
mode: modeValue,
|
|
760
760
|
userJid,
|
|
761
|
-
...(input.shareToFB === undefined ? {} : {
|
|
762
|
-
...(input.shareToIG === undefined ? {} : {
|
|
761
|
+
...(input.shareToFB === undefined ? {} : { shareToFb: input.shareToFB }),
|
|
762
|
+
...(input.shareToIG === undefined ? {} : { shareToIg: input.shareToIG })
|
|
763
763
|
}
|
|
764
764
|
};
|
|
765
765
|
const timestamp = this.serverClock.nowMs();
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { createWriteStream } from 'node:fs';
|
|
1
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
2
2
|
import { pipeline } from 'node:stream/promises';
|
|
3
3
|
import { aggregateReceiptTargets } from '../events/receipt.js';
|
|
4
|
-
import { downloadMediaMessage } from '../media.js';
|
|
4
|
+
import { assertReadableFile, downloadMediaMessage, isReadableStream } from '../media.js';
|
|
5
|
+
import { uploadMedia } from '../messaging/messages.js';
|
|
6
|
+
import { MEDIA_UPLOAD_PATHS } from '../../media/constants.js';
|
|
5
7
|
import { buildAddonAdditionalData, decodeAddonPlaintext, decryptAddonPayload, identifyEncryptedAddon, resolveParentMessageSecret, resolvePollOptionNames, shouldUseAddonAdditionalData } from '../../message/crypto/addon-crypto.js';
|
|
6
8
|
import { unwrapMessage } from '../../message/encode/content.js';
|
|
7
9
|
import { proto } from '../../proto.js';
|
|
@@ -31,15 +33,36 @@ function parseMessageCappingMexResponse(data) {
|
|
|
31
33
|
cappingStatus: tryAsString(root?.capping_status)
|
|
32
34
|
};
|
|
33
35
|
}
|
|
36
|
+
const SIDECAR_UPLOAD_TYPES = new Set([
|
|
37
|
+
'video',
|
|
38
|
+
'ptv',
|
|
39
|
+
'audio',
|
|
40
|
+
'gif',
|
|
41
|
+
'ptt'
|
|
42
|
+
]);
|
|
43
|
+
async function normalizeUploadSource(source) {
|
|
44
|
+
if (source instanceof Uint8Array) {
|
|
45
|
+
return source;
|
|
46
|
+
}
|
|
47
|
+
if (typeof source === 'string') {
|
|
48
|
+
await assertReadableFile(source);
|
|
49
|
+
return createReadStream(source);
|
|
50
|
+
}
|
|
51
|
+
if (isReadableStream(source)) {
|
|
52
|
+
return source;
|
|
53
|
+
}
|
|
54
|
+
throw new Error('media upload received unsupported source type');
|
|
55
|
+
}
|
|
34
56
|
/**
|
|
35
57
|
* Coordinates outbound message sending, receipts, addon decryption, media
|
|
36
|
-
* download, and the related MEX account queries. Accessed via
|
|
58
|
+
* upload/download, and the related MEX account queries. Accessed via
|
|
37
59
|
* {@link WaClient.message}.
|
|
38
60
|
*/
|
|
39
61
|
export class WaMessageCoordinator {
|
|
40
62
|
constructor(deps) {
|
|
41
63
|
this.messageDispatch = deps.messageDispatch;
|
|
42
64
|
this.mediaTransfer = deps.mediaTransfer;
|
|
65
|
+
this.mediaUploadOptions = deps.mediaUploadOptions;
|
|
43
66
|
this.logger = deps.logger;
|
|
44
67
|
this.messageStore = deps.messageStore;
|
|
45
68
|
this.messageSecretStore = deps.messageSecretStore;
|
|
@@ -219,6 +242,58 @@ export class WaMessageCoordinator {
|
|
|
219
242
|
});
|
|
220
243
|
}
|
|
221
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Encrypts and uploads standalone media to the WhatsApp CDN and returns the
|
|
247
|
+
* reusable descriptor, without sending a message - pre-upload once and
|
|
248
|
+
* reference it across sends, or build custom protos. Needs a connected
|
|
249
|
+
* session (the host token comes from a `media_conn` IQ). `source` is bytes,
|
|
250
|
+
* a file path, or a `Readable`. To send the result, spread its fields onto
|
|
251
|
+
* the matching proto message and pass that to {@link send}.
|
|
252
|
+
*
|
|
253
|
+
* @throws when `source` is unsupported, the file is unreadable, or the upload fails.
|
|
254
|
+
* @example
|
|
255
|
+
* ```ts
|
|
256
|
+
* const media = await client.message.upload(await readFile('photo.jpg'), {
|
|
257
|
+
* type: 'image',
|
|
258
|
+
* mimetype: 'image/jpeg'
|
|
259
|
+
* })
|
|
260
|
+
* await client.message.send(jid, {
|
|
261
|
+
* imageMessage: {
|
|
262
|
+
* url: media.url,
|
|
263
|
+
* directPath: media.directPath,
|
|
264
|
+
* mediaKey: media.mediaKey,
|
|
265
|
+
* fileSha256: media.fileSha256,
|
|
266
|
+
* fileEncSha256: media.fileEncSha256,
|
|
267
|
+
* fileLength: media.fileLength,
|
|
268
|
+
* mediaKeyTimestamp: media.mediaKeyTimestamp,
|
|
269
|
+
* mimetype: media.mimetype
|
|
270
|
+
* }
|
|
271
|
+
* })
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
async upload(source, options) {
|
|
275
|
+
const uploadPath = MEDIA_UPLOAD_PATHS[options.type];
|
|
276
|
+
if (!uploadPath) {
|
|
277
|
+
throw new Error(`unknown media upload type: ${String(options.type)}`);
|
|
278
|
+
}
|
|
279
|
+
const result = await uploadMedia(this.mediaUploadOptions, {
|
|
280
|
+
source: await normalizeUploadSource(source),
|
|
281
|
+
cryptoType: options.type,
|
|
282
|
+
uploadPath,
|
|
283
|
+
contentType: options.mimetype,
|
|
284
|
+
mediaKey: options.mediaKey,
|
|
285
|
+
sidecar: options.sidecar ?? SIDECAR_UPLOAD_TYPES.has(options.type),
|
|
286
|
+
firstFrameLength: options.firstFrameLength,
|
|
287
|
+
timeoutMs: options.timeoutMs,
|
|
288
|
+
signal: options.signal,
|
|
289
|
+
logLabel: 'user media upload'
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
...result,
|
|
293
|
+
mediaKeyTimestamp: this.mediaUploadOptions.serverClock.nowSeconds(),
|
|
294
|
+
mimetype: options.mimetype
|
|
295
|
+
};
|
|
296
|
+
}
|
|
222
297
|
/**
|
|
223
298
|
* Resolves the media payload inside `source` and returns a `Readable`
|
|
224
299
|
* stream of the decrypted bytes. Throws when the message has no
|