violetics 7.0.1-alpha → 7.0.2-alpha
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/LICENSE +3 -2
- package/README.md +1001 -232
- package/WAProto/index.js +75379 -142631
- package/engine-requirements.js +11 -8
- package/lib/Defaults/index.js +132 -146
- package/lib/Signal/Group/ciphertext-message.js +2 -6
- package/lib/Signal/Group/group-session-builder.js +7 -42
- package/lib/Signal/Group/group_cipher.js +37 -52
- package/lib/Signal/Group/index.js +11 -57
- package/lib/Signal/Group/keyhelper.js +7 -45
- package/lib/Signal/Group/sender-chain-key.js +7 -16
- package/lib/Signal/Group/sender-key-distribution-message.js +8 -12
- package/lib/Signal/Group/sender-key-message.js +9 -13
- package/lib/Signal/Group/sender-key-name.js +2 -6
- package/lib/Signal/Group/sender-key-record.js +9 -22
- package/lib/Signal/Group/sender-key-state.js +27 -43
- package/lib/Signal/Group/sender-message-key.js +4 -8
- package/lib/Signal/libsignal.js +319 -94
- package/lib/Signal/lid-mapping.js +224 -139
- package/lib/Socket/Client/index.js +2 -19
- package/lib/Socket/Client/types.js +10 -0
- package/lib/Socket/Client/websocket.js +53 -0
- package/lib/Socket/business.js +162 -44
- package/lib/Socket/chats.js +477 -418
- package/lib/Socket/communities.js +430 -0
- package/lib/Socket/groups.js +110 -99
- package/lib/Socket/index.js +10 -10
- package/lib/Socket/messages-recv.js +884 -561
- package/lib/Socket/messages-send.js +859 -428
- package/lib/Socket/mex.js +41 -0
- package/lib/Socket/newsletter.js +195 -390
- package/lib/Socket/socket.js +465 -315
- package/lib/Store/index.js +3 -10
- package/lib/Store/make-in-memory-store.js +73 -79
- package/lib/Store/make-ordered-dictionary.js +4 -7
- package/lib/Store/object-repository.js +2 -6
- package/lib/Types/Auth.js +1 -2
- package/lib/Types/Bussines.js +1 -0
- package/lib/Types/Call.js +1 -2
- package/lib/Types/Chat.js +7 -4
- package/lib/Types/Contact.js +1 -2
- package/lib/Types/Events.js +1 -2
- package/lib/Types/GroupMetadata.js +1 -2
- package/lib/Types/Label.js +2 -5
- package/lib/Types/LabelAssociation.js +2 -5
- package/lib/Types/Message.js +17 -9
- package/lib/Types/Newsletter.js +33 -38
- package/lib/Types/Product.js +1 -2
- package/lib/Types/Signal.js +1 -2
- package/lib/Types/Socket.js +2 -2
- package/lib/Types/State.js +12 -2
- package/lib/Types/USync.js +1 -2
- package/lib/Types/index.js +14 -31
- package/lib/Utils/auth-utils.js +228 -152
- package/lib/Utils/browser-utils.js +28 -0
- package/lib/Utils/business.js +66 -70
- package/lib/Utils/chat-utils.js +331 -249
- package/lib/Utils/crypto.js +57 -91
- package/lib/Utils/decode-wa-message.js +168 -84
- package/lib/Utils/event-buffer.js +138 -80
- package/lib/Utils/generics.js +180 -297
- package/lib/Utils/history.js +83 -49
- package/lib/Utils/identity-change-handler.js +48 -0
- package/lib/Utils/index.js +19 -33
- package/lib/Utils/link-preview.js +14 -23
- package/lib/Utils/logger.js +2 -7
- package/lib/Utils/lt-hash.js +2 -46
- package/lib/Utils/make-mutex.js +24 -47
- package/lib/Utils/message-retry-manager.js +224 -0
- package/lib/Utils/messages-media.js +501 -496
- package/lib/Utils/messages.js +1428 -362
- package/lib/Utils/noise-handler.js +145 -100
- package/lib/Utils/pre-key-manager.js +105 -0
- package/lib/Utils/process-message.js +356 -150
- package/lib/Utils/reporting-utils.js +257 -0
- package/lib/Utils/signal.js +78 -73
- package/lib/Utils/sync-action-utils.js +47 -0
- package/lib/Utils/tc-token-utils.js +17 -0
- package/lib/Utils/use-multi-file-auth-state.js +35 -45
- package/lib/Utils/validate-connection.js +91 -107
- package/lib/WABinary/constants.js +1300 -1304
- package/lib/WABinary/decode.js +26 -48
- package/lib/WABinary/encode.js +109 -155
- package/lib/WABinary/generic-utils.js +161 -149
- package/lib/WABinary/index.js +5 -21
- package/lib/WABinary/jid-utils.js +73 -40
- package/lib/WABinary/types.js +1 -2
- package/lib/WAM/BinaryInfo.js +2 -6
- package/lib/WAM/constants.js +19070 -11568
- package/lib/WAM/encode.js +17 -23
- package/lib/WAM/index.js +3 -19
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +8 -12
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +11 -15
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +9 -13
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +9 -14
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +20 -23
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +13 -9
- package/lib/WAUSync/Protocols/index.js +4 -20
- package/lib/WAUSync/USyncQuery.js +40 -36
- package/lib/WAUSync/USyncUser.js +2 -6
- package/lib/WAUSync/index.js +3 -19
- package/lib/index.js +11 -44
- package/package.json +74 -107
- package/lib/Defaults/baileys-version.json +0 -3
- package/lib/Defaults/phonenumber-mcc.json +0 -223
- package/lib/Signal/Group/queue-job.js +0 -57
- package/lib/Socket/Client/abstract-socket-client.js +0 -13
- package/lib/Socket/Client/mobile-socket-client.js +0 -65
- package/lib/Socket/Client/web-socket-client.js +0 -118
- package/lib/Socket/groupStatus.js +0 -637
- package/lib/Socket/registration.js +0 -166
- package/lib/Socket/usync.js +0 -70
- package/lib/Store/make-cache-manager-store.js +0 -83
- package/lib/Utils/baileys-event-stream.js +0 -63
|
@@ -1,333 +1,295 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
exports.getStatusCodeForMediaRetry = exports.decryptMediaRetryData = exports.decodeMediaRetryNode = exports.encryptMediaRetryRequest = exports.getWAUploadToServer = exports.extensionForMediaMessage = exports.downloadEncryptedContent = exports.downloadContentFromMessage = exports.getUrlFromDirectPath = exports.encryptedStream = exports.prepareStream = exports.getHttpStream = exports.generateThumbnail = exports.getStream = exports.toBuffer = exports.toReadable = exports.getAudioWaveform = exports.getAudioDuration = exports.mediaMessageSHA256B64 = exports.generateProfilePicture = exports.encodeBase64EncodedStringForUpload = exports.extractImageThumb = exports.getMediaKeys = exports.hkdfInfoKey = void 0;
|
|
27
|
-
const boom_1 = require("@hapi/boom");
|
|
28
|
-
const child_process_1 = require("child_process");
|
|
29
|
-
const Crypto = __importStar(require("crypto"));
|
|
30
|
-
const events_1 = require("events");
|
|
31
|
-
const fs_1 = require("fs");
|
|
32
|
-
const os_1 = require("os");
|
|
33
|
-
const path_1 = require("path");
|
|
34
|
-
const stream_1 = require("stream");
|
|
35
|
-
const WAProto_1 = require("../../WAProto");
|
|
36
|
-
const Defaults_1 = require("../Defaults");
|
|
37
|
-
const WABinary_1 = require("../WABinary");
|
|
38
|
-
const crypto_1 = require("./crypto");
|
|
39
|
-
const generics_1 = require("./generics");
|
|
40
|
-
const getTmpFilesDirectory = () => (0, os_1.tmpdir)();
|
|
41
|
-
const getImageProcessingLibrary = async () => {
|
|
42
|
-
const [_jimp, sharp] = await Promise.all([
|
|
43
|
-
(async () => {
|
|
44
|
-
const jimp = await (import('jimp')
|
|
45
|
-
.catch(() => { }));
|
|
46
|
-
return jimp;
|
|
47
|
-
})(),
|
|
48
|
-
(async () => {
|
|
49
|
-
const sharp = await (import('sharp')
|
|
50
|
-
.catch(() => { }));
|
|
51
|
-
return sharp;
|
|
52
|
-
})()
|
|
1
|
+
import { Boom } from '@hapi/boom';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import * as Crypto from 'crypto';
|
|
4
|
+
import { once } from 'events';
|
|
5
|
+
import { createReadStream, createWriteStream, promises as fs, WriteStream } from 'fs';
|
|
6
|
+
import { tmpdir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { Readable, Transform } from 'stream';
|
|
9
|
+
import { URL } from 'url';
|
|
10
|
+
import { proto } from '../../WAProto/index.js';
|
|
11
|
+
import { DEFAULT_ORIGIN, MEDIA_HKDF_KEY_MAPPING, MEDIA_PATH_MAP, NEWSLETTER_MEDIA_PATH_MAP } from '../Defaults/index.js';
|
|
12
|
+
import { getBinaryNodeChild, getBinaryNodeChildBuffer, jidNormalizedUser } from '../WABinary/index.js';
|
|
13
|
+
import { aesDecryptGCM, aesEncryptGCM, hkdf } from './crypto.js';
|
|
14
|
+
import { generateMessageIDV2 } from './generics.js';
|
|
15
|
+
const getTmpFilesDirectory = () => tmpdir();
|
|
16
|
+
let imageProcessingLibrary;
|
|
17
|
+
export const getImageProcessingLibrary = async () => {
|
|
18
|
+
if (imageProcessingLibrary) {
|
|
19
|
+
return imageProcessingLibrary;
|
|
20
|
+
}
|
|
21
|
+
//@ts-ignore
|
|
22
|
+
const [sharp, image, jimp] = await Promise.all([
|
|
23
|
+
import('sharp').catch(() => { }),
|
|
24
|
+
import('@napi-rs/image').catch(() => { }),
|
|
25
|
+
import('jimp').catch(() => { })
|
|
53
26
|
]);
|
|
54
27
|
if (sharp) {
|
|
55
|
-
|
|
28
|
+
imageProcessingLibrary = { sharp };
|
|
29
|
+
}
|
|
30
|
+
else if (image) {
|
|
31
|
+
imageProcessingLibrary = { image };
|
|
56
32
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return { jimp };
|
|
33
|
+
else if (jimp) {
|
|
34
|
+
imageProcessingLibrary = { jimp };
|
|
60
35
|
}
|
|
61
|
-
|
|
36
|
+
else {
|
|
37
|
+
throw new Boom('No image processing library available');
|
|
38
|
+
}
|
|
39
|
+
return imageProcessingLibrary;
|
|
62
40
|
};
|
|
63
|
-
const hkdfInfoKey = (type) => {
|
|
64
|
-
const hkdfInfo =
|
|
41
|
+
export const hkdfInfoKey = (type) => {
|
|
42
|
+
const hkdfInfo = MEDIA_HKDF_KEY_MAPPING[type];
|
|
65
43
|
return `WhatsApp ${hkdfInfo} Keys`;
|
|
66
44
|
};
|
|
67
|
-
|
|
45
|
+
export const getRawMediaUploadData = async (media, mediaType, logger) => {
|
|
46
|
+
const { stream } = await getStream(media);
|
|
47
|
+
logger?.debug('got stream for raw upload');
|
|
48
|
+
const hasher = Crypto.createHash('sha256');
|
|
49
|
+
const filePath = join(tmpdir(), mediaType + generateMessageIDV2());
|
|
50
|
+
const fileWriteStream = createWriteStream(filePath);
|
|
51
|
+
let fileLength = 0;
|
|
52
|
+
try {
|
|
53
|
+
for await (const data of stream) {
|
|
54
|
+
fileLength += data.length;
|
|
55
|
+
hasher.update(data);
|
|
56
|
+
if (!fileWriteStream.write(data)) {
|
|
57
|
+
await once(fileWriteStream, 'drain');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
fileWriteStream.end();
|
|
61
|
+
await once(fileWriteStream, 'finish');
|
|
62
|
+
stream.destroy();
|
|
63
|
+
const fileSha256 = hasher.digest();
|
|
64
|
+
logger?.debug('hashed data for raw upload');
|
|
65
|
+
return {
|
|
66
|
+
filePath: filePath,
|
|
67
|
+
fileSha256,
|
|
68
|
+
fileLength
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
fileWriteStream.destroy();
|
|
73
|
+
stream.destroy();
|
|
74
|
+
try {
|
|
75
|
+
await fs.unlink(filePath);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
//
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
68
83
|
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
|
69
|
-
function getMediaKeys(buffer, mediaType) {
|
|
84
|
+
export async function getMediaKeys(buffer, mediaType) {
|
|
70
85
|
if (!buffer) {
|
|
71
|
-
throw new
|
|
86
|
+
throw new Boom('Cannot derive from empty media key');
|
|
72
87
|
}
|
|
73
88
|
if (typeof buffer === 'string') {
|
|
74
89
|
buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64');
|
|
75
90
|
}
|
|
76
91
|
// expand using HKDF to 112 bytes, also pass in the relevant app info
|
|
77
|
-
const expandedMediaKey =
|
|
92
|
+
const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) });
|
|
78
93
|
return {
|
|
79
94
|
iv: expandedMediaKey.slice(0, 16),
|
|
80
95
|
cipherKey: expandedMediaKey.slice(16, 48),
|
|
81
|
-
macKey: expandedMediaKey.slice(48, 80)
|
|
96
|
+
macKey: expandedMediaKey.slice(48, 80)
|
|
82
97
|
};
|
|
83
98
|
}
|
|
84
|
-
exports.getMediaKeys = getMediaKeys;
|
|
85
99
|
/** Extracts video thumb using FFMPEG */
|
|
86
|
-
const extractVideoThumb = async (path,
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
export const extractVideoThumb = async (path, time, size) => {
|
|
101
|
+
const ff = spawn('ffmpeg', [
|
|
102
|
+
'-loglevel', 'error',
|
|
103
|
+
'-ss', String(time),
|
|
104
|
+
'-i', path,
|
|
105
|
+
'-map_metadata', '-1',
|
|
106
|
+
'-vf', `scale=${size.width}:-1`,
|
|
107
|
+
'-frames:v', '1',
|
|
108
|
+
'-c:v', 'mjpeg',
|
|
109
|
+
'-f', 'image2pipe',
|
|
110
|
+
'pipe:1'
|
|
111
|
+
], {
|
|
112
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
95
113
|
});
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
const stdoutChunks = [];
|
|
115
|
+
const stderrChunks = [];
|
|
116
|
+
ff.stdout.on('data', chunk => stdoutChunks.push(chunk));
|
|
117
|
+
ff.stderr.on('data', chunk => stderrChunks.push(chunk));
|
|
118
|
+
const [code] = await once(ff, 'close');
|
|
119
|
+
if (code !== 0) {
|
|
120
|
+
throw new Boom(
|
|
121
|
+
`FFmpeg failed (code ${code}):\n` +
|
|
122
|
+
Buffer.concat(stderrChunks).toString('utf8')
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return Buffer.concat(stdoutChunks);
|
|
126
|
+
};
|
|
127
|
+
export const extractImageThumb = async (bufferOrFilePath, width = 32) => {
|
|
128
|
+
// TODO: Move entirely to sharp, removing jimp as it supports readable streams
|
|
129
|
+
// This will have positive speed and performance impacts as well as minimizing RAM usage.
|
|
130
|
+
if (bufferOrFilePath instanceof Readable) {
|
|
131
|
+
bufferOrFilePath = await toBuffer(bufferOrFilePath);
|
|
101
132
|
}
|
|
102
133
|
const lib = await getImageProcessingLibrary();
|
|
103
|
-
if ('sharp' in lib &&
|
|
134
|
+
if ('sharp' in lib && lib.sharp?.default) {
|
|
104
135
|
const img = lib.sharp.default(bufferOrFilePath);
|
|
105
136
|
const dimensions = await img.metadata();
|
|
106
|
-
const buffer = await img
|
|
107
|
-
.resize(width)
|
|
108
|
-
.jpeg({ quality: 50 })
|
|
109
|
-
.toBuffer();
|
|
137
|
+
const buffer = await img.resize(width).jpeg({ quality: 50 }).toBuffer();
|
|
110
138
|
return {
|
|
111
139
|
buffer,
|
|
112
140
|
original: {
|
|
113
141
|
width: dimensions.width,
|
|
114
|
-
height: dimensions.height
|
|
115
|
-
}
|
|
142
|
+
height: dimensions.height
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
else if ('image' in lib && lib.image?.Transformer) {
|
|
147
|
+
if (!Buffer.isBuffer(bufferOrFilePath)) {
|
|
148
|
+
bufferOrFilePath = await fs.readFile(bufferOrFilePath);
|
|
149
|
+
}
|
|
150
|
+
const img = new lib.image.Transformer(bufferOrFilePath);
|
|
151
|
+
const dimensions = await img.metadata();
|
|
152
|
+
const buffer = await img.resize(width, undefined, 0).jpeg(50);
|
|
153
|
+
return {
|
|
154
|
+
buffer,
|
|
155
|
+
original: {
|
|
156
|
+
width: dimensions.width,
|
|
157
|
+
height: dimensions.height
|
|
158
|
+
}
|
|
116
159
|
};
|
|
117
160
|
}
|
|
118
|
-
else if ('jimp' in lib &&
|
|
119
|
-
const
|
|
120
|
-
const jimp = await read(bufferOrFilePath);
|
|
161
|
+
else if ('jimp' in lib && lib.jimp?.Jimp) {
|
|
162
|
+
const jimp = await lib.jimp.Jimp.read(bufferOrFilePath);
|
|
121
163
|
const dimensions = {
|
|
122
|
-
width: jimp.
|
|
123
|
-
height: jimp.
|
|
164
|
+
width: jimp.width,
|
|
165
|
+
height: jimp.height
|
|
124
166
|
};
|
|
125
167
|
const buffer = await jimp
|
|
126
|
-
.
|
|
127
|
-
.
|
|
128
|
-
.getBufferAsync(MIME_JPEG);
|
|
168
|
+
.resize({ w: width, mode: lib.jimp.ResizeStrategy.BILINEAR })
|
|
169
|
+
.getBuffer('image/jpeg', { quality: 50 });
|
|
129
170
|
return {
|
|
130
171
|
buffer,
|
|
131
172
|
original: dimensions
|
|
132
173
|
};
|
|
133
174
|
}
|
|
134
175
|
else {
|
|
135
|
-
throw new
|
|
176
|
+
throw new Boom('No image processing library available');
|
|
136
177
|
}
|
|
137
178
|
};
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
.replace(/\=+$/, '')));
|
|
143
|
-
exports.encodeBase64EncodedStringForUpload = encodeBase64EncodedStringForUpload;
|
|
144
|
-
const generateProfilePicture = async (mediaUpload) => {
|
|
145
|
-
var _a, _b;
|
|
146
|
-
let bufferOrFilePath;
|
|
179
|
+
export const encodeBase64EncodedStringForUpload = (b64) => encodeURIComponent(b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''));
|
|
180
|
+
export const generateProfilePicture = async (mediaUpload, dimensions) => {
|
|
181
|
+
let buffer;
|
|
182
|
+
const { width: w = 720, height: h = 720 } = dimensions || {};
|
|
147
183
|
if (Buffer.isBuffer(mediaUpload)) {
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
else if ('url' in mediaUpload) {
|
|
151
|
-
bufferOrFilePath = mediaUpload.url.toString();
|
|
184
|
+
buffer = mediaUpload;
|
|
152
185
|
}
|
|
153
186
|
else {
|
|
154
|
-
|
|
187
|
+
// Use getStream to handle all WAMediaUpload types (Buffer, Stream, URL)
|
|
188
|
+
const { stream } = await getStream(mediaUpload);
|
|
189
|
+
// Convert the resulting stream to a buffer
|
|
190
|
+
buffer = await toBuffer(stream);
|
|
155
191
|
}
|
|
156
192
|
const lib = await getImageProcessingLibrary();
|
|
157
193
|
let img;
|
|
158
|
-
if ('sharp' in lib &&
|
|
159
|
-
img = lib.sharp
|
|
160
|
-
.
|
|
194
|
+
if ('sharp' in lib && lib.sharp?.default) {
|
|
195
|
+
img = lib.sharp
|
|
196
|
+
.default(buffer)
|
|
197
|
+
.resize(w, h)
|
|
161
198
|
.jpeg({
|
|
162
|
-
|
|
163
|
-
|
|
199
|
+
quality: 80
|
|
200
|
+
})
|
|
164
201
|
.toBuffer();
|
|
165
202
|
}
|
|
166
|
-
else if ('
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
203
|
+
else if ('image' in lib && lib.image?.Transformer) {
|
|
204
|
+
img = new lib.image
|
|
205
|
+
.Transformer(buffer)
|
|
206
|
+
.resize(w, h, 0)
|
|
207
|
+
.jpeg(80);
|
|
208
|
+
}
|
|
209
|
+
else if ('jimp' in lib && lib.jimp?.Jimp) {
|
|
210
|
+
const jimp = await lib.jimp.Jimp.read(buffer);
|
|
211
|
+
const min = Math.min(jimp.width, jimp.height);
|
|
212
|
+
const cropped = jimp.crop({ x: 0, y: 0, w: min, h: min });
|
|
213
|
+
img = cropped.resize({ w, h, mode: lib.jimp.ResizeStrategy.BILINEAR }).getBuffer('image/jpeg', { quality: 80 });
|
|
175
214
|
}
|
|
176
215
|
else {
|
|
177
|
-
throw new
|
|
216
|
+
throw new Boom('No image processing library available');
|
|
178
217
|
}
|
|
179
218
|
return {
|
|
180
|
-
img: await img
|
|
219
|
+
img: await img
|
|
181
220
|
};
|
|
182
221
|
};
|
|
183
|
-
exports.generateProfilePicture = generateProfilePicture;
|
|
184
222
|
/** gets the SHA256 of the given media message */
|
|
185
|
-
const mediaMessageSHA256B64 = (message) => {
|
|
223
|
+
export const mediaMessageSHA256B64 = (message) => {
|
|
186
224
|
const media = Object.values(message)[0];
|
|
187
|
-
return
|
|
225
|
+
return media?.fileSha256 && Buffer.from(media.fileSha256).toString('base64');
|
|
188
226
|
};
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
.ffprobe((err, data) => {
|
|
201
|
-
if (err) reject(err);
|
|
202
|
-
else resolve(data.format.duration);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
} catch (error) {
|
|
206
|
-
const musicMetadata = await import('music-metadata');
|
|
207
|
-
let metadata;
|
|
208
|
-
if (Buffer.isBuffer(buffer)) {
|
|
209
|
-
metadata = await musicMetadata.parseBuffer(buffer, undefined, {
|
|
210
|
-
duration: true
|
|
211
|
-
});
|
|
212
|
-
} else if (typeof buffer === 'string') {
|
|
213
|
-
const rStream = (0, fs_1.createReadStream)(buffer);
|
|
214
|
-
try {
|
|
215
|
-
metadata = await musicMetadata.parseStream(rStream, undefined, {
|
|
216
|
-
duration: true
|
|
217
|
-
});
|
|
218
|
-
} finally {
|
|
219
|
-
rStream.destroy();
|
|
220
|
-
}
|
|
221
|
-
} else {
|
|
222
|
-
metadata = await musicMetadata.parseStream(buffer, undefined, {
|
|
223
|
-
duration: true
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
return metadata.format.duration;
|
|
227
|
+
export async function getAudioDuration(buffer) {
|
|
228
|
+
const musicMetadata = await import('music-metadata');
|
|
229
|
+
let metadata;
|
|
230
|
+
const options = {
|
|
231
|
+
duration: true
|
|
232
|
+
};
|
|
233
|
+
if (Buffer.isBuffer(buffer)) {
|
|
234
|
+
metadata = await musicMetadata.parseBuffer(buffer, undefined, options);
|
|
235
|
+
}
|
|
236
|
+
else if (typeof buffer === 'string') {
|
|
237
|
+
metadata = await musicMetadata.parseFile(buffer, options);
|
|
227
238
|
}
|
|
239
|
+
else {
|
|
240
|
+
metadata = await musicMetadata.parseStream(buffer, undefined, options);
|
|
241
|
+
}
|
|
242
|
+
return metadata.format.duration;
|
|
228
243
|
}
|
|
229
|
-
|
|
230
|
-
|
|
244
|
+
/**
|
|
245
|
+
referenced from and modifying https://github.com/wppconnect-team/wa-js/blob/main/src/chat/functions/prepareAudioWaveform.ts
|
|
246
|
+
*/
|
|
247
|
+
export async function getAudioWaveform(buffer, logger) {
|
|
231
248
|
try {
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
249
|
+
// @ts-ignore
|
|
250
|
+
const { default: decoder } = await import('audio-decode');
|
|
235
251
|
let audioData;
|
|
236
252
|
if (Buffer.isBuffer(buffer)) {
|
|
237
253
|
audioData = buffer;
|
|
238
|
-
} else if (typeof buffer === 'string') {
|
|
239
|
-
const rStream = require('fs').createReadStream(buffer);
|
|
240
|
-
audioData = await exports.toBuffer(rStream);
|
|
241
|
-
} else {
|
|
242
|
-
audioData = await exports.toBuffer(buffer);
|
|
243
254
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const max = Math.max(...avg);
|
|
273
|
-
const normalized = avg.map(v => Math.floor((v / max) * 100));
|
|
274
|
-
resolve(new Uint8Array(normalized));
|
|
275
|
-
})
|
|
276
|
-
.pipe()
|
|
277
|
-
.on('data', chunk => chunks.push(chunk));
|
|
278
|
-
});
|
|
279
|
-
} catch (e) {
|
|
280
|
-
logger?.debug(e);
|
|
255
|
+
else if (typeof buffer === 'string') {
|
|
256
|
+
const rStream = createReadStream(buffer);
|
|
257
|
+
audioData = await toBuffer(rStream);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
audioData = await toBuffer(buffer);
|
|
261
|
+
}
|
|
262
|
+
const audioBuffer = await decoder(audioData);
|
|
263
|
+
const rawData = audioBuffer.getChannelData(0); // We only need to work with one channel of data
|
|
264
|
+
const samples = 64; // Number of samples we want to have in our final data set
|
|
265
|
+
const blockSize = Math.floor(rawData.length / samples); // the number of samples in each subdivision
|
|
266
|
+
const filteredData = [];
|
|
267
|
+
for (let i = 0; i < samples; i++) {
|
|
268
|
+
const blockStart = blockSize * i; // the location of the first sample in the block
|
|
269
|
+
let sum = 0;
|
|
270
|
+
for (let j = 0; j < blockSize; j++) {
|
|
271
|
+
sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block
|
|
272
|
+
}
|
|
273
|
+
filteredData.push(sum / blockSize); // divide the sum by the block size to get the average
|
|
274
|
+
}
|
|
275
|
+
// This guarantees that the largest data point will be set to 1, and the rest of the data will scale proportionally.
|
|
276
|
+
const multiplier = Math.pow(Math.max(...filteredData), -1);
|
|
277
|
+
const normalizedData = filteredData.map(n => n * multiplier);
|
|
278
|
+
// Generate waveform like WhatsApp
|
|
279
|
+
const waveform = new Uint8Array(normalizedData.map(n => Math.floor(100 * n)));
|
|
280
|
+
return waveform;
|
|
281
281
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
async function convertToOpusBuffer(buffer, logger) {
|
|
285
|
-
try {
|
|
286
|
-
const { PassThrough } = require('stream');
|
|
287
|
-
const ff = require('fluent-ffmpeg');
|
|
288
|
-
|
|
289
|
-
return await new Promise((resolve, reject) => {
|
|
290
|
-
const inStream = new PassThrough();
|
|
291
|
-
const outStream = new PassThrough();
|
|
292
|
-
const chunks = [];
|
|
293
|
-
inStream.end(buffer);
|
|
294
|
-
|
|
295
|
-
ff(inStream)
|
|
296
|
-
.noVideo()
|
|
297
|
-
.audioCodec('libopus')
|
|
298
|
-
.format('ogg')
|
|
299
|
-
.audioBitrate('48k')
|
|
300
|
-
.audioChannels(1)
|
|
301
|
-
.audioFrequency(48000)
|
|
302
|
-
.outputOptions([
|
|
303
|
-
'-vn',
|
|
304
|
-
'-b:a 64k',
|
|
305
|
-
'-ac 2',
|
|
306
|
-
'-ar 48000',
|
|
307
|
-
'-map_metadata', '-1',
|
|
308
|
-
'-application', 'voip'
|
|
309
|
-
])
|
|
310
|
-
.on('error', reject)
|
|
311
|
-
.on('end', () => resolve(Buffer.concat(chunks)))
|
|
312
|
-
.pipe(outStream, {
|
|
313
|
-
end: true
|
|
314
|
-
});
|
|
315
|
-
outStream.on('data', c => chunks.push(c));
|
|
316
|
-
});
|
|
317
|
-
} catch (e) {
|
|
318
|
-
logger?.debug(e);
|
|
319
|
-
throw e;
|
|
282
|
+
catch (e) {
|
|
283
|
+
logger?.debug('Failed to generate waveform: ' + e);
|
|
320
284
|
}
|
|
321
285
|
}
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
const readable = new stream_1.Readable({ read: () => { } });
|
|
286
|
+
export const toReadable = (buffer) => {
|
|
287
|
+
const readable = new Readable({ read: () => { } });
|
|
325
288
|
readable.push(buffer);
|
|
326
289
|
readable.push(null);
|
|
327
290
|
return readable;
|
|
328
291
|
};
|
|
329
|
-
|
|
330
|
-
const toBuffer = async (stream) => {
|
|
292
|
+
export const toBuffer = async (stream) => {
|
|
331
293
|
const chunks = [];
|
|
332
294
|
for await (const chunk of stream) {
|
|
333
295
|
chunks.push(chunk);
|
|
@@ -335,45 +297,44 @@ const toBuffer = async (stream) => {
|
|
|
335
297
|
stream.destroy();
|
|
336
298
|
return Buffer.concat(chunks);
|
|
337
299
|
};
|
|
338
|
-
|
|
339
|
-
const getStream = async (item, opts) => {
|
|
300
|
+
export const getStream = async (item, opts) => {
|
|
340
301
|
if (Buffer.isBuffer(item)) {
|
|
341
|
-
return { stream:
|
|
302
|
+
return { stream: toReadable(item), type: 'buffer' };
|
|
342
303
|
}
|
|
343
304
|
if ('stream' in item) {
|
|
344
305
|
return { stream: item.stream, type: 'readable' };
|
|
345
306
|
}
|
|
346
|
-
|
|
347
|
-
|
|
307
|
+
const urlStr = item.url.toString();
|
|
308
|
+
if (urlStr.startsWith('data:')) {
|
|
309
|
+
const buffer = Buffer.from(urlStr.split(',')[1], 'base64');
|
|
310
|
+
return { stream: toReadable(buffer), type: 'buffer' };
|
|
348
311
|
}
|
|
349
|
-
|
|
312
|
+
if (urlStr.startsWith('http://') || urlStr.startsWith('https://')) {
|
|
313
|
+
return { stream: await getHttpStream(item.url, opts), type: 'remote' };
|
|
314
|
+
}
|
|
315
|
+
return { stream: createReadStream(item.url), type: 'file' };
|
|
350
316
|
};
|
|
351
|
-
exports.getStream = getStream;
|
|
352
317
|
/** generates a thumbnail for a given media, if required */
|
|
353
|
-
async function generateThumbnail(file, mediaType, options) {
|
|
354
|
-
var _a;
|
|
318
|
+
export async function generateThumbnail(file, mediaType, options) {
|
|
355
319
|
let thumbnail;
|
|
356
320
|
let originalImageDimensions;
|
|
357
321
|
if (mediaType === 'image') {
|
|
358
|
-
const { buffer, original } = await
|
|
359
|
-
thumbnail = buffer
|
|
322
|
+
const { buffer, original } = await extractImageThumb(file);
|
|
323
|
+
thumbnail = buffer;
|
|
360
324
|
if (original.width && original.height) {
|
|
361
325
|
originalImageDimensions = {
|
|
362
326
|
width: original.width,
|
|
363
|
-
height: original.height
|
|
327
|
+
height: original.height
|
|
364
328
|
};
|
|
365
329
|
}
|
|
366
330
|
}
|
|
367
331
|
else if (mediaType === 'video') {
|
|
368
|
-
const imgFilename = (0, path_1.join)(getTmpFilesDirectory(), (0, generics_1.generateMessageID)() + '.jpg');
|
|
369
332
|
try {
|
|
370
|
-
await extractVideoThumb(file,
|
|
371
|
-
|
|
372
|
-
thumbnail = buff.toString('base64');
|
|
373
|
-
await fs_1.promises.unlink(imgFilename);
|
|
333
|
+
const buffer = await extractVideoThumb(file, '00:00:00', { width: 32, height: 32 });
|
|
334
|
+
thumbnail = buffer;
|
|
374
335
|
}
|
|
375
336
|
catch (err) {
|
|
376
|
-
|
|
337
|
+
options.logger?.debug('could not generate video thumb: ' + err);
|
|
377
338
|
}
|
|
378
339
|
}
|
|
379
340
|
return {
|
|
@@ -381,180 +342,129 @@ async function generateThumbnail(file, mediaType, options) {
|
|
|
381
342
|
originalImageDimensions
|
|
382
343
|
};
|
|
383
344
|
}
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const { stream, type } = await (0, exports.getStream)(media, opts);
|
|
393
|
-
logger === null || logger === void 0 ? void 0 : logger.debug('fetched media stream');
|
|
394
|
-
let bodyPath;
|
|
395
|
-
let didSaveToTmpPath = false;
|
|
396
|
-
try {
|
|
397
|
-
const buffer = await (0, exports.toBuffer)(stream);
|
|
398
|
-
if (type === 'file') {
|
|
399
|
-
bodyPath = media.url;
|
|
400
|
-
}
|
|
401
|
-
else if (saveOriginalFileIfRequired) {
|
|
402
|
-
bodyPath = (0, path_1.join)(getTmpFilesDirectory(), mediaType + (0, generics_1.generateMessageID)());
|
|
403
|
-
(0, fs_1.writeFileSync)(bodyPath, buffer);
|
|
404
|
-
didSaveToTmpPath = true;
|
|
405
|
-
}
|
|
406
|
-
const fileLength = buffer.length;
|
|
407
|
-
const fileSha256 = Crypto.createHash('sha256').update(buffer).digest();
|
|
408
|
-
stream === null || stream === void 0 ? void 0 : stream.destroy();
|
|
409
|
-
logger === null || logger === void 0 ? void 0 : logger.debug('prepare stream data successfully');
|
|
410
|
-
return {
|
|
411
|
-
mediaKey: undefined,
|
|
412
|
-
encWriteStream: buffer,
|
|
413
|
-
fileLength,
|
|
414
|
-
fileSha256,
|
|
415
|
-
fileEncSha256: undefined,
|
|
416
|
-
bodyPath,
|
|
417
|
-
didSaveToTmpPath
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
catch (error) {
|
|
421
|
-
// destroy all streams with error
|
|
422
|
-
stream.destroy();
|
|
423
|
-
if (didSaveToTmpPath) {
|
|
424
|
-
try {
|
|
425
|
-
await fs_1.promises.unlink(bodyPath);
|
|
426
|
-
}
|
|
427
|
-
catch (err) {
|
|
428
|
-
logger === null || logger === void 0 ? void 0 : logger.error({ err }, 'failed to save to tmp path');
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
throw error;
|
|
345
|
+
export const getHttpStream = async (url, options = {}) => {
|
|
346
|
+
const response = await fetch(url.toString(), {
|
|
347
|
+
dispatcher: options.dispatcher,
|
|
348
|
+
method: 'GET',
|
|
349
|
+
headers: options.headers
|
|
350
|
+
});
|
|
351
|
+
if (!response.ok) {
|
|
352
|
+
throw new Boom(`Failed to fetch stream from ${url}`, { statusCode: response.status, data: { url } });
|
|
432
353
|
}
|
|
354
|
+
// @ts-ignore Node18+ Readable.fromWeb exists
|
|
355
|
+
return response.body instanceof Readable ? response.body : Readable.fromWeb(response.body);
|
|
433
356
|
};
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
let finalStream = stream;
|
|
439
|
-
if (mediaType === 'audio' && (isPtt === true || forceOpus === true)) {
|
|
440
|
-
try {
|
|
441
|
-
const buffer = await (0, exports.toBuffer)(stream);
|
|
442
|
-
const opusBuffer = await exports.convertToOpusBuffer(buffer, logger);
|
|
443
|
-
finalStream = (0, exports.toReadable)(opusBuffer);
|
|
444
|
-
} catch (error) {
|
|
445
|
-
const { stream: newStream } = await (0, exports.getStream)(media, opts);
|
|
446
|
-
finalStream = newStream;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
357
|
+
export const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
|
|
358
|
+
const { stream, type } = await getStream(media, opts);
|
|
359
|
+
logger?.debug('fetched media stream');
|
|
450
360
|
const mediaKey = Crypto.randomBytes(32);
|
|
451
|
-
const { cipherKey, iv, macKey } = getMediaKeys(mediaKey, mediaType);
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
let
|
|
455
|
-
let
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
else if (saveOriginalFileIfRequired) {
|
|
461
|
-
bodyPath = (0, path_1.join)(getTmpFilesDirectory(), mediaType + (0, generics_1.generateMessageID)());
|
|
462
|
-
writeStream = (0, fs_1.createWriteStream)(bodyPath);
|
|
463
|
-
didSaveToTmpPath = true;
|
|
464
|
-
}
|
|
465
|
-
|
|
361
|
+
const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType);
|
|
362
|
+
const encFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2() + '-enc');
|
|
363
|
+
const encFileWriteStream = createWriteStream(encFilePath);
|
|
364
|
+
let originalFileStream;
|
|
365
|
+
let originalFilePath;
|
|
366
|
+
if (saveOriginalFileIfRequired) {
|
|
367
|
+
originalFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2() + '-original');
|
|
368
|
+
originalFileStream = createWriteStream(originalFilePath);
|
|
369
|
+
}
|
|
466
370
|
let fileLength = 0;
|
|
467
371
|
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
372
|
+
const hmac = Crypto.createHmac('sha256', macKey).update(iv);
|
|
373
|
+
const sha256Plain = Crypto.createHash('sha256');
|
|
374
|
+
const sha256Enc = Crypto.createHash('sha256');
|
|
375
|
+
const onChunk = async (buff) => {
|
|
376
|
+
sha256Enc.update(buff);
|
|
377
|
+
hmac.update(buff);
|
|
378
|
+
// Handle backpressure: if write returns false, wait for drain
|
|
379
|
+
if (!encFileWriteStream.write(buff)) {
|
|
380
|
+
await once(encFileWriteStream, 'drain');
|
|
381
|
+
}
|
|
382
|
+
};
|
|
472
383
|
try {
|
|
473
|
-
for await (const data of
|
|
384
|
+
for await (const data of stream) {
|
|
474
385
|
fileLength += data.length;
|
|
475
|
-
if (type === 'remote'
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
throw new
|
|
386
|
+
if (type === 'remote' &&
|
|
387
|
+
opts?.maxContentLength &&
|
|
388
|
+
fileLength + data.length > opts.maxContentLength) {
|
|
389
|
+
throw new Boom(`content length exceeded when encrypting "${type}"`, {
|
|
479
390
|
data: { media, type }
|
|
480
391
|
});
|
|
481
392
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (!writeStream.write(data)) {
|
|
486
|
-
await (0, events_1.once)(writeStream, 'drain');
|
|
393
|
+
if (originalFileStream) {
|
|
394
|
+
if (!originalFileStream.write(data)) {
|
|
395
|
+
await once(originalFileStream, 'drain');
|
|
487
396
|
}
|
|
488
397
|
}
|
|
489
|
-
|
|
398
|
+
sha256Plain.update(data);
|
|
399
|
+
await onChunk(aes.update(data));
|
|
490
400
|
}
|
|
491
|
-
|
|
492
|
-
onChunk(aes.final());
|
|
401
|
+
await onChunk(aes.final());
|
|
493
402
|
const mac = hmac.digest().slice(0, 10);
|
|
494
|
-
sha256Enc
|
|
403
|
+
sha256Enc.update(mac);
|
|
495
404
|
const fileSha256 = sha256Plain.digest();
|
|
496
405
|
const fileEncSha256 = sha256Enc.digest();
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
406
|
+
encFileWriteStream.write(mac);
|
|
407
|
+
const encFinishPromise = once(encFileWriteStream, 'finish');
|
|
408
|
+
const originalFinishPromise = originalFileStream ? once(originalFileStream, 'finish') : Promise.resolve();
|
|
409
|
+
encFileWriteStream.end();
|
|
410
|
+
originalFileStream?.end?.();
|
|
411
|
+
stream.destroy();
|
|
412
|
+
// Wait for write streams to fully flush to disk
|
|
413
|
+
// This helps reduce memory pressure by allowing OS to release buffers
|
|
414
|
+
await encFinishPromise;
|
|
415
|
+
await originalFinishPromise;
|
|
416
|
+
logger?.debug('encrypted data successfully');
|
|
503
417
|
return {
|
|
504
418
|
mediaKey,
|
|
505
|
-
|
|
506
|
-
|
|
419
|
+
originalFilePath,
|
|
420
|
+
encFilePath,
|
|
507
421
|
mac,
|
|
508
422
|
fileEncSha256,
|
|
509
423
|
fileSha256,
|
|
510
|
-
fileLength
|
|
511
|
-
didSaveToTmpPath
|
|
424
|
+
fileLength
|
|
512
425
|
};
|
|
513
426
|
}
|
|
514
427
|
catch (error) {
|
|
515
|
-
|
|
516
|
-
|
|
428
|
+
// destroy all streams with error
|
|
429
|
+
encFileWriteStream.destroy();
|
|
430
|
+
originalFileStream?.destroy?.();
|
|
517
431
|
aes.destroy();
|
|
518
432
|
hmac.destroy();
|
|
519
433
|
sha256Plain.destroy();
|
|
520
434
|
sha256Enc.destroy();
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
await
|
|
526
|
-
}
|
|
527
|
-
catch (err) {
|
|
435
|
+
stream.destroy();
|
|
436
|
+
try {
|
|
437
|
+
await fs.unlink(encFilePath);
|
|
438
|
+
if (originalFilePath) {
|
|
439
|
+
await fs.unlink(originalFilePath);
|
|
528
440
|
}
|
|
529
441
|
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
logger?.error({ err }, 'failed deleting tmp files');
|
|
444
|
+
}
|
|
530
445
|
throw error;
|
|
531
446
|
}
|
|
532
|
-
|
|
533
|
-
function onChunk(buff) {
|
|
534
|
-
sha256Enc = sha256Enc.update(buff);
|
|
535
|
-
hmac = hmac.update(buff);
|
|
536
|
-
encWriteStream.push(buff);
|
|
537
|
-
}
|
|
538
447
|
};
|
|
539
|
-
exports.encryptedStream = encryptedStream;
|
|
540
448
|
const DEF_HOST = 'mmg.whatsapp.net';
|
|
541
449
|
const AES_CHUNK_SIZE = 16;
|
|
542
450
|
const toSmallestChunkSize = (num) => {
|
|
543
451
|
return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE;
|
|
544
452
|
};
|
|
545
|
-
const getUrlFromDirectPath = (directPath) => `https://${DEF_HOST}${directPath}`;
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
const downloadUrl = url
|
|
549
|
-
|
|
550
|
-
|
|
453
|
+
export const getUrlFromDirectPath = (directPath) => `https://${DEF_HOST}${directPath}`;
|
|
454
|
+
export const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
|
|
455
|
+
const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/');
|
|
456
|
+
const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath);
|
|
457
|
+
if (!downloadUrl) {
|
|
458
|
+
throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 });
|
|
459
|
+
}
|
|
460
|
+
const keys = await getMediaKeys(mediaKey, type);
|
|
461
|
+
return downloadEncryptedContent(downloadUrl, keys, opts);
|
|
551
462
|
};
|
|
552
|
-
exports.downloadContentFromMessage = downloadContentFromMessage;
|
|
553
463
|
/**
|
|
554
464
|
* Decrypts and downloads an AES256-CBC encrypted file given the keys.
|
|
555
465
|
* Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
|
|
556
466
|
* */
|
|
557
|
-
const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
|
|
467
|
+
export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
|
|
558
468
|
let bytesFetched = 0;
|
|
559
469
|
let startChunk = 0;
|
|
560
470
|
let firstBlockIsIV = false;
|
|
@@ -568,9 +478,14 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
|
|
|
568
478
|
}
|
|
569
479
|
}
|
|
570
480
|
const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined;
|
|
481
|
+
const headersInit = options?.headers ? options.headers : undefined;
|
|
571
482
|
const headers = {
|
|
572
|
-
...(
|
|
573
|
-
|
|
483
|
+
...(headersInit
|
|
484
|
+
? Array.isArray(headersInit)
|
|
485
|
+
? Object.fromEntries(headersInit)
|
|
486
|
+
: headersInit
|
|
487
|
+
: {}),
|
|
488
|
+
Origin: DEFAULT_ORIGIN
|
|
574
489
|
};
|
|
575
490
|
if (startChunk || endChunk) {
|
|
576
491
|
headers.Range = `bytes=${startChunk}-`;
|
|
@@ -579,11 +494,9 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
|
|
|
579
494
|
}
|
|
580
495
|
}
|
|
581
496
|
// download the message
|
|
582
|
-
const fetched = await
|
|
583
|
-
...options || {},
|
|
584
|
-
headers
|
|
585
|
-
maxBodyLength: Infinity,
|
|
586
|
-
maxContentLength: Infinity,
|
|
497
|
+
const fetched = await getHttpStream(downloadUrl, {
|
|
498
|
+
...(options || {}),
|
|
499
|
+
headers
|
|
587
500
|
});
|
|
588
501
|
let remainingBytes = Buffer.from([]);
|
|
589
502
|
let aes;
|
|
@@ -598,7 +511,7 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
|
|
|
598
511
|
push(bytes);
|
|
599
512
|
}
|
|
600
513
|
};
|
|
601
|
-
const output = new
|
|
514
|
+
const output = new Transform({
|
|
602
515
|
transform(chunk, _, callback) {
|
|
603
516
|
let data = Buffer.concat([remainingBytes, chunk]);
|
|
604
517
|
const decryptLength = toSmallestChunkSize(data.length);
|
|
@@ -633,18 +546,15 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
|
|
|
633
546
|
catch (error) {
|
|
634
547
|
callback(error);
|
|
635
548
|
}
|
|
636
|
-
}
|
|
549
|
+
}
|
|
637
550
|
});
|
|
638
551
|
return fetched.pipe(output, { end: true });
|
|
639
552
|
};
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const getExtension = (mimetype) => mimetype.split(';')[0].split('/')[1];
|
|
553
|
+
export function extensionForMediaMessage(message) {
|
|
554
|
+
const getExtension = (mimetype) => mimetype.split(';')[0]?.split('/')[1];
|
|
643
555
|
const type = Object.keys(message)[0];
|
|
644
556
|
let extension;
|
|
645
|
-
if (type === 'locationMessage' ||
|
|
646
|
-
type === 'liveLocationMessage' ||
|
|
647
|
-
type === 'productMessage') {
|
|
557
|
+
if (type === 'locationMessage' || type === 'liveLocationMessage' || type === 'productMessage') {
|
|
648
558
|
extension = '.jpeg';
|
|
649
559
|
}
|
|
650
560
|
else {
|
|
@@ -653,54 +563,158 @@ function extensionForMediaMessage(message) {
|
|
|
653
563
|
}
|
|
654
564
|
return extension;
|
|
655
565
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
566
|
+
const isNodeRuntime = () => {
|
|
567
|
+
return (typeof process !== 'undefined' &&
|
|
568
|
+
process.versions?.node !== null &&
|
|
569
|
+
typeof process.versions.bun === 'undefined' &&
|
|
570
|
+
typeof globalThis.Deno === 'undefined');
|
|
571
|
+
};
|
|
572
|
+
export const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent }, redirectCount = 0) => {
|
|
573
|
+
if (redirectCount > 5) {
|
|
574
|
+
throw new Error('Too many redirects');
|
|
575
|
+
}
|
|
576
|
+
const parsedUrl = new URL(url);
|
|
577
|
+
const httpModule = parsedUrl.protocol === 'https:' ? await import('https') : await import('http');
|
|
578
|
+
// Get file size for Content-Length header (required for Node.js streaming)
|
|
579
|
+
const fileStats = await fs.stat(filePath);
|
|
580
|
+
const fileSize = fileStats.size;
|
|
581
|
+
return new Promise((resolve, reject) => {
|
|
582
|
+
const req = httpModule.request({
|
|
583
|
+
hostname: parsedUrl.hostname,
|
|
584
|
+
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
585
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
586
|
+
method: 'POST',
|
|
587
|
+
headers: {
|
|
588
|
+
...headers,
|
|
589
|
+
'Content-Length': fileSize
|
|
590
|
+
},
|
|
591
|
+
agent,
|
|
592
|
+
timeout: timeoutMs
|
|
593
|
+
}, res => {
|
|
594
|
+
// Handle redirects (3xx)
|
|
595
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
596
|
+
res.resume(); // Consume response to free resources
|
|
597
|
+
const newUrl = new URL(res.headers.location, url).toString();
|
|
598
|
+
resolve(uploadWithNodeHttp({
|
|
599
|
+
url: newUrl,
|
|
600
|
+
filePath,
|
|
601
|
+
headers,
|
|
602
|
+
timeoutMs,
|
|
603
|
+
agent
|
|
604
|
+
}, redirectCount + 1));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
let body = '';
|
|
608
|
+
res.on('data', chunk => (body += chunk));
|
|
609
|
+
res.on('end', () => {
|
|
610
|
+
try {
|
|
611
|
+
resolve(JSON.parse(body));
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
resolve(undefined);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
req.on('error', reject);
|
|
619
|
+
req.on('timeout', () => {
|
|
620
|
+
req.destroy();
|
|
621
|
+
reject(new Error('Upload timeout'));
|
|
622
|
+
});
|
|
623
|
+
const stream = createReadStream(filePath);
|
|
624
|
+
stream.pipe(req);
|
|
625
|
+
stream.on('error', err => {
|
|
626
|
+
req.destroy();
|
|
627
|
+
reject(err);
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
};
|
|
631
|
+
const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) => {
|
|
632
|
+
// Convert Node.js Readable to Web ReadableStream
|
|
633
|
+
const nodeStream = createReadStream(filePath);
|
|
634
|
+
const webStream = Readable.toWeb(nodeStream);
|
|
635
|
+
const response = await fetch(url, {
|
|
636
|
+
dispatcher: agent,
|
|
637
|
+
method: 'POST',
|
|
638
|
+
body: webStream,
|
|
639
|
+
headers,
|
|
640
|
+
duplex: 'half',
|
|
641
|
+
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
|
|
642
|
+
});
|
|
643
|
+
try {
|
|
644
|
+
return (await response.json());
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
/**
|
|
651
|
+
* Uploads media to WhatsApp servers.
|
|
652
|
+
*
|
|
653
|
+
* ## Why we have two upload implementations:
|
|
654
|
+
*
|
|
655
|
+
* Node.js's native `fetch` (powered by undici) has a known bug where it buffers
|
|
656
|
+
* the entire request body in memory before sending, even when using streams.
|
|
657
|
+
* This causes memory issues with large files (e.g., 1GB file = 1GB+ memory usage).
|
|
658
|
+
* See: https://github.com/nodejs/undici/issues/4058
|
|
659
|
+
*
|
|
660
|
+
* Other runtimes (Bun, Deno, browsers) correctly stream the request body without
|
|
661
|
+
* buffering, so we can use the web-standard Fetch API there.
|
|
662
|
+
*
|
|
663
|
+
* ## Future considerations:
|
|
664
|
+
* Once the undici bug is fixed, we can simplify this to use only the Fetch API
|
|
665
|
+
* across all runtimes. Monitor the GitHub issue for updates.
|
|
666
|
+
*/
|
|
667
|
+
const uploadMedia = async (params, logger) => {
|
|
668
|
+
if (isNodeRuntime()) {
|
|
669
|
+
logger?.debug('Using Node.js https module for upload (avoids undici buffering bug)');
|
|
670
|
+
return uploadWithNodeHttp(params);
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
logger?.debug('Using web-standard Fetch API for upload');
|
|
674
|
+
return uploadWithFetch(params);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
|
|
678
|
+
return async (filePath, { mediaType, fileEncSha256B64, timeoutMs, newsletter }) => {
|
|
661
679
|
// send a query JSON to obtain the url & auth token to upload our media
|
|
662
680
|
let uploadInfo = await refreshMediaConn(false);
|
|
663
681
|
let urls;
|
|
664
682
|
const hosts = [...customUploadHosts, ...uploadInfo.hosts];
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
683
|
+
fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64);
|
|
684
|
+
// Prepare common headers
|
|
685
|
+
const customHeaders = (() => {
|
|
686
|
+
const hdrs = options?.headers;
|
|
687
|
+
if (!hdrs)
|
|
688
|
+
return {};
|
|
689
|
+
return Array.isArray(hdrs) ? Object.fromEntries(hdrs) : hdrs;
|
|
690
|
+
})();
|
|
691
|
+
const headers = {
|
|
692
|
+
...customHeaders,
|
|
693
|
+
'Content-Type': 'application/octet-stream',
|
|
694
|
+
Origin: DEFAULT_ORIGIN
|
|
695
|
+
};
|
|
696
|
+
for (const { hostname } of hosts) {
|
|
678
697
|
logger.debug(`uploading to "${hostname}"`);
|
|
679
|
-
const auth = encodeURIComponent(uploadInfo.auth);
|
|
680
|
-
|
|
698
|
+
const auth = encodeURIComponent(uploadInfo.auth);
|
|
699
|
+
// vltcs@changes 06-02-26 --- Switch media path map for newsletter uploads
|
|
700
|
+
const mediaPathMap = newsletter ? NEWSLETTER_MEDIA_PATH_MAP : MEDIA_PATH_MAP
|
|
701
|
+
const url = `https://${hostname}${mediaPathMap[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;
|
|
681
702
|
let result;
|
|
682
703
|
try {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
'Origin': Defaults_1.DEFAULT_ORIGIN
|
|
692
|
-
},
|
|
693
|
-
httpsAgent: fetchAgent,
|
|
694
|
-
timeout: timeoutMs,
|
|
695
|
-
responseType: 'json',
|
|
696
|
-
maxBodyLength: Infinity,
|
|
697
|
-
maxContentLength: Infinity,
|
|
698
|
-
});
|
|
699
|
-
result = body.data;
|
|
700
|
-
if ((result === null || result === void 0 ? void 0 : result.url) || (result === null || result === void 0 ? void 0 : result.directPath)) {
|
|
704
|
+
result = await uploadMedia({
|
|
705
|
+
url,
|
|
706
|
+
filePath,
|
|
707
|
+
headers,
|
|
708
|
+
timeoutMs,
|
|
709
|
+
agent: fetchAgent
|
|
710
|
+
}, logger);
|
|
711
|
+
if (result?.url || result?.direct_path) {
|
|
701
712
|
urls = {
|
|
702
713
|
mediaUrl: result.url,
|
|
703
714
|
directPath: result.direct_path,
|
|
715
|
+
meta_hmac: result.meta_hmac,
|
|
716
|
+
fbid: result.fbid,
|
|
717
|
+
ts: result.ts,
|
|
704
718
|
handle: result.handle
|
|
705
719
|
};
|
|
706
720
|
break;
|
|
@@ -711,37 +725,33 @@ const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options },
|
|
|
711
725
|
}
|
|
712
726
|
}
|
|
713
727
|
catch (error) {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
-
const isLast = hostname === ((_b = hosts[uploadInfo.hosts.length - 1]) === null || _b === void 0 ? void 0 : _b.hostname);
|
|
718
|
-
logger.warn({ trace: error.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`);
|
|
728
|
+
const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname;
|
|
729
|
+
logger.warn({ trace: error?.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`);
|
|
719
730
|
}
|
|
720
731
|
}
|
|
721
732
|
if (!urls) {
|
|
722
|
-
throw new
|
|
733
|
+
throw new Boom('Media upload failed on all hosts', { statusCode: 500 });
|
|
723
734
|
}
|
|
724
735
|
return urls;
|
|
725
736
|
};
|
|
726
737
|
};
|
|
727
|
-
exports.getWAUploadToServer = getWAUploadToServer;
|
|
728
738
|
const getMediaRetryKey = (mediaKey) => {
|
|
729
|
-
return
|
|
739
|
+
return hkdf(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' });
|
|
730
740
|
};
|
|
731
741
|
/**
|
|
732
742
|
* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
|
|
733
743
|
*/
|
|
734
|
-
const encryptMediaRetryRequest = (key, mediaKey, meId) => {
|
|
744
|
+
export const encryptMediaRetryRequest = (key, mediaKey, meId) => {
|
|
735
745
|
const recp = { stanzaId: key.id };
|
|
736
|
-
const recpBuffer =
|
|
746
|
+
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish();
|
|
737
747
|
const iv = Crypto.randomBytes(12);
|
|
738
748
|
const retryKey = getMediaRetryKey(mediaKey);
|
|
739
|
-
const ciphertext =
|
|
749
|
+
const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id));
|
|
740
750
|
const req = {
|
|
741
751
|
tag: 'receipt',
|
|
742
752
|
attrs: {
|
|
743
753
|
id: key.id,
|
|
744
|
-
to:
|
|
754
|
+
to: jidNormalizedUser(meId),
|
|
745
755
|
type: 'server-error'
|
|
746
756
|
},
|
|
747
757
|
content: [
|
|
@@ -760,7 +770,7 @@ const encryptMediaRetryRequest = (key, mediaKey, meId) => {
|
|
|
760
770
|
tag: 'rmr',
|
|
761
771
|
attrs: {
|
|
762
772
|
jid: key.remoteJid,
|
|
763
|
-
|
|
773
|
+
from_me: (!!key.fromMe).toString(),
|
|
764
774
|
// @ts-ignore
|
|
765
775
|
participant: key.participant || undefined
|
|
766
776
|
}
|
|
@@ -769,9 +779,8 @@ const encryptMediaRetryRequest = (key, mediaKey, meId) => {
|
|
|
769
779
|
};
|
|
770
780
|
return req;
|
|
771
781
|
};
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
const rmrNode = (0, WABinary_1.getBinaryNodeChild)(node, 'rmr');
|
|
782
|
+
export const decodeMediaRetryNode = (node) => {
|
|
783
|
+
const rmrNode = getBinaryNodeChild(node, 'rmr');
|
|
775
784
|
const event = {
|
|
776
785
|
key: {
|
|
777
786
|
id: node.attrs.id,
|
|
@@ -780,40 +789,36 @@ const decodeMediaRetryNode = (node) => {
|
|
|
780
789
|
participant: rmrNode.attrs.participant
|
|
781
790
|
}
|
|
782
791
|
};
|
|
783
|
-
const errorNode =
|
|
792
|
+
const errorNode = getBinaryNodeChild(node, 'error');
|
|
784
793
|
if (errorNode) {
|
|
785
794
|
const errorCode = +errorNode.attrs.code;
|
|
786
|
-
event.error = new
|
|
795
|
+
event.error = new Boom(`Failed to re-upload media (${errorCode})`, {
|
|
796
|
+
data: errorNode.attrs,
|
|
797
|
+
statusCode: getStatusCodeForMediaRetry(errorCode)
|
|
798
|
+
});
|
|
787
799
|
}
|
|
788
800
|
else {
|
|
789
|
-
const encryptedInfoNode =
|
|
790
|
-
const ciphertext =
|
|
791
|
-
const iv =
|
|
801
|
+
const encryptedInfoNode = getBinaryNodeChild(node, 'encrypt');
|
|
802
|
+
const ciphertext = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_p');
|
|
803
|
+
const iv = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_iv');
|
|
792
804
|
if (ciphertext && iv) {
|
|
793
805
|
event.media = { ciphertext, iv };
|
|
794
806
|
}
|
|
795
807
|
else {
|
|
796
|
-
event.error = new
|
|
808
|
+
event.error = new Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 });
|
|
797
809
|
}
|
|
798
810
|
}
|
|
799
811
|
return event;
|
|
800
812
|
};
|
|
801
|
-
|
|
802
|
-
const decryptMediaRetryData = ({ ciphertext, iv }, mediaKey, msgId) => {
|
|
813
|
+
export const decryptMediaRetryData = ({ ciphertext, iv }, mediaKey, msgId) => {
|
|
803
814
|
const retryKey = getMediaRetryKey(mediaKey);
|
|
804
|
-
const plaintext =
|
|
805
|
-
return
|
|
815
|
+
const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId));
|
|
816
|
+
return proto.MediaRetryNotification.decode(plaintext);
|
|
806
817
|
};
|
|
807
|
-
|
|
808
|
-
const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code];
|
|
809
|
-
exports.getStatusCodeForMediaRetry = getStatusCodeForMediaRetry;
|
|
818
|
+
export const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code];
|
|
810
819
|
const MEDIA_RETRY_STATUS_MAP = {
|
|
811
|
-
[
|
|
812
|
-
[
|
|
813
|
-
[
|
|
814
|
-
[
|
|
815
|
-
};
|
|
816
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
817
|
-
function __importStar(arg0) {
|
|
818
|
-
throw new Error('Function not implemented.');
|
|
819
|
-
}
|
|
820
|
+
[proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
|
|
821
|
+
[proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
|
|
822
|
+
[proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
|
|
823
|
+
[proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
|
|
824
|
+
};
|