baileyz 1.0.6 → 1.0.8

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.
@@ -1,319 +1,286 @@
1
- "use strict"
2
-
3
- Object.defineProperty(exports, "__esModule", { value: true })
4
-
5
- const { Boom } = require("@hapi/boom")
6
- const { exec } = require("child_process")
7
- const { once } = require("events")
8
- const {
9
- createHash,
10
- randomBytes,
11
- createHmac,
12
- createCipheriv,
13
- createDecipheriv
14
- } = require("crypto")
15
- const {
16
- promises,
17
- createReadStream,
18
- createWriteStream
19
- } = require("fs")
20
- const {
21
- parseBuffer,
22
- parseFile,
23
- parseStream
24
- } = require('music-metadata')
25
- const { tmpdir } = require("os")
26
- const { join } = require("path")
27
- const {
28
- Readable,
29
- Transform
30
- } = require("stream")
31
- const { proto } = require("../../WAProto")
32
- const {
33
- MEDIA_PATH_MAP,
34
- MEDIA_HKDF_KEY_MAPPING
35
- } = require("../Defaults/media")
36
- const { DEFAULT_ORIGIN } = require("../Defaults/constants")
37
- const {
38
- getBinaryNodeChild,
39
- getBinaryNodeChildBuffer,
40
- jidNormalizedUser
41
- } = require("../WABinary")
42
- const {
43
- aesDecryptGCM,
44
- aesEncryptGCM,
45
- hkdf
46
- } = require("./crypto")
47
- const { generateMessageID } = require("./generics")
48
-
49
- const getTmpFilesDirectory = () => tmpdir()
50
-
51
- const getImageProcessingLibrary = () => {
52
- let sharp, jimp
53
-
54
- try {
55
- sharp = require('sharp')
56
- } catch {}
57
-
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
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
+ })()
53
+ ]);
58
54
  if (sharp) {
59
- return { sharp }
55
+ return { sharp };
60
56
  }
61
-
62
- try {
63
- jimp = require('jimp')
64
- } catch {}
65
-
57
+ const jimp = (_jimp === null || _jimp === void 0 ? void 0 : _jimp.default) || _jimp;
66
58
  if (jimp) {
67
- return { jimp }
59
+ return { jimp };
68
60
  }
69
-
70
- throw new Boom('No image processing library available')
71
- }
72
-
61
+ throw new boom_1.Boom('No image processing library available');
62
+ };
73
63
  const hkdfInfoKey = (type) => {
74
- const hkdfInfo = MEDIA_HKDF_KEY_MAPPING[type]
75
- return `WhatsApp ${hkdfInfo} Keys`
76
- }
77
-
78
- const getRawMediaUploadData = async (media, mediaType, logger) => {
79
- const { stream } = await getStream(media)
80
-
81
- logger?.debug('got stream for raw upload')
82
-
83
- const hasher = createHash('sha256')
84
- const filePath = join(getTmpFilesDirectory(), mediaType + generateMessageID())
85
- const fileWriteStream = createWriteStream(filePath)
86
-
87
- let fileLength = 0
88
-
89
- try {
90
- for await (const data of stream) {
91
- fileLength += data.length
92
- hasher.update(data)
93
-
94
- if (!fileWriteStream.write(data)) {
95
- await once(fileWriteStream, 'drain')
96
- }
97
- }
98
-
99
- fileWriteStream.end()
100
- await once(fileWriteStream, 'finish')
101
- stream.destroy()
102
-
103
- const fileSha256 = hasher.digest()
104
-
105
- logger?.debug('hashed data for raw upload')
106
-
107
- return {
108
- filePath: filePath,
109
- fileSha256,
110
- fileLength
111
- }
112
- }
113
- catch (error) {
114
- fileWriteStream.destroy()
115
- stream.destroy()
116
-
117
- try {
118
- await promises.unlink(filePath)
119
- }
120
- catch {
121
- //
122
- }
123
- throw error
124
- }
125
- }
126
-
64
+ const hkdfInfo = Defaults_1.MEDIA_HKDF_KEY_MAPPING[type];
65
+ return `WhatsApp ${hkdfInfo} Keys`;
66
+ };
67
+ exports.hkdfInfoKey = hkdfInfoKey;
127
68
  /** generates all the keys required to encrypt/decrypt & sign a media message */
128
- async function getMediaKeys(buffer, mediaType) {
69
+ function getMediaKeys(buffer, mediaType) {
129
70
  if (!buffer) {
130
- throw new Boom('Cannot derive from empty media key')
71
+ throw new boom_1.Boom('Cannot derive from empty media key');
131
72
  }
132
73
  if (typeof buffer === 'string') {
133
- buffer = Buffer.from(buffer.replace('data:base64,', ''), 'base64')
74
+ buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64');
134
75
  }
135
76
  // expand using HKDF to 112 bytes, also pass in the relevant app info
136
- const expandedMediaKey = await hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
77
+ const expandedMediaKey = (0, crypto_1.hkdf)(buffer, 112, { info: (0, exports.hkdfInfoKey)(mediaType) });
137
78
  return {
138
79
  iv: expandedMediaKey.slice(0, 16),
139
80
  cipherKey: expandedMediaKey.slice(16, 48),
140
- macKey: expandedMediaKey.slice(48, 80)
141
- }
81
+ macKey: expandedMediaKey.slice(48, 80),
82
+ };
142
83
  }
143
-
84
+ exports.getMediaKeys = getMediaKeys;
144
85
  /** Extracts video thumb using FFMPEG */
145
86
  const extractVideoThumb = async (path, destPath, time, size) => new Promise((resolve, reject) => {
146
- const cmd = `ffmpeg -ss ${time} -i ${path} -y -vf scale=${size.width}:-1 -vframes 1 -f image2 ${destPath}`
147
- exec(cmd, err => {
87
+ const cmd = `ffmpeg -ss ${time} -i ${path} -y -vf scale=${size.width}:-1 -vframes 1 -f image2 ${destPath}`;
88
+ (0, child_process_1.exec)(cmd, (err) => {
148
89
  if (err) {
149
- reject(err)
90
+ reject(err);
150
91
  }
151
92
  else {
152
- resolve()
153
- }
154
- })
155
- })
156
-
157
- const extractImageThumb = async (bufferOrFilePath, width = 32, quality = 50) => {
158
- // TODO: Move entirely to sharp, removing jimp as it supports readable streams
159
- // This will have positive speed and performance impacts as well as minimizing RAM usage.
160
- if (bufferOrFilePath instanceof Readable) {
161
- bufferOrFilePath = await toBuffer(bufferOrFilePath)
162
- }
163
-
164
- const lib = await getImageProcessingLibrary()
165
-
166
- if ('sharp' in lib && typeof lib.sharp === 'function') {
167
- const img = lib.sharp(bufferOrFilePath)
168
- const dimensions = await img.metadata()
169
- const buffer = await img.resize(width).jpeg({ quality: 50 }).toBuffer()
93
+ resolve();
94
+ }
95
+ });
96
+ });
97
+ const extractImageThumb = async (bufferOrFilePath, width = 32) => {
98
+ var _a, _b;
99
+ if (bufferOrFilePath instanceof stream_1.Readable) {
100
+ bufferOrFilePath = await (0, exports.toBuffer)(bufferOrFilePath);
101
+ }
102
+ const lib = await getImageProcessingLibrary();
103
+ if ('sharp' in lib && typeof ((_a = lib.sharp) === null || _a === void 0 ? void 0 : _a.default) === 'function') {
104
+ const img = lib.sharp.default(bufferOrFilePath);
105
+ const dimensions = await img.metadata();
106
+ const buffer = await img
107
+ .resize(width)
108
+ .jpeg({ quality: 50 })
109
+ .toBuffer();
170
110
  return {
171
111
  buffer,
172
112
  original: {
173
113
  width: dimensions.width,
174
- height: dimensions.height
175
- }
176
- }
114
+ height: dimensions.height,
115
+ },
116
+ };
177
117
  }
178
- else if ('jimp' in lib && typeof lib.jimp.read === 'function') {
179
- const { read, MIME_JPEG, RESIZE_BEZIER, AUTO } = lib.jimp
180
- const jimp = await read(bufferOrFilePath)
118
+ else if ('jimp' in lib && typeof ((_b = lib.jimp) === null || _b === void 0 ? void 0 : _b.read) === 'function') {
119
+ const { read, MIME_JPEG, RESIZE_BILINEAR, AUTO } = lib.jimp;
120
+ const jimp = await read(bufferOrFilePath);
181
121
  const dimensions = {
182
122
  width: jimp.getWidth(),
183
- height: jimp.getHeight()
184
- }
123
+ height: jimp.getHeight()
124
+ };
185
125
  const buffer = await jimp
186
- .quality(quality)
187
- .resize(width, AUTO, RESIZE_BEZIER)
188
- .getBufferAsync(MIME_JPEG)
126
+ .quality(50)
127
+ .resize(width, AUTO, RESIZE_BILINEAR)
128
+ .getBufferAsync(MIME_JPEG);
189
129
  return {
190
130
  buffer,
191
131
  original: dimensions
192
- }
132
+ };
193
133
  }
194
134
  else {
195
- throw new Boom('No image processing library available')
135
+ throw new boom_1.Boom('No image processing library available');
196
136
  }
197
- }
198
-
137
+ };
138
+ exports.extractImageThumb = extractImageThumb;
199
139
  const encodeBase64EncodedStringForUpload = (b64) => (encodeURIComponent(b64
200
140
  .replace(/\+/g, '-')
201
141
  .replace(/\//g, '_')
202
- .replace(/\=+$/, '')))
203
-
204
- const generateProfilePicture = async (mediaUpload, dimensions) => {
205
- let buffer
206
-
207
- const { width: w = 640, height: h = 640 } = dimensions || {}
208
-
142
+ .replace(/\=+$/, '')));
143
+ exports.encodeBase64EncodedStringForUpload = encodeBase64EncodedStringForUpload;
144
+ const generateProfilePicture = async (mediaUpload) => {
145
+ var _a, _b;
146
+ let bufferOrFilePath;
209
147
  if (Buffer.isBuffer(mediaUpload)) {
210
- buffer = mediaUpload
148
+ bufferOrFilePath = mediaUpload;
149
+ }
150
+ else if ('url' in mediaUpload) {
151
+ bufferOrFilePath = mediaUpload.url.toString();
211
152
  }
212
153
  else {
213
- // Use getStream to handle all WAMediaUpload types (Buffer, Stream, URL)
214
- const { stream } = await getStream(mediaUpload)
215
- // Convert the resulting stream to a buffer
216
- buffer = await toBuffer(stream)
154
+ bufferOrFilePath = await (0, exports.toBuffer)(mediaUpload.stream);
217
155
  }
218
- const lib = await getImageProcessingLibrary()
219
-
220
- let img
221
-
222
- if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
223
- img = lib.sharp
224
- .default(buffer)
225
- .resize(w, h)
156
+ const lib = await getImageProcessingLibrary();
157
+ let img;
158
+ if ('sharp' in lib && typeof ((_a = lib.sharp) === null || _a === void 0 ? void 0 : _a.default) === 'function') {
159
+ img = lib.sharp.default(bufferOrFilePath)
160
+ .resize(640, 640)
226
161
  .jpeg({
227
- quality: 50
228
- }).toBuffer()
229
- }
230
- else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
231
- const jimp = await lib.jimp.read(buffer)
232
- const min = Math.min(jimp.width, jimp.height)
233
- const cropped = jimp.crop({ x: 0, y: 0, w: min, h: min })
234
- img = cropped.resize({ w, h, mode: lib.jimp.ResizeStrategy.BILINEAR }).getBuffer('image/jpeg', { quality: 50 })
162
+ quality: 50,
163
+ })
164
+ .toBuffer();
165
+ }
166
+ else if ('jimp' in lib && typeof ((_b = lib.jimp) === null || _b === void 0 ? void 0 : _b.read) === 'function') {
167
+ const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp;
168
+ const jimp = await read(bufferOrFilePath);
169
+ const min = Math.min(jimp.getWidth(), jimp.getHeight());
170
+ const cropped = jimp.crop(0, 0, min, min);
171
+ img = cropped
172
+ .quality(50)
173
+ .resize(640, 640, RESIZE_BILINEAR)
174
+ .getBufferAsync(MIME_JPEG);
235
175
  }
236
176
  else {
237
- throw new Boom('No image processing library available');
177
+ throw new boom_1.Boom('No image processing library available');
238
178
  }
239
179
  return {
240
- img: await img
241
- }
242
- }
243
-
180
+ img: await img,
181
+ };
182
+ };
183
+ exports.generateProfilePicture = generateProfilePicture;
244
184
  /** gets the SHA256 of the given media message */
245
185
  const mediaMessageSHA256B64 = (message) => {
246
- const media = Object.values(message)[0]
247
- return media?.fileSha256 && Buffer.from(media.fileSha256).toString('base64')
248
- }
249
-
186
+ const media = Object.values(message)[0];
187
+ return (media === null || media === void 0 ? void 0 : media.fileSha256) && Buffer.from(media.fileSha256).toString('base64');
188
+ };
189
+ exports.mediaMessageSHA256B64 = mediaMessageSHA256B64;
250
190
  async function getAudioDuration(buffer) {
251
- const options = {
252
- duration: true
253
- }
254
-
255
- let metadata
256
-
257
- if (Buffer.isBuffer(buffer)) {
258
- metadata = await parseBuffer(buffer, undefined, options)
259
- }
260
- else if (typeof buffer === 'string') {
261
- metadata = await parseFile(buffer, options)
262
- }
263
- else {
264
- metadata = await parseStream(buffer, undefined, options)
191
+ try {
192
+ const { PassThrough } = require('stream');
193
+ const ff = require('fluent-ffmpeg');
194
+
195
+ return await new Promise((resolve, reject) => {
196
+ const inputStream = new PassThrough();
197
+ inputStream.end(buffer);
198
+
199
+ ff(inputStream)
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;
265
227
  }
266
- return metadata.format?.duration
267
228
  }
268
-
269
- /**
270
- referenced from and modifying https://github.com/wppconnect-team/wa-js/blob/main/src/chat/functions/prepareAudioWaveform.ts
271
- */
229
+ exports.getAudioDuration = getAudioDuration;
272
230
  async function getAudioWaveform(buffer, logger) {
273
231
  try {
274
- const { default: decoder } = await eval('import(\'audio-decode\')')
232
+ const { PassThrough } = require('stream');
233
+ const ff = require('fluent-ffmpeg');
275
234
 
276
- let audioData
277
-
235
+ let audioData;
278
236
  if (Buffer.isBuffer(buffer)) {
279
- audioData = buffer
280
- }
281
- else if (typeof buffer === 'string') {
282
- const rStream = createReadStream(buffer)
283
- audioData = await toBuffer(rStream)
284
- }
285
- else {
286
- audioData = await toBuffer(buffer)
287
- }
288
-
289
- const audioBuffer = await decoder(audioData)
290
- const rawData = audioBuffer.getChannelData(0) // We only need to work with one channel of data
291
- const samples = 64 // Number of samples we want to have in our final data set
292
- const blockSize = Math.floor(rawData.length / samples) // the number of samples in each subdivision
293
- const filteredData = []
294
-
295
- for (let i = 0; i < samples; i++) {
296
- const blockStart = blockSize * i // the location of the first sample in the block
297
- let sum = 0
298
- for (let j = 0; j < blockSize; j++) {
299
- sum = sum + Math.abs(rawData[blockStart + j]) // find the sum of all the samples in the block
300
- }
301
- filteredData.push(sum / blockSize) // divide the sum by the block size to get the average
237
+ 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);
302
243
  }
303
244
 
304
- // This guarantees that the largest data point will be set to 1, and the rest of the data will scale proportionally.
305
- const multiplier = Math.pow(Math.max(...filteredData), -1)
306
- const normalizedData = filteredData.map((n) => n * multiplier)
307
-
308
- // Generate waveform like WhatsApp
309
- const waveform = new Uint8Array(normalizedData.map((n) => Math.floor(100 * n)))
310
- return waveform
311
- }
312
- catch (e) {
313
- logger?.debug('Failed to generate waveform: ' + e)
245
+ return await new Promise((resolve, reject) => {
246
+ const inputStream = new PassThrough();
247
+ inputStream.end(audioData);
248
+ const chunks = [];
249
+ const bars = 64;
250
+
251
+ ff(inputStream)
252
+ .audioChannels(1)
253
+ .audioFrequency(16000)
254
+ .format('s16le')
255
+ .on('error', reject)
256
+ .on('end', () => {
257
+ const rawData = Buffer.concat(chunks);
258
+ const samples = rawData.length / 2;
259
+ const amplitudes = [];
260
+
261
+ for (let i = 0; i < samples; i++) {
262
+ amplitudes.push(Math.abs(rawData.readInt16LE(i * 2)) / 32768);
263
+ }
264
+
265
+ const blockSize = Math.floor(amplitudes.length / bars);
266
+ const avg = [];
267
+ for (let i = 0; i < bars; i++) {
268
+ const block = amplitudes.slice(i * blockSize, (i + 1) * blockSize);
269
+ avg.push(block.reduce((a, b) => a + b, 0) / block.length);
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);
314
281
  }
315
282
  }
316
-
283
+ exports.getAudioWaveform = getAudioWaveform;
317
284
  async function convertToOpusBuffer(buffer, logger) {
318
285
  try {
319
286
  const { PassThrough } = require('stream');
@@ -352,659 +319,429 @@ async function convertToOpusBuffer(buffer, logger) {
352
319
  throw e;
353
320
  }
354
321
  }
355
-
322
+ exports.convertToOpusBuffer = convertToOpusBuffer;
356
323
  const toReadable = (buffer) => {
357
- const readable = new Readable({ read: () => { } })
358
- readable.push(buffer)
359
- readable.push(null)
360
- return readable
361
- }
362
-
324
+ const readable = new stream_1.Readable({ read: () => { } });
325
+ readable.push(buffer);
326
+ readable.push(null);
327
+ return readable;
328
+ };
329
+ exports.toReadable = toReadable;
363
330
  const toBuffer = async (stream) => {
364
- const chunks = []
331
+ const chunks = [];
365
332
  for await (const chunk of stream) {
366
- chunks.push(chunk)
333
+ chunks.push(chunk);
367
334
  }
368
- stream.destroy()
369
- return Buffer.concat(chunks)
370
- }
371
-
335
+ stream.destroy();
336
+ return Buffer.concat(chunks);
337
+ };
338
+ exports.toBuffer = toBuffer;
372
339
  const getStream = async (item, opts) => {
373
340
  if (Buffer.isBuffer(item)) {
374
- return { stream: toReadable(item), type: 'buffer' }
341
+ return { stream: (0, exports.toReadable)(item), type: 'buffer' };
375
342
  }
376
-
377
343
  if ('stream' in item) {
378
- return { stream: item.stream, type: 'readable' }
379
- }
380
-
381
- const urlStr = item.url.toString()
382
-
383
- if (urlStr.startsWith('data:')) {
384
- const buffer = Buffer.from(urlStr.split(',')[1], 'base64')
385
- return { stream: await toReadable(buffer), type: 'buffer' }
344
+ return { stream: item.stream, type: 'readable' };
386
345
  }
387
-
388
- if (urlStr.startsWith('http://') || urlStr.startsWith('https://')) {
389
- return { stream: await getHttpStream(item.url, opts), type: 'remote' }
346
+ if (item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
347
+ return { stream: await (0, exports.getHttpStream)(item.url, opts), type: 'remote' };
390
348
  }
391
-
392
- return { stream: createReadStream(item.url), type: 'file' }
393
- }
394
-
349
+ return { stream: (0, fs_1.createReadStream)(item.url), type: 'file' };
350
+ };
351
+ exports.getStream = getStream;
395
352
  /** generates a thumbnail for a given media, if required */
396
353
  async function generateThumbnail(file, mediaType, options) {
397
- let thumbnail
398
- let originalImageDimensions
399
-
354
+ var _a;
355
+ let thumbnail;
356
+ let originalImageDimensions;
400
357
  if (mediaType === 'image') {
401
- const { buffer, original } = await extractImageThumb(file)
402
-
403
- thumbnail = buffer.toString('base64')
404
-
358
+ const { buffer, original } = await (0, exports.extractImageThumb)(file);
359
+ thumbnail = buffer.toString('base64');
405
360
  if (original.width && original.height) {
406
361
  originalImageDimensions = {
407
362
  width: original.width,
408
- height: original.height
409
- }
363
+ height: original.height,
364
+ };
410
365
  }
411
366
  }
412
367
  else if (mediaType === 'video') {
413
- const imgFilename = join(getTmpFilesDirectory(), generateMessageID() + '.jpg')
368
+ const imgFilename = (0, path_1.join)(getTmpFilesDirectory(), (0, generics_1.generateMessageID)() + '.jpg');
414
369
  try {
415
- await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 })
416
- const buff = await promises.readFile(imgFilename)
417
-
418
- thumbnail = buff.toString('base64')
419
-
420
- await promises.unlink(imgFilename)
370
+ await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 });
371
+ const buff = await fs_1.promises.readFile(imgFilename);
372
+ thumbnail = buff.toString('base64');
373
+ await fs_1.promises.unlink(imgFilename);
421
374
  }
422
375
  catch (err) {
423
- options.logger?.debug('could not generate video thumb: ' + err)
376
+ (_a = options.logger) === null || _a === void 0 ? void 0 : _a.debug('could not generate video thumb: ' + err);
424
377
  }
425
378
  }
426
379
  return {
427
380
  thumbnail,
428
381
  originalImageDimensions
429
- }
382
+ };
430
383
  }
431
-
384
+ exports.generateThumbnail = generateThumbnail;
432
385
  const getHttpStream = async (url, options = {}) => {
433
- const response = await fetch(url.toString(), {
434
- dispatcher: options.dispatcher,
435
- method: 'GET',
436
- headers: options.headers
437
- })
438
-
439
- if (!response.ok) {
440
- throw new Boom(`Failed to fetch stream from ${url}`, { statusCode: response.status, data: { url } })
441
- }
442
-
443
- return response.body instanceof Readable ? response.body : Readable.fromWeb(response.body)
444
- }
445
-
446
- /*const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
447
- const { stream, type } = await getStream(media, opts)
448
- logger?.debug('fetched media stream')
449
-
450
- const encFilePath = join(tmpdir(), mediaType + generateMessageID() + '-plain')
451
- const encFileWriteStream = createWriteStream(encFilePath)
452
-
453
- let originalFilePath
454
- let originalFileStream
455
-
456
- if (type === 'file') {
457
- originalFilePath = media.url.toString()
458
- } else if (saveOriginalFileIfRequired) {
459
- originalFilePath = join(tmpdir(), mediaType + generateMessageID() + '-original')
460
- originalFileStream = createWriteStream(originalFilePath)
461
- }
462
-
463
- let fileLength = 0
464
- const sha256 = createHash('sha256')
465
-
386
+ const { default: axios } = await import('axios');
387
+ const fetched = await axios.get(url.toString(), { ...options, responseType: 'stream' });
388
+ return fetched.data;
389
+ };
390
+ exports.getHttpStream = getHttpStream;
391
+ const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
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;
466
396
  try {
467
- for await (const data of stream) {
468
- fileLength += data.length
469
-
470
- if (type === 'remote'
471
- && opts?.maxContentLength
472
- && fileLength + data.length > opts.maxContentLength) {
473
- throw new Boom(`content length exceeded when preparing "${type}"`, {
474
- data: { media, type }
475
- })
476
- }
477
-
478
- sha256.update(data)
479
- encFileWriteStream.write(data)
480
-
481
- if (originalFileStream && !originalFileStream.write(data)) {
482
- await once(originalFileStream, 'drain')
483
- }
484
- }
485
-
486
- const fileSha256 = sha256.digest()
487
- encFileWriteStream.end()
488
- originalFileStream?.end?.call(originalFileStream)
489
- stream.destroy()
490
-
491
- logger?.debug('prepared plain stream successfully')
492
-
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');
493
410
  return {
494
411
  mediaKey: undefined,
495
- originalFilePath,
496
- encFilePath,
497
- mac: undefined,
498
- fileEncSha256: undefined,
412
+ encWriteStream: buffer,
413
+ fileLength,
499
414
  fileSha256,
500
- fileLength
501
- }
415
+ fileEncSha256: undefined,
416
+ bodyPath,
417
+ didSaveToTmpPath
418
+ };
502
419
  }
503
420
  catch (error) {
504
- encFileWriteStream.destroy()
505
- originalFileStream?.destroy?.call(originalFileStream)
506
- sha256.destroy()
507
- stream.destroy()
508
- try {
509
- await promises.unlink(encFilePath)
510
- if (originalFilePath && didSaveToTmpPath) {
511
- await promises.unlink(originalFilePath)
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');
512
429
  }
513
- } catch (err) {
514
- logger?.error({ err }, 'failed deleting tmp files')
515
430
  }
516
- throw error
431
+ throw error;
517
432
  }
518
- }*/
519
-
433
+ };
434
+ exports.prepareStream = prepareStream;
520
435
  const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus } = {}) => {
521
- const { stream, type } = await getStream(media, opts)
436
+ const { stream, type } = await (0, exports.getStream)(media, opts);
522
437
 
523
438
  let finalStream = stream;
524
439
  if (mediaType === 'audio' && (isPtt === true || forceOpus === true)) {
525
440
  try {
526
- const buffer = await toBuffer(stream);
527
- const opusBuffer = await convertToOpusBuffer(buffer, logger);
528
- finalStream = toReadable(opusBuffer);
441
+ const buffer = await (0, exports.toBuffer)(stream);
442
+ const opusBuffer = await exports.convertToOpusBuffer(buffer, logger);
443
+ finalStream = (0, exports.toReadable)(opusBuffer);
529
444
  } catch (error) {
530
- if (isPtt) {
531
- throw error;
532
- }
533
- const { stream: newStream } = await getStream(media, opts);
445
+ const { stream: newStream } = await (0, exports.getStream)(media, opts);
534
446
  finalStream = newStream;
535
447
  }
536
448
  }
537
449
 
538
- logger?.debug('fetched media stream')
539
-
540
- const mediaKey = randomBytes(32)
541
- const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType)
542
- const encFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageID() + '-enc')
543
- const encFileWriteStream = createWriteStream(encFilePath)
544
-
545
- let originalFileStream;
546
- let originalFilePath
547
-
548
- if (saveOriginalFileIfRequired) {
549
- originalFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageID() + '-original')
550
- originalFileStream = createWriteStream(originalFilePath)
450
+ const mediaKey = Crypto.randomBytes(32);
451
+ const { cipherKey, iv, macKey } = getMediaKeys(mediaKey, mediaType);
452
+ const encWriteStream = new stream_1.Readable({ read: () => { } });
453
+ let bodyPath;
454
+ let writeStream;
455
+ let didSaveToTmpPath = false;
456
+
457
+ if (type === 'file') {
458
+ bodyPath = media.url;
551
459
  }
552
-
553
- let fileLength = 0
554
-
555
- const aes = createCipheriv('aes-256-cbc', cipherKey, iv)
556
- const hmac = createHmac('sha256', macKey).update(iv)
557
- const sha256Plain = createHash('sha256');
558
- const sha256Enc = createHash('sha256')
559
-
560
- const onChunk = async (buff) => {
561
- sha256Enc.update(buff)
562
- hmac.update(buff)
563
-
564
- // Handle backpressure: if write returns false, wait for drain
565
- if (!encFileWriteStream.write(buff)) {
566
- await once(encFileWriteStream, 'drain')
567
- }
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;
568
464
  }
569
-
465
+
466
+ let fileLength = 0;
467
+ const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv);
468
+ let hmac = Crypto.createHmac('sha256', macKey).update(iv);
469
+ let sha256Plain = Crypto.createHash('sha256');
470
+ let sha256Enc = Crypto.createHash('sha256');
471
+
570
472
  try {
571
- for await (const data of stream) {
572
- fileLength += data.length
573
-
574
- if (type === 'remote' &&
575
- opts?.maxContentLength &&
576
- fileLength + data.length > opts.maxContentLength) {
577
- throw new Boom(`content length exceeded when encrypting "${type}"`, {
473
+ for await (const data of finalStream) {
474
+ fileLength += data.length;
475
+ if (type === 'remote'
476
+ && (opts === null || opts === void 0 ? void 0 : opts.maxContentLength)
477
+ && fileLength + data.length > opts.maxContentLength) {
478
+ throw new boom_1.Boom(`content length exceeded when encrypting "${type}"`, {
578
479
  data: { media, type }
579
- })
480
+ });
580
481
  }
581
-
582
- if (originalFileStream) {
583
- if (!originalFileStream.write(data)) {
584
- await once(originalFileStream, 'drain')
482
+
483
+ sha256Plain = sha256Plain.update(data);
484
+ if (writeStream) {
485
+ if (!writeStream.write(data)) {
486
+ await (0, events_1.once)(writeStream, 'drain');
585
487
  }
586
488
  }
587
-
588
- sha256Plain.update(data)
589
-
590
- await onChunk(aes.update(data))
489
+ onChunk(aes.update(data));
591
490
  }
592
-
593
- await onChunk(aes.final())
594
- const mac = hmac.digest().slice(0, 10)
595
-
596
- sha256Enc.update(mac)
597
-
598
- const fileSha256 = sha256Plain.digest()
599
- const fileEncSha256 = sha256Enc.digest()
600
-
601
- encFileWriteStream.write(mac)
602
-
603
- const encFinishPromise = once(encFileWriteStream, 'finish')
604
- const originalFinishPromise = originalFileStream ? once(originalFileStream, 'finish') : Promise.resolve()
605
-
606
- encFileWriteStream.end()
607
- originalFileStream?.end?.()
608
- stream.destroy()
609
-
610
- // Wait for write streams to fully flush to disk
611
- // This helps reduce memory pressure by allowing OS to release buffers
612
- await encFinishPromise
613
- await originalFinishPromise
614
-
615
- logger?.debug('encrypted data successfully')
616
-
491
+
492
+ onChunk(aes.final());
493
+ const mac = hmac.digest().slice(0, 10);
494
+ sha256Enc = sha256Enc.update(mac);
495
+ const fileSha256 = sha256Plain.digest();
496
+ const fileEncSha256 = sha256Enc.digest();
497
+
498
+ encWriteStream.push(mac);
499
+ encWriteStream.push(null);
500
+ writeStream === null || writeStream === void 0 ? void 0 : writeStream.end();
501
+ finalStream.destroy();
502
+
617
503
  return {
618
504
  mediaKey,
619
- originalFilePath,
620
- encFilePath,
505
+ encWriteStream,
506
+ bodyPath,
621
507
  mac,
622
508
  fileEncSha256,
623
509
  fileSha256,
624
- fileLength
625
- }
510
+ fileLength,
511
+ didSaveToTmpPath
512
+ };
626
513
  }
627
514
  catch (error) {
628
- // destroy all streams with error
629
- encFileWriteStream.destroy()
630
- originalFileStream?.destroy?.()
631
- aes.destroy()
632
- hmac.destroy()
633
- sha256Plain.destroy()
634
- sha256Enc.destroy()
635
- stream.destroy()
636
-
637
- try {
638
- await promises.unlink(encFilePath)
639
-
640
- if (originalFilePath) {
641
- await promises.unlink(originalFilePath)
515
+ encWriteStream.destroy();
516
+ writeStream === null || writeStream === void 0 ? void 0 : writeStream.destroy();
517
+ aes.destroy();
518
+ hmac.destroy();
519
+ sha256Plain.destroy();
520
+ sha256Enc.destroy();
521
+ finalStream.destroy();
522
+
523
+ if (didSaveToTmpPath) {
524
+ try {
525
+ await fs_1.promises.unlink(bodyPath);
526
+ }
527
+ catch (err) {
642
528
  }
643
529
  }
644
- catch (err) {
645
- logger?.error({ err }, 'failed deleting tmp files')
646
- }
647
- throw error
530
+ throw error;
648
531
  }
649
- }
650
-
651
- const DEF_HOST = 'mmg.whatsapp.net'
652
-
653
- const AES_CHUNK_SIZE = 16
654
-
532
+
533
+ function onChunk(buff) {
534
+ sha256Enc = sha256Enc.update(buff);
535
+ hmac = hmac.update(buff);
536
+ encWriteStream.push(buff);
537
+ }
538
+ };
539
+ exports.encryptedStream = encryptedStream;
540
+ const DEF_HOST = 'mmg.whatsapp.net';
541
+ const AES_CHUNK_SIZE = 16;
655
542
  const toSmallestChunkSize = (num) => {
656
- return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE
657
- }
658
- const getUrlFromDirectPath = (directPath) => `https://${DEF_HOST}${directPath}`
659
-
660
- const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
661
- const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/')
662
- const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath)
663
-
664
- if (!downloadUrl) {
665
- throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 })
666
- }
667
-
668
- const keys = await getMediaKeys(mediaKey, type)
669
- return downloadEncryptedContent(downloadUrl, keys, opts)
670
- }
671
-
543
+ return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE;
544
+ };
545
+ const getUrlFromDirectPath = (directPath) => `https://${DEF_HOST}${directPath}`;
546
+ exports.getUrlFromDirectPath = getUrlFromDirectPath;
547
+ const downloadContentFromMessage = ({ mediaKey, directPath, url }, type, opts = {}) => {
548
+ const downloadUrl = url || (0, exports.getUrlFromDirectPath)(directPath);
549
+ const keys = getMediaKeys(mediaKey, type);
550
+ return (0, exports.downloadEncryptedContent)(downloadUrl, keys, opts);
551
+ };
552
+ exports.downloadContentFromMessage = downloadContentFromMessage;
672
553
  /**
673
554
  * Decrypts and downloads an AES256-CBC encrypted file given the keys.
674
555
  * Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
675
556
  * */
676
557
  const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
677
- let bytesFetched = 0
678
- let startChunk = 0
679
- let firstBlockIsIV = false
680
-
558
+ let bytesFetched = 0;
559
+ let startChunk = 0;
560
+ let firstBlockIsIV = false;
681
561
  // if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV
682
562
  if (startByte) {
683
- const chunk = toSmallestChunkSize(startByte || 0)
684
-
563
+ const chunk = toSmallestChunkSize(startByte || 0);
685
564
  if (chunk) {
686
- startChunk = chunk - AES_CHUNK_SIZE
687
- bytesFetched = chunk
688
- firstBlockIsIV = true
565
+ startChunk = chunk - AES_CHUNK_SIZE;
566
+ bytesFetched = chunk;
567
+ firstBlockIsIV = true;
689
568
  }
690
569
  }
691
-
692
- const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined
693
- const headersInit = options?.headers ? options.headers : undefined
570
+ const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined;
694
571
  const headers = {
695
- ...(headersInit
696
- ? Array.isArray(headersInit)
697
- ? Object.fromEntries(headersInit)
698
- : headersInit
699
- : {}),
700
- Origin: DEFAULT_ORIGIN
701
- }
702
-
572
+ ...(options === null || options === void 0 ? void 0 : options.headers) || {},
573
+ Origin: Defaults_1.DEFAULT_ORIGIN,
574
+ };
703
575
  if (startChunk || endChunk) {
704
- headers.Range = `bytes=${startChunk}-`
705
-
576
+ headers.Range = `bytes=${startChunk}-`;
706
577
  if (endChunk) {
707
- headers.Range += endChunk
578
+ headers.Range += endChunk;
708
579
  }
709
580
  }
710
-
711
581
  // download the message
712
- const fetched = await getHttpStream(downloadUrl, {
713
- ...(options || {}),
714
- headers
715
- })
716
-
717
- let remainingBytes = Buffer.from([])
718
- let aes
719
-
582
+ const fetched = await (0, exports.getHttpStream)(downloadUrl, {
583
+ ...options || {},
584
+ headers,
585
+ maxBodyLength: Infinity,
586
+ maxContentLength: Infinity,
587
+ });
588
+ let remainingBytes = Buffer.from([]);
589
+ let aes;
720
590
  const pushBytes = (bytes, push) => {
721
591
  if (startByte || endByte) {
722
- const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0)
723
- const end = bytesFetched + bytes.length < endByte ? undefined : Math.max(endByte - bytesFetched, 0)
724
-
725
- push(bytes.slice(start, end))
726
- bytesFetched += bytes.length
592
+ const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0);
593
+ const end = bytesFetched + bytes.length < endByte ? undefined : Math.max(endByte - bytesFetched, 0);
594
+ push(bytes.slice(start, end));
595
+ bytesFetched += bytes.length;
727
596
  }
728
597
  else {
729
- push(bytes)
598
+ push(bytes);
730
599
  }
731
- }
732
-
733
- const output = new Transform({
600
+ };
601
+ const output = new stream_1.Transform({
734
602
  transform(chunk, _, callback) {
735
- let data = Buffer.concat([remainingBytes, chunk])
736
-
737
- const decryptLength = toSmallestChunkSize(data.length)
738
-
739
- remainingBytes = data.slice(decryptLength)
740
- data = data.slice(0, decryptLength)
741
-
603
+ let data = Buffer.concat([remainingBytes, chunk]);
604
+ const decryptLength = toSmallestChunkSize(data.length);
605
+ remainingBytes = data.slice(decryptLength);
606
+ data = data.slice(0, decryptLength);
742
607
  if (!aes) {
743
- let ivValue = iv
744
-
608
+ let ivValue = iv;
745
609
  if (firstBlockIsIV) {
746
- ivValue = data.slice(0, AES_CHUNK_SIZE)
747
- data = data.slice(AES_CHUNK_SIZE)
610
+ ivValue = data.slice(0, AES_CHUNK_SIZE);
611
+ data = data.slice(AES_CHUNK_SIZE);
748
612
  }
749
-
750
- aes = createDecipheriv('aes-256-cbc', cipherKey, ivValue)
751
-
613
+ aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue);
752
614
  // if an end byte that is not EOF is specified
753
615
  // stop auto padding (PKCS7) -- otherwise throws an error for decryption
754
616
  if (endByte) {
755
- aes.setAutoPadding(false)
617
+ aes.setAutoPadding(false);
756
618
  }
757
619
  }
758
620
  try {
759
- pushBytes(aes.update(data), b => this.push(b))
760
- callback()
621
+ pushBytes(aes.update(data), b => this.push(b));
622
+ callback();
761
623
  }
762
624
  catch (error) {
763
- callback(error)
625
+ callback(error);
764
626
  }
765
627
  },
766
628
  final(callback) {
767
629
  try {
768
- pushBytes(aes.final(), b => this.push(b))
769
- callback()
630
+ pushBytes(aes.final(), b => this.push(b));
631
+ callback();
770
632
  }
771
633
  catch (error) {
772
- callback(error)
634
+ callback(error);
773
635
  }
774
- }
775
- })
776
-
777
- return fetched.pipe(output, { end: true })
778
- }
779
-
636
+ },
637
+ });
638
+ return fetched.pipe(output, { end: true });
639
+ };
640
+ exports.downloadEncryptedContent = downloadEncryptedContent;
780
641
  function extensionForMediaMessage(message) {
781
- const getExtension = (mimetype) => mimetype.split('')[0].split('/')[1]
782
- const type = Object.keys(message)[0]
783
- let extension
642
+ const getExtension = (mimetype) => mimetype.split(';')[0].split('/')[1];
643
+ const type = Object.keys(message)[0];
644
+ let extension;
784
645
  if (type === 'locationMessage' ||
785
646
  type === 'liveLocationMessage' ||
786
647
  type === 'productMessage') {
787
- extension = '.jpeg'
788
- }
789
- else {
790
- const messageContent = message[type]
791
- extension = getExtension(messageContent.mimetype)
792
- }
793
- return extension
794
- }
795
-
796
- const isNodeRuntime = () => {
797
- return (typeof process !== 'undefined' &&
798
- process.versions?.node !== null &&
799
- typeof process.versions.bun === 'undefined' &&
800
- typeof globalThis.Deno === 'undefined')
801
- }
802
-
803
- const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent }, redirectCount = 0) => {
804
- if (redirectCount > 5) {
805
- throw new Error('Too many redirects')
806
- }
807
-
808
- const parsedUrl = new URL(url)
809
- const httpModule = parsedUrl.protocol === 'https:' ? require('https') : require('http')
810
-
811
- // Get file size for Content-Length header (required for Node.js streaming)
812
- const fileStats = await promises.stat(filePath)
813
- const fileSize = fileStats.size
814
-
815
- return new Promise((resolve, reject) => {
816
- const req = httpModule.request({
817
- hostname: parsedUrl.hostname,
818
- port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
819
- path: parsedUrl.pathname + parsedUrl.search,
820
- method: 'POST',
821
- headers: {
822
- ...headers,
823
- 'Content-Length': fileSize
824
- },
825
- agent,
826
- timeout: timeoutMs
827
- }, res => {
828
- // Handle redirects (3xx)
829
- if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
830
- res.resume() // Consume response to free resources
831
-
832
- const newUrl = new URL(res.headers.location, url).toString()
833
-
834
- resolve(uploadWithNodeHttp({
835
- url: newUrl,
836
- filePath,
837
- headers,
838
- timeoutMs,
839
- agent
840
- }, redirectCount + 1))
841
- return
842
- }
843
-
844
- let body = ''
845
-
846
- res.on('data', chunk => (body += chunk))
847
- res.on('end', () => {
848
- try {
849
- resolve(JSON.parse(body))
850
- }
851
- catch {
852
- resolve(undefined)
853
- }
854
- })
855
- })
856
-
857
- req.on('error', reject)
858
- req.on('timeout', () => {
859
- req.destroy()
860
- reject(new Error('Upload timeout'))
861
- })
862
-
863
- const stream = createReadStream(filePath)
864
-
865
- stream.pipe(req)
866
- stream.on('error', err => {
867
- req.destroy()
868
- reject(err)
869
- })
870
- })
871
- }
872
-
873
- const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) => {
874
- // Convert Node.js Readable to Web ReadableStream
875
- const nodeStream = createReadStream(filePath)
876
- const webStream = Readable.toWeb(nodeStream)
877
- const response = await fetch(url, {
878
- dispatcher: agent,
879
- method: 'POST',
880
- body: webStream,
881
- headers,
882
- duplex: 'half',
883
- signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
884
- })
885
-
886
- try {
887
- return (await response.json())
888
- }
889
- catch {
890
- return undefined
891
- }
892
- }
893
-
894
- /**
895
- * Uploads media to WhatsApp servers.
896
- *
897
- * ## Why we have two upload implementations:
898
- *
899
- * Node.js's native `fetch` (powered by undici) has a known bug where it buffers
900
- * the entire request body in memory before sending, even when using streams.
901
- * This causes memory issues with large files (e.g., 1GB file = 1GB+ memory usage).
902
- * See: https://github.com/nodejs/undici/issues/4058
903
- *
904
- * Other runtimes (Bun, Deno, browsers) correctly stream the request body without
905
- * buffering, so we can use the web-standard Fetch API there.
906
- *
907
- * ## Future considerations:
908
- * Once the undici bug is fixed, we can simplify this to use only the Fetch API
909
- * across all runtimes. Monitor the GitHub issue for updates.
910
- */
911
- const uploadMedia = async (params, logger) => {
912
- if (isNodeRuntime()) {
913
- logger?.debug('Using Node.js https module for upload (avoids undici buffering bug)')
914
- return uploadWithNodeHttp(params)
648
+ extension = '.jpeg';
915
649
  }
916
650
  else {
917
- logger?.debug('Using web-standard Fetch API for upload');
918
- return uploadWithFetch(params)
651
+ const messageContent = message[type];
652
+ extension = getExtension(messageContent.mimetype);
919
653
  }
654
+ return extension;
920
655
  }
921
-
656
+ exports.extensionForMediaMessage = extensionForMediaMessage;
922
657
  const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
923
- return async (filePath, { mediaType, fileEncSha256B64, timeoutMs }) => {
658
+ return async (stream, { mediaType, fileEncSha256B64, newsletter, timeoutMs }) => {
659
+ var _a, _b;
660
+ const { default: axios } = await import('axios');
924
661
  // send a query JSON to obtain the url & auth token to upload our media
925
- let uploadInfo = await refreshMediaConn(false)
926
- let urls
927
-
928
- const hosts = [...customUploadHosts, ...uploadInfo.hosts]
929
-
930
- fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64)
931
-
932
- // Prepare common headers
933
- const customHeaders = (() => {
934
- const hdrs = options?.headers;
935
- if (!hdrs)
936
- return {};
937
- return Array.isArray(hdrs) ? Object.fromEntries(hdrs) : hdrs
938
- })()
939
-
940
- const headers = {
941
- ...customHeaders,
942
- 'Content-Type': 'application/octet-stream',
943
- Origin: DEFAULT_ORIGIN
662
+ let uploadInfo = await refreshMediaConn(false);
663
+ let urls;
664
+ const hosts = [...customUploadHosts, ...uploadInfo.hosts];
665
+ const chunks = [];
666
+ if (!Buffer.isBuffer(stream)) {
667
+ for await (const chunk of stream) {
668
+ chunks.push(chunk);
669
+ }
944
670
  }
945
-
946
- for (const { hostname } of hosts) {
947
- logger.debug(`uploading to "${hostname}"`)
948
-
949
- const auth = encodeURIComponent(uploadInfo.auth)
950
- const url = `https://${hostname}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
951
-
952
- let result
953
-
671
+ const reqBody = Buffer.isBuffer(stream) ? stream : Buffer.concat(chunks);
672
+ fileEncSha256B64 = (0, exports.encodeBase64EncodedStringForUpload)(fileEncSha256B64);
673
+ let media = Defaults_1.MEDIA_PATH_MAP[mediaType];
674
+ if (newsletter) {
675
+ media = media === null || media === void 0 ? void 0 : media.replace('/mms/', '/newsletter/newsletter-');
676
+ }
677
+ for (const { hostname, maxContentLengthBytes } of hosts) {
678
+ logger.debug(`uploading to "${hostname}"`);
679
+ const auth = encodeURIComponent(uploadInfo.auth); // the auth token
680
+ const url = `https://${hostname}${media}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;
681
+ let result;
954
682
  try {
955
- result = await uploadMedia({
956
- url,
957
- filePath,
958
- headers,
959
- timeoutMs,
960
- agent: fetchAgent
961
- }, logger);
962
- if (result?.url || result?.direct_path) {
683
+ if (maxContentLengthBytes && reqBody.length > maxContentLengthBytes) {
684
+ throw new boom_1.Boom(`Body too large for "${hostname}"`, { statusCode: 413 });
685
+ }
686
+ const body = await axios.post(url, reqBody, {
687
+ ...options,
688
+ headers: {
689
+ ...options.headers || {},
690
+ 'Content-Type': 'application/octet-stream',
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)) {
963
701
  urls = {
964
702
  mediaUrl: result.url,
965
703
  directPath: result.direct_path,
966
- meta_hmac: result.meta_hmac,
967
- fbid: result.fbid,
968
- ts: result.ts
969
- }
970
- break
704
+ handle: result.handle
705
+ };
706
+ break;
971
707
  }
972
708
  else {
973
- uploadInfo = await refreshMediaConn(true)
974
- throw new Error(`upload failed, reason: ${JSON.stringify(result)}`)
709
+ uploadInfo = await refreshMediaConn(true);
710
+ throw new Error(`upload failed, reason: ${JSON.stringify(result)}`);
975
711
  }
976
712
  }
977
713
  catch (error) {
978
- const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname
979
- logger.warn({ trace: error?.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`)
714
+ if (axios.isAxiosError(error)) {
715
+ result = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data;
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...'}`);
980
719
  }
981
720
  }
982
-
983
721
  if (!urls) {
984
- throw new Boom('Media upload failed on all hosts', { statusCode: 500 })
722
+ throw new boom_1.Boom('Media upload failed on all hosts', { statusCode: 500 });
985
723
  }
986
-
987
- return urls
988
- }
989
- }
990
-
724
+ return urls;
725
+ };
726
+ };
727
+ exports.getWAUploadToServer = getWAUploadToServer;
991
728
  const getMediaRetryKey = (mediaKey) => {
992
- return hkdf(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' })
993
- }
729
+ return (0, crypto_1.hkdf)(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' });
730
+ };
994
731
  /**
995
732
  * Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
996
733
  */
997
- const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
998
- const recp = { stanzaId: key.id }
999
- const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
1000
- const iv = randomBytes(12)
1001
- const retryKey = await getMediaRetryKey(mediaKey)
1002
- const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id))
734
+ const encryptMediaRetryRequest = (key, mediaKey, meId) => {
735
+ const recp = { stanzaId: key.id };
736
+ const recpBuffer = WAProto_1.proto.ServerErrorReceipt.encode(recp).finish();
737
+ const iv = Crypto.randomBytes(12);
738
+ const retryKey = getMediaRetryKey(mediaKey);
739
+ const ciphertext = (0, crypto_1.aesEncryptGCM)(recpBuffer, retryKey, iv, Buffer.from(key.id));
1003
740
  const req = {
1004
741
  tag: 'receipt',
1005
742
  attrs: {
1006
743
  id: key.id,
1007
- to: jidNormalizedUser(meId),
744
+ to: (0, WABinary_1.jidNormalizedUser)(meId),
1008
745
  type: 'server-error'
1009
746
  },
1010
747
  content: [
@@ -1023,17 +760,18 @@ const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
1023
760
  tag: 'rmr',
1024
761
  attrs: {
1025
762
  jid: key.remoteJid,
1026
- from_me: (!!key.fromMe).toString(),
763
+ 'from_me': (!!key.fromMe).toString(),
764
+ // @ts-ignore
1027
765
  participant: key.participant || undefined
1028
766
  }
1029
767
  }
1030
768
  ]
1031
- }
1032
- return req
1033
- }
1034
-
769
+ };
770
+ return req;
771
+ };
772
+ exports.encryptMediaRetryRequest = encryptMediaRetryRequest;
1035
773
  const decodeMediaRetryNode = (node) => {
1036
- const rmrNode = getBinaryNodeChild(node, 'rmr')
774
+ const rmrNode = (0, WABinary_1.getBinaryNodeChild)(node, 'rmr');
1037
775
  const event = {
1038
776
  key: {
1039
777
  id: node.attrs.id,
@@ -1041,69 +779,41 @@ const decodeMediaRetryNode = (node) => {
1041
779
  fromMe: rmrNode.attrs.from_me === 'true',
1042
780
  participant: rmrNode.attrs.participant
1043
781
  }
1044
- }
1045
- const errorNode = getBinaryNodeChild(node, 'error')
782
+ };
783
+ const errorNode = (0, WABinary_1.getBinaryNodeChild)(node, 'error');
1046
784
  if (errorNode) {
1047
- const errorCode = +errorNode.attrs.code
1048
- event.error = new Boom(`Failed to re-upload media (${errorCode})`, { data: errorNode.attrs, statusCode: getStatusCodeForMediaRetry(errorCode) })
785
+ const errorCode = +errorNode.attrs.code;
786
+ event.error = new boom_1.Boom(`Failed to re-upload media (${errorCode})`, { data: errorNode.attrs, statusCode: (0, exports.getStatusCodeForMediaRetry)(errorCode) });
1049
787
  }
1050
788
  else {
1051
- const encryptedInfoNode = getBinaryNodeChild(node, 'encrypt')
1052
- const ciphertext = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_p')
1053
- const iv = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_iv')
789
+ const encryptedInfoNode = (0, WABinary_1.getBinaryNodeChild)(node, 'encrypt');
790
+ const ciphertext = (0, WABinary_1.getBinaryNodeChildBuffer)(encryptedInfoNode, 'enc_p');
791
+ const iv = (0, WABinary_1.getBinaryNodeChildBuffer)(encryptedInfoNode, 'enc_iv');
1054
792
  if (ciphertext && iv) {
1055
- event.media = { ciphertext, iv }
793
+ event.media = { ciphertext, iv };
1056
794
  }
1057
795
  else {
1058
- event.error = new Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 })
1059
- }
1060
- }
1061
- return event
1062
- }
1063
-
1064
- const decryptMediaRetryData = async ({ ciphertext, iv }, mediaKey, msgId) => {
1065
- const retryKey = await getMediaRetryKey(mediaKey)
1066
- const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId))
1067
- return proto.MediaRetryNotification.decode(plaintext)
1068
- }
1069
-
1070
- const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code]
1071
-
796
+ event.error = new boom_1.Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 });
797
+ }
798
+ }
799
+ return event;
800
+ };
801
+ exports.decodeMediaRetryNode = decodeMediaRetryNode;
802
+ const decryptMediaRetryData = ({ ciphertext, iv }, mediaKey, msgId) => {
803
+ const retryKey = getMediaRetryKey(mediaKey);
804
+ const plaintext = (0, crypto_1.aesDecryptGCM)(ciphertext, retryKey, iv, Buffer.from(msgId));
805
+ return WAProto_1.proto.MediaRetryNotification.decode(plaintext);
806
+ };
807
+ exports.decryptMediaRetryData = decryptMediaRetryData;
808
+ const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code];
809
+ exports.getStatusCodeForMediaRetry = getStatusCodeForMediaRetry;
1072
810
  const MEDIA_RETRY_STATUS_MAP = {
1073
- [proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
1074
- [proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
1075
- [proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
1076
- [proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418,
1077
- }
1078
-
1079
- module.exports = {
1080
- hkdfInfoKey,
1081
- getMediaKeys,
1082
- extractVideoThumb,
1083
- extractImageThumb,
1084
- encodeBase64EncodedStringForUpload,
1085
- generateProfilePicture,
1086
- mediaMessageSHA256B64,
1087
- getAudioDuration,
1088
- getAudioWaveform,
1089
- toReadable,
1090
- toBuffer,
1091
- getStream,
1092
- generateThumbnail,
1093
- getHttpStream,
1094
- //prepareStream,
1095
- encryptedStream,
1096
- getUrlFromDirectPath,
1097
- downloadContentFromMessage,
1098
- downloadEncryptedContent,
1099
- extensionForMediaMessage,
1100
- uploadWithNodeHttp,
1101
- getRawMediaUploadData,
1102
- getWAUploadToServer,
1103
- getMediaRetryKey,
1104
- encryptMediaRetryRequest,
1105
- decodeMediaRetryNode,
1106
- decryptMediaRetryData,
1107
- getStatusCodeForMediaRetry,
1108
- MEDIA_RETRY_STATUS_MAP
811
+ [WAProto_1.proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
812
+ [WAProto_1.proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
813
+ [WAProto_1.proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
814
+ [WAProto_1.proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418,
815
+ };
816
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
817
+ function __importStar(arg0) {
818
+ throw new Error('Function not implemented.');
1109
819
  }