zapo-js 1.8.0 → 1.8.1
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.js +2 -1
- package/dist/client/coordinators/WaRetryCoordinator.js +1 -1
- package/dist/client/persistence/group-history.js +4 -1
- package/dist/client/persistence/history-sync.d.ts +2 -0
- package/dist/client/persistence/history-sync.js +33 -19
- package/dist/esm/client/WaClient.js +2 -1
- package/dist/esm/client/coordinators/WaRetryCoordinator.js +1 -1
- package/dist/esm/client/persistence/group-history.js +4 -1
- package/dist/esm/client/persistence/history-sync.js +34 -20
- package/dist/esm/media/constants.js +6 -0
- package/dist/esm/media/container.js +105 -0
- package/dist/esm/media/crypto/WaMediaCrypto.js +9 -2
- package/dist/esm/message/primitives/incoming.js +27 -3
- package/dist/media/constants.d.ts +6 -0
- package/dist/media/constants.js +7 -1
- package/dist/media/container.d.ts +43 -0
- package/dist/media/container.js +110 -0
- package/dist/media/crypto/WaMediaCrypto.js +9 -2
- package/dist/message/primitives/incoming.d.ts +17 -0
- package/dist/message/primitives/incoming.js +28 -3
- package/package.json +1 -1
package/dist/client/WaClient.js
CHANGED
|
@@ -278,7 +278,8 @@ class WaClientImpl extends node_events_1.EventEmitter {
|
|
|
278
278
|
emitEvent: this.emit.bind(this),
|
|
279
279
|
onPrivacyTokens: (conversations) => this.deps.trustedContactToken.hydrateFromHistorySync(conversations),
|
|
280
280
|
onNctSalt: (salt) => this.deps.trustedContactToken.hydrateNctSaltFromHistorySync(salt),
|
|
281
|
-
onProcessed: sendHistSyncReceipt
|
|
281
|
+
onProcessed: sendHistSyncReceipt,
|
|
282
|
+
meJid: this.deps.authClient.getCurrentCredentials()?.meJid
|
|
282
283
|
}, protocolMessage.historySyncNotification);
|
|
283
284
|
}
|
|
284
285
|
else if (sendHistSyncReceipt) {
|
|
@@ -25,7 +25,7 @@ const RETRY_SESSION_BASE_KEY_CACHE_MAX_ENTRIES = 8192;
|
|
|
25
25
|
// serialize on the prekey lock and hit the store); excess under flood is dropped.
|
|
26
26
|
const DECRYPT_FAILURE_QUEUE_MAX_SIZE = 1024;
|
|
27
27
|
const DECRYPT_FAILURE_QUEUE_MAX_CONCURRENCY = 8;
|
|
28
|
-
const PLACEHOLDER_RESEND_RETRY_THRESHOLD =
|
|
28
|
+
const PLACEHOLDER_RESEND_RETRY_THRESHOLD = 4;
|
|
29
29
|
const PLACEHOLDER_RESEND_BATCH_SIZE = 32;
|
|
30
30
|
const PLACEHOLDER_RESEND_DEBOUNCE_MS = 200;
|
|
31
31
|
const PLACEHOLDER_RESEND_FALLBACK_MAX_AGE_DAYS = 14;
|
|
@@ -4,6 +4,7 @@ exports.runGroupHistoryBundle = runGroupHistoryBundle;
|
|
|
4
4
|
exports.processGroupHistoryBundle = processGroupHistoryBundle;
|
|
5
5
|
const history_blob_1 = require("../persistence/history-blob");
|
|
6
6
|
const group_history_1 = require("../../message/kinds/group-history");
|
|
7
|
+
const incoming_1 = require("../../message/primitives/incoming");
|
|
7
8
|
const _proto_1 = require("../../proto");
|
|
8
9
|
const jid_1 = require("../../protocol/jid");
|
|
9
10
|
const primitives_1 = require("../../util/primitives");
|
|
@@ -79,10 +80,12 @@ async function processGroupHistoryBundle(deps, input) {
|
|
|
79
80
|
(oldestTimestampMs === undefined || timestampMs < oldestTimestampMs)) {
|
|
80
81
|
oldestTimestampMs = timestampMs;
|
|
81
82
|
}
|
|
83
|
+
const authorJid = (0, incoming_1.resolveWebMessageInfoAuthor)(webMsg, deps.meJid, input.groupJid);
|
|
82
84
|
const write = deps.writeBehind.persistMessageAsync({
|
|
83
85
|
id: webMsg.key.id,
|
|
84
86
|
threadJid: input.groupJid,
|
|
85
|
-
senderJid:
|
|
87
|
+
senderJid: authorJid,
|
|
88
|
+
participantJid: authorJid,
|
|
86
89
|
fromMe: webMsg.key.fromMe === true,
|
|
87
90
|
timestampMs: timestampMs || undefined,
|
|
88
91
|
messageBytes: _proto_1.proto.Message.encode(webMsg.message).finish()
|
|
@@ -33,6 +33,8 @@ interface WaHistorySyncDeps {
|
|
|
33
33
|
readonly onNctSalt?: (salt: Uint8Array) => Promise<void>;
|
|
34
34
|
/** Acks the chunk via the `hist_sync` receipt so the primary stops resending it. */
|
|
35
35
|
readonly onProcessed?: (syncType: Proto.Message.HistorySyncType) => Promise<void>;
|
|
36
|
+
/** Author fallback for self-sent group messages that carry no participant. */
|
|
37
|
+
readonly meJid?: string | null;
|
|
36
38
|
}
|
|
37
39
|
export declare function runHistorySyncNotification(deps: WaHistorySyncDeps, notification: Proto.Message.IHistorySyncNotification): Promise<void>;
|
|
38
40
|
/**
|
|
@@ -4,6 +4,7 @@ exports.CONVERSATION_FIELDS = exports.HISTORY_SYNC_FIELDS = void 0;
|
|
|
4
4
|
exports.runHistorySyncNotification = runHistorySyncNotification;
|
|
5
5
|
exports.processHistorySyncNotification = processHistorySyncNotification;
|
|
6
6
|
const history_blob_1 = require("../persistence/history-blob");
|
|
7
|
+
const incoming_1 = require("../../message/primitives/incoming");
|
|
7
8
|
const _proto_1 = require("../../proto");
|
|
8
9
|
const jid_1 = require("../../protocol/jid");
|
|
9
10
|
const message_1 = require("../../protocol/message");
|
|
@@ -158,15 +159,7 @@ async function consumeHistorySyncStream(deps, inflated, state) {
|
|
|
158
159
|
parked[parked.length] = message;
|
|
159
160
|
return undefined;
|
|
160
161
|
}
|
|
161
|
-
state
|
|
162
|
-
pushWrite(state, deps.writeBehind.persistMessageAsync({
|
|
163
|
-
id: message.id,
|
|
164
|
-
threadJid,
|
|
165
|
-
senderJid: message.senderJid,
|
|
166
|
-
fromMe: message.fromMe,
|
|
167
|
-
timestampMs: message.timestampMs,
|
|
168
|
-
messageBytes: message.messageBytes
|
|
169
|
-
}));
|
|
162
|
+
persistHistoryMessage(deps, state, message, threadJid);
|
|
170
163
|
return maybeFlush(state);
|
|
171
164
|
}
|
|
172
165
|
if (headerParts) {
|
|
@@ -292,18 +285,35 @@ async function closeConversation(deps, state, headerParts, threadJid, parked) {
|
|
|
292
285
|
}
|
|
293
286
|
}
|
|
294
287
|
for (const message of parked ?? []) {
|
|
295
|
-
state
|
|
296
|
-
pushWrite(state, deps.writeBehind.persistMessageAsync({
|
|
297
|
-
id: message.id,
|
|
298
|
-
threadJid: resolvedJid,
|
|
299
|
-
senderJid: message.senderJid,
|
|
300
|
-
fromMe: message.fromMe,
|
|
301
|
-
timestampMs: message.timestampMs,
|
|
302
|
-
messageBytes: message.messageBytes
|
|
303
|
-
}));
|
|
288
|
+
persistHistoryMessage(deps, state, message, resolvedJid);
|
|
304
289
|
}
|
|
305
290
|
await maybeFlush(state);
|
|
306
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Resolves the author here rather than at decode time because a message may be
|
|
294
|
+
* read before its `Conversation.id`, and the thread type decides both the
|
|
295
|
+
* self-sent fallback and the shape of the record.
|
|
296
|
+
*
|
|
297
|
+
* `participantJid` carries the group/broadcast author and stays absent in 1:1
|
|
298
|
+
* threads, where `senderJid` falls back to the thread JID - the same shape the
|
|
299
|
+
* live message path persists. A group author that stays unresolved leaves both
|
|
300
|
+
* fields empty: writing the group JID there would read downstream as if the
|
|
301
|
+
* group itself had sent the message.
|
|
302
|
+
*/
|
|
303
|
+
function persistHistoryMessage(deps, state, message, threadJid) {
|
|
304
|
+
state.messagesCount += 1;
|
|
305
|
+
const authorJid = (0, incoming_1.resolveWebMessageInfoAuthor)(message.authorInfo, deps.meJid, threadJid);
|
|
306
|
+
const isGroupOrBroadcast = (0, jid_1.isGroupJid)(threadJid) || (0, jid_1.isBroadcastJid)(threadJid);
|
|
307
|
+
pushWrite(state, deps.writeBehind.persistMessageAsync({
|
|
308
|
+
id: message.id,
|
|
309
|
+
threadJid,
|
|
310
|
+
senderJid: isGroupOrBroadcast ? authorJid : (authorJid ?? threadJid),
|
|
311
|
+
participantJid: isGroupOrBroadcast ? authorJid : undefined,
|
|
312
|
+
fromMe: message.fromMe,
|
|
313
|
+
timestampMs: message.timestampMs,
|
|
314
|
+
messageBytes: message.messageBytes
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
307
317
|
async function settleHistorySyncChunk(deps, state) {
|
|
308
318
|
for (const pushname of state.pushnames) {
|
|
309
319
|
if (!pushname.id) {
|
|
@@ -365,7 +375,11 @@ function readHistoryMessage(record) {
|
|
|
365
375
|
const timestampMs = (0, primitives_1.longToNumber)(webMsg.messageTimestamp) * 1000;
|
|
366
376
|
return {
|
|
367
377
|
id: webMsg.key.id,
|
|
368
|
-
|
|
378
|
+
authorInfo: {
|
|
379
|
+
key: { fromMe: webMsg.key.fromMe, participant: webMsg.key.participant },
|
|
380
|
+
participant: webMsg.participant,
|
|
381
|
+
originalSelfAuthorUserJidString: webMsg.originalSelfAuthorUserJidString
|
|
382
|
+
},
|
|
369
383
|
fromMe: webMsg.key.fromMe === true,
|
|
370
384
|
timestampMs: timestampMs || undefined,
|
|
371
385
|
messageBytes: _proto_1.proto.Message.encode(webMsg.message).finish()
|
|
@@ -275,7 +275,8 @@ class WaClientImpl extends EventEmitter {
|
|
|
275
275
|
emitEvent: this.emit.bind(this),
|
|
276
276
|
onPrivacyTokens: (conversations) => this.deps.trustedContactToken.hydrateFromHistorySync(conversations),
|
|
277
277
|
onNctSalt: (salt) => this.deps.trustedContactToken.hydrateNctSaltFromHistorySync(salt),
|
|
278
|
-
onProcessed: sendHistSyncReceipt
|
|
278
|
+
onProcessed: sendHistSyncReceipt,
|
|
279
|
+
meJid: this.deps.authClient.getCurrentCredentials()?.meJid
|
|
279
280
|
}, protocolMessage.historySyncNotification);
|
|
280
281
|
}
|
|
281
282
|
else if (sendHistSyncReceipt) {
|
|
@@ -22,7 +22,7 @@ const RETRY_SESSION_BASE_KEY_CACHE_MAX_ENTRIES = 8192;
|
|
|
22
22
|
// serialize on the prekey lock and hit the store); excess under flood is dropped.
|
|
23
23
|
const DECRYPT_FAILURE_QUEUE_MAX_SIZE = 1024;
|
|
24
24
|
const DECRYPT_FAILURE_QUEUE_MAX_CONCURRENCY = 8;
|
|
25
|
-
const PLACEHOLDER_RESEND_RETRY_THRESHOLD =
|
|
25
|
+
const PLACEHOLDER_RESEND_RETRY_THRESHOLD = 4;
|
|
26
26
|
const PLACEHOLDER_RESEND_BATCH_SIZE = 32;
|
|
27
27
|
const PLACEHOLDER_RESEND_DEBOUNCE_MS = 200;
|
|
28
28
|
const PLACEHOLDER_RESEND_FALLBACK_MAX_AGE_DAYS = 14;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { flushPendingWrites, openHistoryBlobStream } from '../persistence/history-blob.js';
|
|
2
2
|
import { streamGroupHistoryBundle } from '../../message/kinds/group-history.js';
|
|
3
|
+
import { resolveWebMessageInfoAuthor } from '../../message/primitives/incoming.js';
|
|
3
4
|
import { proto } from '../../proto.js';
|
|
4
5
|
import { isGroupJid, toUserJid } from '../../protocol/jid.js';
|
|
5
6
|
import { longToNumber, toError } from '../../util/primitives.js';
|
|
@@ -75,10 +76,12 @@ export async function processGroupHistoryBundle(deps, input) {
|
|
|
75
76
|
(oldestTimestampMs === undefined || timestampMs < oldestTimestampMs)) {
|
|
76
77
|
oldestTimestampMs = timestampMs;
|
|
77
78
|
}
|
|
79
|
+
const authorJid = resolveWebMessageInfoAuthor(webMsg, deps.meJid, input.groupJid);
|
|
78
80
|
const write = deps.writeBehind.persistMessageAsync({
|
|
79
81
|
id: webMsg.key.id,
|
|
80
82
|
threadJid: input.groupJid,
|
|
81
|
-
senderJid:
|
|
83
|
+
senderJid: authorJid,
|
|
84
|
+
participantJid: authorJid,
|
|
82
85
|
fromMe: webMsg.key.fromMe === true,
|
|
83
86
|
timestampMs: timestampMs || undefined,
|
|
84
87
|
messageBytes: proto.Message.encode(webMsg.message).finish()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { flushPendingWrites, openHistoryBlobStream } from '../persistence/history-blob.js';
|
|
2
|
+
import { resolveWebMessageInfoAuthor } from '../../message/primitives/incoming.js';
|
|
2
3
|
import { proto } from '../../proto.js';
|
|
3
|
-
import { isUserJid } from '../../protocol/jid.js';
|
|
4
|
+
import { isBroadcastJid, isGroupJid, isUserJid } from '../../protocol/jid.js';
|
|
4
5
|
import { normalizeEphemeralSettingSeconds } from '../../protocol/message.js';
|
|
5
6
|
import { normalizeUsername } from '../../protocol/username.js';
|
|
6
7
|
import { concatBytes, TEXT_DECODER } from '../../util/bytes.js';
|
|
@@ -153,15 +154,7 @@ async function consumeHistorySyncStream(deps, inflated, state) {
|
|
|
153
154
|
parked[parked.length] = message;
|
|
154
155
|
return undefined;
|
|
155
156
|
}
|
|
156
|
-
state
|
|
157
|
-
pushWrite(state, deps.writeBehind.persistMessageAsync({
|
|
158
|
-
id: message.id,
|
|
159
|
-
threadJid,
|
|
160
|
-
senderJid: message.senderJid,
|
|
161
|
-
fromMe: message.fromMe,
|
|
162
|
-
timestampMs: message.timestampMs,
|
|
163
|
-
messageBytes: message.messageBytes
|
|
164
|
-
}));
|
|
157
|
+
persistHistoryMessage(deps, state, message, threadJid);
|
|
165
158
|
return maybeFlush(state);
|
|
166
159
|
}
|
|
167
160
|
if (headerParts) {
|
|
@@ -287,18 +280,35 @@ async function closeConversation(deps, state, headerParts, threadJid, parked) {
|
|
|
287
280
|
}
|
|
288
281
|
}
|
|
289
282
|
for (const message of parked ?? []) {
|
|
290
|
-
state
|
|
291
|
-
pushWrite(state, deps.writeBehind.persistMessageAsync({
|
|
292
|
-
id: message.id,
|
|
293
|
-
threadJid: resolvedJid,
|
|
294
|
-
senderJid: message.senderJid,
|
|
295
|
-
fromMe: message.fromMe,
|
|
296
|
-
timestampMs: message.timestampMs,
|
|
297
|
-
messageBytes: message.messageBytes
|
|
298
|
-
}));
|
|
283
|
+
persistHistoryMessage(deps, state, message, resolvedJid);
|
|
299
284
|
}
|
|
300
285
|
await maybeFlush(state);
|
|
301
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Resolves the author here rather than at decode time because a message may be
|
|
289
|
+
* read before its `Conversation.id`, and the thread type decides both the
|
|
290
|
+
* self-sent fallback and the shape of the record.
|
|
291
|
+
*
|
|
292
|
+
* `participantJid` carries the group/broadcast author and stays absent in 1:1
|
|
293
|
+
* threads, where `senderJid` falls back to the thread JID - the same shape the
|
|
294
|
+
* live message path persists. A group author that stays unresolved leaves both
|
|
295
|
+
* fields empty: writing the group JID there would read downstream as if the
|
|
296
|
+
* group itself had sent the message.
|
|
297
|
+
*/
|
|
298
|
+
function persistHistoryMessage(deps, state, message, threadJid) {
|
|
299
|
+
state.messagesCount += 1;
|
|
300
|
+
const authorJid = resolveWebMessageInfoAuthor(message.authorInfo, deps.meJid, threadJid);
|
|
301
|
+
const isGroupOrBroadcast = isGroupJid(threadJid) || isBroadcastJid(threadJid);
|
|
302
|
+
pushWrite(state, deps.writeBehind.persistMessageAsync({
|
|
303
|
+
id: message.id,
|
|
304
|
+
threadJid,
|
|
305
|
+
senderJid: isGroupOrBroadcast ? authorJid : (authorJid ?? threadJid),
|
|
306
|
+
participantJid: isGroupOrBroadcast ? authorJid : undefined,
|
|
307
|
+
fromMe: message.fromMe,
|
|
308
|
+
timestampMs: message.timestampMs,
|
|
309
|
+
messageBytes: message.messageBytes
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
302
312
|
async function settleHistorySyncChunk(deps, state) {
|
|
303
313
|
for (const pushname of state.pushnames) {
|
|
304
314
|
if (!pushname.id) {
|
|
@@ -360,7 +370,11 @@ function readHistoryMessage(record) {
|
|
|
360
370
|
const timestampMs = longToNumber(webMsg.messageTimestamp) * 1000;
|
|
361
371
|
return {
|
|
362
372
|
id: webMsg.key.id,
|
|
363
|
-
|
|
373
|
+
authorInfo: {
|
|
374
|
+
key: { fromMe: webMsg.key.fromMe, participant: webMsg.key.participant },
|
|
375
|
+
participant: webMsg.participant,
|
|
376
|
+
originalSelfAuthorUserJidString: webMsg.originalSelfAuthorUserJidString
|
|
377
|
+
},
|
|
364
378
|
fromMe: webMsg.key.fromMe === true,
|
|
365
379
|
timestampMs: timestampMs || undefined,
|
|
366
380
|
messageBytes: proto.Message.encode(webMsg.message).finish()
|
|
@@ -12,6 +12,12 @@ export const MAC_KEY_END = 80;
|
|
|
12
12
|
export const HMAC_TRUNCATED_SIZE = 10;
|
|
13
13
|
export const SIDECAR_CHUNK_SIZE = 65536;
|
|
14
14
|
export const SIDECAR_HMAC_SIZE = 10;
|
|
15
|
+
/**
|
|
16
|
+
* Leading bytes buffered when deciding whether a payload supports progressive
|
|
17
|
+
* playback. The top-level boxes that settle it sit at the very front of the
|
|
18
|
+
* file, so this only has to survive a few small boxes before `moov`/`mdat`.
|
|
19
|
+
*/
|
|
20
|
+
export const CONTAINER_SCAN_MAX_BYTES = 1024;
|
|
15
21
|
export const MEDIA_UPLOAD_PATHS = Object.freeze({
|
|
16
22
|
image: '/mms/image',
|
|
17
23
|
video: '/mms/video',
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { CONTAINER_SCAN_MAX_BYTES } from './constants.js';
|
|
2
|
+
const BOX_HEADER_SIZE = 8;
|
|
3
|
+
const LARGE_BOX_HEADER_SIZE = 16;
|
|
4
|
+
const LARGE_BOX_SIZE_MARKER = 1;
|
|
5
|
+
const BOX_TYPE_FTYP = 0x66747970;
|
|
6
|
+
const BOX_TYPE_MOOV = 0x6d6f6f76;
|
|
7
|
+
const BOX_TYPE_MDAT = 0x6d646174;
|
|
8
|
+
function readU32(bytes, offset) {
|
|
9
|
+
return (((bytes[offset] << 24) |
|
|
10
|
+
(bytes[offset + 1] << 16) |
|
|
11
|
+
(bytes[offset + 2] << 8) |
|
|
12
|
+
bytes[offset + 3]) >>>
|
|
13
|
+
0);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Decides whether a media payload supports progressive playback, by walking
|
|
17
|
+
* the top-level ISO base media file format (mp4/mov/3gp/m4a) boxes over the
|
|
18
|
+
* leading bytes.
|
|
19
|
+
*
|
|
20
|
+
* An ISO-BMFF file is streamable only when its `moov` index box precedes the
|
|
21
|
+
* `mdat` payload box. With `mdat` first, a player has to reach the end of the
|
|
22
|
+
* download before it can start decoding, so advertising the file as seekable
|
|
23
|
+
* (by attaching a streaming sidecar) makes the recipient fail to open it
|
|
24
|
+
* instead of falling back to a plain download.
|
|
25
|
+
*
|
|
26
|
+
* Anything that is not ISO-BMFF - Ogg/Opus voice notes, for instance - is
|
|
27
|
+
* reported as streamable: those containers carry their index inline and have
|
|
28
|
+
* no equivalent failure mode.
|
|
29
|
+
*
|
|
30
|
+
* @param head - Leading bytes of the payload, starting at offset 0.
|
|
31
|
+
* @returns `true` when streamable, `false` when not, `null` when `head` is
|
|
32
|
+
* too short to tell and the caller should retry with more bytes.
|
|
33
|
+
*/
|
|
34
|
+
export function scanStreamable(head) {
|
|
35
|
+
if (head.byteLength < BOX_HEADER_SIZE) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (readU32(head, 4) !== BOX_TYPE_FTYP) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
let offset = 0;
|
|
42
|
+
while (offset + BOX_HEADER_SIZE <= head.byteLength) {
|
|
43
|
+
const type = readU32(head, offset + 4);
|
|
44
|
+
if (type === BOX_TYPE_MOOV)
|
|
45
|
+
return true;
|
|
46
|
+
if (type === BOX_TYPE_MDAT)
|
|
47
|
+
return false;
|
|
48
|
+
let size = readU32(head, offset);
|
|
49
|
+
let headerSize = BOX_HEADER_SIZE;
|
|
50
|
+
if (size === LARGE_BOX_SIZE_MARKER) {
|
|
51
|
+
if (offset + LARGE_BOX_HEADER_SIZE > head.byteLength)
|
|
52
|
+
return null;
|
|
53
|
+
if (readU32(head, offset + BOX_HEADER_SIZE) !== 0)
|
|
54
|
+
return true;
|
|
55
|
+
size = readU32(head, offset + BOX_HEADER_SIZE + 4);
|
|
56
|
+
headerSize = LARGE_BOX_HEADER_SIZE;
|
|
57
|
+
}
|
|
58
|
+
else if (size === 0) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (size < headerSize)
|
|
62
|
+
return true;
|
|
63
|
+
offset += size;
|
|
64
|
+
if (offset > CONTAINER_SCAN_MAX_BYTES)
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return offset > CONTAINER_SCAN_MAX_BYTES ? true : null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Incremental front-end for {@link scanStreamable}, for callers that see the
|
|
71
|
+
* payload as a sequence of chunks. Buffers at most
|
|
72
|
+
* {@link CONTAINER_SCAN_MAX_BYTES} leading bytes and settles on `true` once
|
|
73
|
+
* that budget is spent without a verdict.
|
|
74
|
+
*/
|
|
75
|
+
export class WaStreamableScanner {
|
|
76
|
+
constructor() {
|
|
77
|
+
this.head = new Uint8Array(CONTAINER_SCAN_MAX_BYTES);
|
|
78
|
+
this.headLength = 0;
|
|
79
|
+
this.settled = null;
|
|
80
|
+
}
|
|
81
|
+
/** Verdict so far; `null` until enough leading bytes have been seen. */
|
|
82
|
+
get verdict() {
|
|
83
|
+
return this.settled;
|
|
84
|
+
}
|
|
85
|
+
/** Feeds the next payload chunk. Ignored once the verdict has settled. */
|
|
86
|
+
push(chunk) {
|
|
87
|
+
if (this.settled !== null || chunk.byteLength === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const room = this.head.byteLength - this.headLength;
|
|
91
|
+
if (room > 0) {
|
|
92
|
+
const copied = Math.min(room, chunk.byteLength);
|
|
93
|
+
this.head.set(chunk.subarray(0, copied), this.headLength);
|
|
94
|
+
this.headLength += copied;
|
|
95
|
+
}
|
|
96
|
+
this.settled = scanStreamable(this.head.subarray(0, this.headLength));
|
|
97
|
+
if (this.settled === null && this.headLength === this.head.byteLength) {
|
|
98
|
+
this.settled = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Verdict to use once the payload has ended, defaulting to streamable. */
|
|
102
|
+
finish() {
|
|
103
|
+
return this.settled ?? true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -8,6 +8,7 @@ import { hkdf } from '../../crypto/core/hkdf.js';
|
|
|
8
8
|
import { aesCbcDecrypt, aesCbcEncryptWithTrailer, hmacSha256Sign, sha256 } from '../../crypto/core/primitives.js';
|
|
9
9
|
import { randomBytesAsync } from '../../crypto/core/random.js';
|
|
10
10
|
import { ENC_KEY_END, ENC_KEY_START, HMAC_TRUNCATED_SIZE, IV_SIZE, MAC_KEY_END, MAC_KEY_START, MEDIA_HKDF_SIZE, SIDECAR_CHUNK_SIZE, SIDECAR_HMAC_SIZE } from '../constants.js';
|
|
11
|
+
import { scanStreamable, WaStreamableScanner } from '../container.js';
|
|
11
12
|
import { getWaMediaHkdfInfo, WA_APP_STATE_KEY_TYPES } from '../../protocol/constants.js';
|
|
12
13
|
import { assertByteLength, toBytesView, toChunkBytes, uint8TimingSafeEqual } from '../../util/bytes.js';
|
|
13
14
|
import { toError } from '../../util/primitives.js';
|
|
@@ -105,7 +106,7 @@ export class WaMediaCrypto {
|
|
|
105
106
|
const signature = mac.subarray(0, HMAC_TRUNCATED_SIZE);
|
|
106
107
|
ciphertextHmac.set(signature, cipherLength);
|
|
107
108
|
let streamingSidecar;
|
|
108
|
-
if (options?.sidecar !== false) {
|
|
109
|
+
if (options?.sidecar !== false && scanStreamable(plaintext) !== false) {
|
|
109
110
|
const acc = new SidecarAccumulator(keys.macKey, plaintext.byteLength);
|
|
110
111
|
acc.push(keys.iv);
|
|
111
112
|
acc.push(ciphertext);
|
|
@@ -219,7 +220,8 @@ async function pumpEncryption(plaintext, output, keys, computeSidecar, firstFram
|
|
|
219
220
|
const plainHash = createHash('sha256');
|
|
220
221
|
const encHash = createHash('sha256');
|
|
221
222
|
const hmac = createHmac('sha256', keys.macKey);
|
|
222
|
-
|
|
223
|
+
let sidecar = computeSidecar ? new SidecarAccumulator(keys.macKey, expectedFileSize) : null;
|
|
224
|
+
const scanner = computeSidecar ? new WaStreamableScanner() : null;
|
|
223
225
|
const ffTarget = firstFrameLength !== undefined
|
|
224
226
|
? IV_SIZE + Math.ceil(firstFrameLength / AES_BLOCK_SIZE) * AES_BLOCK_SIZE
|
|
225
227
|
: 0;
|
|
@@ -249,6 +251,11 @@ async function pumpEncryption(plaintext, output, keys, computeSidecar, firstFram
|
|
|
249
251
|
continue;
|
|
250
252
|
plaintextLength += plainChunk.byteLength;
|
|
251
253
|
plainHash.update(plainChunk);
|
|
254
|
+
if (scanner !== null && scanner.verdict === null) {
|
|
255
|
+
scanner.push(plainChunk);
|
|
256
|
+
if (scanner.verdict === false)
|
|
257
|
+
sidecar = null;
|
|
258
|
+
}
|
|
252
259
|
await consumeCiphertext(cipher.update(plainChunk));
|
|
253
260
|
}
|
|
254
261
|
await consumeCiphertext(cipher.final());
|
|
@@ -125,6 +125,32 @@ function buildIncomingEventRawNode(node) {
|
|
|
125
125
|
content: children
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Resolves who authored a {@link proto.IWebMessageInfo}. The top-level
|
|
130
|
+
* `participant` wins over `key.participant` - history sync carries the group
|
|
131
|
+
* sender in the former and leaves the latter empty on a large share of records.
|
|
132
|
+
* A self-sent group/broadcast message with neither falls back to this account,
|
|
133
|
+
* preferring `originalSelfAuthorUserJidString` over `meJid` (the current account
|
|
134
|
+
* user JID) so a message authored under a previous identity keeps its author.
|
|
135
|
+
*
|
|
136
|
+
* The participant fields are read whatever the thread type is, matching how
|
|
137
|
+
* WhatsApp Web builds the author: only the self-sent fallback is gated on
|
|
138
|
+
* group/broadcast. A 1:1 record normally carries neither, so this returns
|
|
139
|
+
* `undefined` there and the caller supplies the thread JID instead.
|
|
140
|
+
*
|
|
141
|
+
* `threadJid` overrides `key.remoteJid` for carriers that know the thread out
|
|
142
|
+
* of band, such as a group history bundle whose records may omit it.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveWebMessageInfoAuthor(webMessageInfo, meJid, threadJid) {
|
|
145
|
+
const key = webMessageInfo.key ?? {};
|
|
146
|
+
const chatJid = threadJid ?? key.remoteJid ?? undefined;
|
|
147
|
+
const isGroupOrBroadcast = chatJid ? isGroupJid(chatJid) || isBroadcastJid(chatJid) : false;
|
|
148
|
+
const rawSelfAuthor = webMessageInfo.originalSelfAuthorUserJidString ?? meJid ?? undefined;
|
|
149
|
+
const selfAuthor = key.fromMe === true && isGroupOrBroadcast && rawSelfAuthor
|
|
150
|
+
? toUserJid(rawSelfAuthor)
|
|
151
|
+
: undefined;
|
|
152
|
+
return webMessageInfo.participant ?? key.participant ?? selfAuthor ?? undefined;
|
|
153
|
+
}
|
|
128
154
|
/**
|
|
129
155
|
* Rebuilds a `message` event from a recovered {@link proto.IWebMessageInfo}
|
|
130
156
|
* (placeholder resend). `meJid` is the current account user JID, used as the
|
|
@@ -137,9 +163,7 @@ export function buildRecoveredIncomingEvent(webMessageInfo, meJid) {
|
|
|
137
163
|
const fromMe = key.fromMe === true;
|
|
138
164
|
const isGroup = chatJid ? isGroupJid(chatJid) : false;
|
|
139
165
|
const isBroadcast = chatJid ? isBroadcastJid(chatJid) : false;
|
|
140
|
-
const
|
|
141
|
-
const selfAuthor = fromMe && (isGroup || isBroadcast) && rawSelfAuthor ? toUserJid(rawSelfAuthor) : undefined;
|
|
142
|
-
const participant = webMessageInfo.participant ?? key.participant ?? selfAuthor;
|
|
166
|
+
const participant = resolveWebMessageInfoAuthor(webMessageInfo, meJid);
|
|
143
167
|
const pushName = webMessageInfo.pushName ?? undefined;
|
|
144
168
|
const rawSender = fromMe ? undefined : isGroup || isBroadcast ? participant : chatJid;
|
|
145
169
|
const sender = rawSender ? parseJidFull(rawSender) : null;
|
|
@@ -9,6 +9,12 @@ export declare const MAC_KEY_END = 80;
|
|
|
9
9
|
export declare const HMAC_TRUNCATED_SIZE = 10;
|
|
10
10
|
export declare const SIDECAR_CHUNK_SIZE = 65536;
|
|
11
11
|
export declare const SIDECAR_HMAC_SIZE = 10;
|
|
12
|
+
/**
|
|
13
|
+
* Leading bytes buffered when deciding whether a payload supports progressive
|
|
14
|
+
* playback. The top-level boxes that settle it sit at the very front of the
|
|
15
|
+
* file, so this only has to survive a few small boxes before `moov`/`mdat`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const CONTAINER_SCAN_MAX_BYTES = 1024;
|
|
12
18
|
export type MediaUploadKind = 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'gif' | 'ptt' | 'ptv' | 'ads-image' | 'ads-video' | 'group-history' | 'md-app-state' | 'md-msg-hist' | 'music-artwork' | 'newsletter-music-artwork' | 'sticker-pack' | 'thumbnail-document' | 'thumbnail-image' | 'thumbnail-link' | 'thumbnail-sticker-pack' | 'thumbnail-video' | 'waffle-image' | 'waffle-video';
|
|
13
19
|
export declare const MEDIA_UPLOAD_PATHS: Readonly<Record<MediaUploadKind, string>>;
|
|
14
20
|
export type NewsletterMediaKind = 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'sticker-pack' | 'gif' | 'ptt' | 'ptv' | 'thumbnail-link';
|
package/dist/media/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NEWSLETTER_MEDIA_UPLOAD_PATHS = exports.PPS_UPLOAD_PATHS = exports.MEDIA_UPLOAD_PATHS = exports.SIDECAR_HMAC_SIZE = exports.SIDECAR_CHUNK_SIZE = exports.HMAC_TRUNCATED_SIZE = exports.MAC_KEY_END = exports.MAC_KEY_START = exports.ENC_KEY_END = exports.ENC_KEY_START = exports.IV_SIZE = exports.MEDIA_HKDF_SIZE = exports.MEDIA_CONN_CACHE_GRACE_MS = exports.DEFAULT_MEDIA_HOSTS = void 0;
|
|
3
|
+
exports.NEWSLETTER_MEDIA_UPLOAD_PATHS = exports.PPS_UPLOAD_PATHS = exports.MEDIA_UPLOAD_PATHS = exports.CONTAINER_SCAN_MAX_BYTES = exports.SIDECAR_HMAC_SIZE = exports.SIDECAR_CHUNK_SIZE = exports.HMAC_TRUNCATED_SIZE = exports.MAC_KEY_END = exports.MAC_KEY_START = exports.ENC_KEY_END = exports.ENC_KEY_START = exports.IV_SIZE = exports.MEDIA_HKDF_SIZE = exports.MEDIA_CONN_CACHE_GRACE_MS = exports.DEFAULT_MEDIA_HOSTS = void 0;
|
|
4
4
|
exports.DEFAULT_MEDIA_HOSTS = Object.freeze([
|
|
5
5
|
'mmg.whatsapp.net',
|
|
6
6
|
'mmg-fallback.whatsapp.net'
|
|
@@ -15,6 +15,12 @@ exports.MAC_KEY_END = 80;
|
|
|
15
15
|
exports.HMAC_TRUNCATED_SIZE = 10;
|
|
16
16
|
exports.SIDECAR_CHUNK_SIZE = 65536;
|
|
17
17
|
exports.SIDECAR_HMAC_SIZE = 10;
|
|
18
|
+
/**
|
|
19
|
+
* Leading bytes buffered when deciding whether a payload supports progressive
|
|
20
|
+
* playback. The top-level boxes that settle it sit at the very front of the
|
|
21
|
+
* file, so this only has to survive a few small boxes before `moov`/`mdat`.
|
|
22
|
+
*/
|
|
23
|
+
exports.CONTAINER_SCAN_MAX_BYTES = 1024;
|
|
18
24
|
exports.MEDIA_UPLOAD_PATHS = Object.freeze({
|
|
19
25
|
image: '/mms/image',
|
|
20
26
|
video: '/mms/video',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verdict of {@link scanStreamable}: `true` when the payload can be played
|
|
3
|
+
* back while it downloads, `false` when it cannot, `null` while the leading
|
|
4
|
+
* bytes seen so far are not enough to decide.
|
|
5
|
+
*/
|
|
6
|
+
export type WaStreamableVerdict = boolean | null;
|
|
7
|
+
/**
|
|
8
|
+
* Decides whether a media payload supports progressive playback, by walking
|
|
9
|
+
* the top-level ISO base media file format (mp4/mov/3gp/m4a) boxes over the
|
|
10
|
+
* leading bytes.
|
|
11
|
+
*
|
|
12
|
+
* An ISO-BMFF file is streamable only when its `moov` index box precedes the
|
|
13
|
+
* `mdat` payload box. With `mdat` first, a player has to reach the end of the
|
|
14
|
+
* download before it can start decoding, so advertising the file as seekable
|
|
15
|
+
* (by attaching a streaming sidecar) makes the recipient fail to open it
|
|
16
|
+
* instead of falling back to a plain download.
|
|
17
|
+
*
|
|
18
|
+
* Anything that is not ISO-BMFF - Ogg/Opus voice notes, for instance - is
|
|
19
|
+
* reported as streamable: those containers carry their index inline and have
|
|
20
|
+
* no equivalent failure mode.
|
|
21
|
+
*
|
|
22
|
+
* @param head - Leading bytes of the payload, starting at offset 0.
|
|
23
|
+
* @returns `true` when streamable, `false` when not, `null` when `head` is
|
|
24
|
+
* too short to tell and the caller should retry with more bytes.
|
|
25
|
+
*/
|
|
26
|
+
export declare function scanStreamable(head: Uint8Array): WaStreamableVerdict;
|
|
27
|
+
/**
|
|
28
|
+
* Incremental front-end for {@link scanStreamable}, for callers that see the
|
|
29
|
+
* payload as a sequence of chunks. Buffers at most
|
|
30
|
+
* {@link CONTAINER_SCAN_MAX_BYTES} leading bytes and settles on `true` once
|
|
31
|
+
* that budget is spent without a verdict.
|
|
32
|
+
*/
|
|
33
|
+
export declare class WaStreamableScanner {
|
|
34
|
+
private readonly head;
|
|
35
|
+
private headLength;
|
|
36
|
+
private settled;
|
|
37
|
+
/** Verdict so far; `null` until enough leading bytes have been seen. */
|
|
38
|
+
get verdict(): WaStreamableVerdict;
|
|
39
|
+
/** Feeds the next payload chunk. Ignored once the verdict has settled. */
|
|
40
|
+
push(chunk: Uint8Array): void;
|
|
41
|
+
/** Verdict to use once the payload has ended, defaulting to streamable. */
|
|
42
|
+
finish(): boolean;
|
|
43
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WaStreamableScanner = void 0;
|
|
4
|
+
exports.scanStreamable = scanStreamable;
|
|
5
|
+
const constants_1 = require("./constants");
|
|
6
|
+
const BOX_HEADER_SIZE = 8;
|
|
7
|
+
const LARGE_BOX_HEADER_SIZE = 16;
|
|
8
|
+
const LARGE_BOX_SIZE_MARKER = 1;
|
|
9
|
+
const BOX_TYPE_FTYP = 0x66747970;
|
|
10
|
+
const BOX_TYPE_MOOV = 0x6d6f6f76;
|
|
11
|
+
const BOX_TYPE_MDAT = 0x6d646174;
|
|
12
|
+
function readU32(bytes, offset) {
|
|
13
|
+
return (((bytes[offset] << 24) |
|
|
14
|
+
(bytes[offset + 1] << 16) |
|
|
15
|
+
(bytes[offset + 2] << 8) |
|
|
16
|
+
bytes[offset + 3]) >>>
|
|
17
|
+
0);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Decides whether a media payload supports progressive playback, by walking
|
|
21
|
+
* the top-level ISO base media file format (mp4/mov/3gp/m4a) boxes over the
|
|
22
|
+
* leading bytes.
|
|
23
|
+
*
|
|
24
|
+
* An ISO-BMFF file is streamable only when its `moov` index box precedes the
|
|
25
|
+
* `mdat` payload box. With `mdat` first, a player has to reach the end of the
|
|
26
|
+
* download before it can start decoding, so advertising the file as seekable
|
|
27
|
+
* (by attaching a streaming sidecar) makes the recipient fail to open it
|
|
28
|
+
* instead of falling back to a plain download.
|
|
29
|
+
*
|
|
30
|
+
* Anything that is not ISO-BMFF - Ogg/Opus voice notes, for instance - is
|
|
31
|
+
* reported as streamable: those containers carry their index inline and have
|
|
32
|
+
* no equivalent failure mode.
|
|
33
|
+
*
|
|
34
|
+
* @param head - Leading bytes of the payload, starting at offset 0.
|
|
35
|
+
* @returns `true` when streamable, `false` when not, `null` when `head` is
|
|
36
|
+
* too short to tell and the caller should retry with more bytes.
|
|
37
|
+
*/
|
|
38
|
+
function scanStreamable(head) {
|
|
39
|
+
if (head.byteLength < BOX_HEADER_SIZE) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (readU32(head, 4) !== BOX_TYPE_FTYP) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
let offset = 0;
|
|
46
|
+
while (offset + BOX_HEADER_SIZE <= head.byteLength) {
|
|
47
|
+
const type = readU32(head, offset + 4);
|
|
48
|
+
if (type === BOX_TYPE_MOOV)
|
|
49
|
+
return true;
|
|
50
|
+
if (type === BOX_TYPE_MDAT)
|
|
51
|
+
return false;
|
|
52
|
+
let size = readU32(head, offset);
|
|
53
|
+
let headerSize = BOX_HEADER_SIZE;
|
|
54
|
+
if (size === LARGE_BOX_SIZE_MARKER) {
|
|
55
|
+
if (offset + LARGE_BOX_HEADER_SIZE > head.byteLength)
|
|
56
|
+
return null;
|
|
57
|
+
if (readU32(head, offset + BOX_HEADER_SIZE) !== 0)
|
|
58
|
+
return true;
|
|
59
|
+
size = readU32(head, offset + BOX_HEADER_SIZE + 4);
|
|
60
|
+
headerSize = LARGE_BOX_HEADER_SIZE;
|
|
61
|
+
}
|
|
62
|
+
else if (size === 0) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (size < headerSize)
|
|
66
|
+
return true;
|
|
67
|
+
offset += size;
|
|
68
|
+
if (offset > constants_1.CONTAINER_SCAN_MAX_BYTES)
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return offset > constants_1.CONTAINER_SCAN_MAX_BYTES ? true : null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Incremental front-end for {@link scanStreamable}, for callers that see the
|
|
75
|
+
* payload as a sequence of chunks. Buffers at most
|
|
76
|
+
* {@link CONTAINER_SCAN_MAX_BYTES} leading bytes and settles on `true` once
|
|
77
|
+
* that budget is spent without a verdict.
|
|
78
|
+
*/
|
|
79
|
+
class WaStreamableScanner {
|
|
80
|
+
constructor() {
|
|
81
|
+
this.head = new Uint8Array(constants_1.CONTAINER_SCAN_MAX_BYTES);
|
|
82
|
+
this.headLength = 0;
|
|
83
|
+
this.settled = null;
|
|
84
|
+
}
|
|
85
|
+
/** Verdict so far; `null` until enough leading bytes have been seen. */
|
|
86
|
+
get verdict() {
|
|
87
|
+
return this.settled;
|
|
88
|
+
}
|
|
89
|
+
/** Feeds the next payload chunk. Ignored once the verdict has settled. */
|
|
90
|
+
push(chunk) {
|
|
91
|
+
if (this.settled !== null || chunk.byteLength === 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const room = this.head.byteLength - this.headLength;
|
|
95
|
+
if (room > 0) {
|
|
96
|
+
const copied = Math.min(room, chunk.byteLength);
|
|
97
|
+
this.head.set(chunk.subarray(0, copied), this.headLength);
|
|
98
|
+
this.headLength += copied;
|
|
99
|
+
}
|
|
100
|
+
this.settled = scanStreamable(this.head.subarray(0, this.headLength));
|
|
101
|
+
if (this.settled === null && this.headLength === this.head.byteLength) {
|
|
102
|
+
this.settled = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Verdict to use once the payload has ended, defaulting to streamable. */
|
|
106
|
+
finish() {
|
|
107
|
+
return this.settled ?? true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.WaStreamableScanner = WaStreamableScanner;
|
|
@@ -11,6 +11,7 @@ const hkdf_1 = require("../../crypto/core/hkdf");
|
|
|
11
11
|
const primitives_1 = require("../../crypto/core/primitives");
|
|
12
12
|
const random_1 = require("../../crypto/core/random");
|
|
13
13
|
const constants_1 = require("../constants");
|
|
14
|
+
const container_1 = require("../container");
|
|
14
15
|
const constants_2 = require("../../protocol/constants");
|
|
15
16
|
const bytes_1 = require("../../util/bytes");
|
|
16
17
|
const primitives_2 = require("../../util/primitives");
|
|
@@ -108,7 +109,7 @@ class WaMediaCrypto {
|
|
|
108
109
|
const signature = mac.subarray(0, constants_1.HMAC_TRUNCATED_SIZE);
|
|
109
110
|
ciphertextHmac.set(signature, cipherLength);
|
|
110
111
|
let streamingSidecar;
|
|
111
|
-
if (options?.sidecar !== false) {
|
|
112
|
+
if (options?.sidecar !== false && (0, container_1.scanStreamable)(plaintext) !== false) {
|
|
112
113
|
const acc = new SidecarAccumulator(keys.macKey, plaintext.byteLength);
|
|
113
114
|
acc.push(keys.iv);
|
|
114
115
|
acc.push(ciphertext);
|
|
@@ -223,7 +224,8 @@ async function pumpEncryption(plaintext, output, keys, computeSidecar, firstFram
|
|
|
223
224
|
const plainHash = (0, node_crypto_1.createHash)('sha256');
|
|
224
225
|
const encHash = (0, node_crypto_1.createHash)('sha256');
|
|
225
226
|
const hmac = (0, node_crypto_1.createHmac)('sha256', keys.macKey);
|
|
226
|
-
|
|
227
|
+
let sidecar = computeSidecar ? new SidecarAccumulator(keys.macKey, expectedFileSize) : null;
|
|
228
|
+
const scanner = computeSidecar ? new container_1.WaStreamableScanner() : null;
|
|
227
229
|
const ffTarget = firstFrameLength !== undefined
|
|
228
230
|
? constants_1.IV_SIZE + Math.ceil(firstFrameLength / AES_BLOCK_SIZE) * AES_BLOCK_SIZE
|
|
229
231
|
: 0;
|
|
@@ -253,6 +255,11 @@ async function pumpEncryption(plaintext, output, keys, computeSidecar, firstFram
|
|
|
253
255
|
continue;
|
|
254
256
|
plaintextLength += plainChunk.byteLength;
|
|
255
257
|
plainHash.update(plainChunk);
|
|
258
|
+
if (scanner !== null && scanner.verdict === null) {
|
|
259
|
+
scanner.push(plainChunk);
|
|
260
|
+
if (scanner.verdict === false)
|
|
261
|
+
sidecar = null;
|
|
262
|
+
}
|
|
256
263
|
await consumeCiphertext(cipher.update(plainChunk));
|
|
257
264
|
}
|
|
258
265
|
await consumeCiphertext(cipher.final());
|
|
@@ -24,6 +24,23 @@ interface WaIncomingMessageAckHandlerOptions {
|
|
|
24
24
|
readonly emitUnhandledStanza?: (event: WaIncomingUnhandledStanzaEvent) => void;
|
|
25
25
|
readonly emitDecryptedPayload?: (build: () => WaIncomingDecryptedPayloadEvent) => void;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolves who authored a {@link proto.IWebMessageInfo}. The top-level
|
|
29
|
+
* `participant` wins over `key.participant` - history sync carries the group
|
|
30
|
+
* sender in the former and leaves the latter empty on a large share of records.
|
|
31
|
+
* A self-sent group/broadcast message with neither falls back to this account,
|
|
32
|
+
* preferring `originalSelfAuthorUserJidString` over `meJid` (the current account
|
|
33
|
+
* user JID) so a message authored under a previous identity keeps its author.
|
|
34
|
+
*
|
|
35
|
+
* The participant fields are read whatever the thread type is, matching how
|
|
36
|
+
* WhatsApp Web builds the author: only the self-sent fallback is gated on
|
|
37
|
+
* group/broadcast. A 1:1 record normally carries neither, so this returns
|
|
38
|
+
* `undefined` there and the caller supplies the thread JID instead.
|
|
39
|
+
*
|
|
40
|
+
* `threadJid` overrides `key.remoteJid` for carriers that know the thread out
|
|
41
|
+
* of band, such as a group history bundle whose records may omit it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveWebMessageInfoAuthor(webMessageInfo: proto.IWebMessageInfo, meJid?: string | null, threadJid?: string): string | undefined;
|
|
27
44
|
/**
|
|
28
45
|
* Rebuilds a `message` event from a recovered {@link proto.IWebMessageInfo}
|
|
29
46
|
* (placeholder resend). `meJid` is the current account user JID, used as the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveWebMessageInfoAuthor = resolveWebMessageInfoAuthor;
|
|
3
4
|
exports.buildRecoveredIncomingEvent = buildRecoveredIncomingEvent;
|
|
4
5
|
exports.handleIncomingMessageAck = handleIncomingMessageAck;
|
|
5
6
|
const context_info_1 = require("../context-info");
|
|
@@ -129,6 +130,32 @@ function buildIncomingEventRawNode(node) {
|
|
|
129
130
|
content: children
|
|
130
131
|
};
|
|
131
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolves who authored a {@link proto.IWebMessageInfo}. The top-level
|
|
135
|
+
* `participant` wins over `key.participant` - history sync carries the group
|
|
136
|
+
* sender in the former and leaves the latter empty on a large share of records.
|
|
137
|
+
* A self-sent group/broadcast message with neither falls back to this account,
|
|
138
|
+
* preferring `originalSelfAuthorUserJidString` over `meJid` (the current account
|
|
139
|
+
* user JID) so a message authored under a previous identity keeps its author.
|
|
140
|
+
*
|
|
141
|
+
* The participant fields are read whatever the thread type is, matching how
|
|
142
|
+
* WhatsApp Web builds the author: only the self-sent fallback is gated on
|
|
143
|
+
* group/broadcast. A 1:1 record normally carries neither, so this returns
|
|
144
|
+
* `undefined` there and the caller supplies the thread JID instead.
|
|
145
|
+
*
|
|
146
|
+
* `threadJid` overrides `key.remoteJid` for carriers that know the thread out
|
|
147
|
+
* of band, such as a group history bundle whose records may omit it.
|
|
148
|
+
*/
|
|
149
|
+
function resolveWebMessageInfoAuthor(webMessageInfo, meJid, threadJid) {
|
|
150
|
+
const key = webMessageInfo.key ?? {};
|
|
151
|
+
const chatJid = threadJid ?? key.remoteJid ?? undefined;
|
|
152
|
+
const isGroupOrBroadcast = chatJid ? (0, jid_1.isGroupJid)(chatJid) || (0, jid_1.isBroadcastJid)(chatJid) : false;
|
|
153
|
+
const rawSelfAuthor = webMessageInfo.originalSelfAuthorUserJidString ?? meJid ?? undefined;
|
|
154
|
+
const selfAuthor = key.fromMe === true && isGroupOrBroadcast && rawSelfAuthor
|
|
155
|
+
? (0, jid_1.toUserJid)(rawSelfAuthor)
|
|
156
|
+
: undefined;
|
|
157
|
+
return webMessageInfo.participant ?? key.participant ?? selfAuthor ?? undefined;
|
|
158
|
+
}
|
|
132
159
|
/**
|
|
133
160
|
* Rebuilds a `message` event from a recovered {@link proto.IWebMessageInfo}
|
|
134
161
|
* (placeholder resend). `meJid` is the current account user JID, used as the
|
|
@@ -141,9 +168,7 @@ function buildRecoveredIncomingEvent(webMessageInfo, meJid) {
|
|
|
141
168
|
const fromMe = key.fromMe === true;
|
|
142
169
|
const isGroup = chatJid ? (0, jid_1.isGroupJid)(chatJid) : false;
|
|
143
170
|
const isBroadcast = chatJid ? (0, jid_1.isBroadcastJid)(chatJid) : false;
|
|
144
|
-
const
|
|
145
|
-
const selfAuthor = fromMe && (isGroup || isBroadcast) && rawSelfAuthor ? (0, jid_1.toUserJid)(rawSelfAuthor) : undefined;
|
|
146
|
-
const participant = webMessageInfo.participant ?? key.participant ?? selfAuthor;
|
|
171
|
+
const participant = resolveWebMessageInfoAuthor(webMessageInfo, meJid);
|
|
147
172
|
const pushName = webMessageInfo.pushName ?? undefined;
|
|
148
173
|
const rawSender = fromMe ? undefined : isGroup || isBroadcast ? participant : chatJid;
|
|
149
174
|
const sender = rawSender ? (0, jid_1.parseJidFull)(rawSender) : null;
|