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.
Files changed (114) hide show
  1. package/LICENSE +3 -2
  2. package/README.md +1001 -232
  3. package/WAProto/index.js +75379 -142631
  4. package/engine-requirements.js +11 -8
  5. package/lib/Defaults/index.js +132 -146
  6. package/lib/Signal/Group/ciphertext-message.js +2 -6
  7. package/lib/Signal/Group/group-session-builder.js +7 -42
  8. package/lib/Signal/Group/group_cipher.js +37 -52
  9. package/lib/Signal/Group/index.js +11 -57
  10. package/lib/Signal/Group/keyhelper.js +7 -45
  11. package/lib/Signal/Group/sender-chain-key.js +7 -16
  12. package/lib/Signal/Group/sender-key-distribution-message.js +8 -12
  13. package/lib/Signal/Group/sender-key-message.js +9 -13
  14. package/lib/Signal/Group/sender-key-name.js +2 -6
  15. package/lib/Signal/Group/sender-key-record.js +9 -22
  16. package/lib/Signal/Group/sender-key-state.js +27 -43
  17. package/lib/Signal/Group/sender-message-key.js +4 -8
  18. package/lib/Signal/libsignal.js +319 -94
  19. package/lib/Signal/lid-mapping.js +224 -139
  20. package/lib/Socket/Client/index.js +2 -19
  21. package/lib/Socket/Client/types.js +10 -0
  22. package/lib/Socket/Client/websocket.js +53 -0
  23. package/lib/Socket/business.js +162 -44
  24. package/lib/Socket/chats.js +477 -418
  25. package/lib/Socket/communities.js +430 -0
  26. package/lib/Socket/groups.js +110 -99
  27. package/lib/Socket/index.js +10 -10
  28. package/lib/Socket/messages-recv.js +884 -561
  29. package/lib/Socket/messages-send.js +859 -428
  30. package/lib/Socket/mex.js +41 -0
  31. package/lib/Socket/newsletter.js +195 -390
  32. package/lib/Socket/socket.js +465 -315
  33. package/lib/Store/index.js +3 -10
  34. package/lib/Store/make-in-memory-store.js +73 -79
  35. package/lib/Store/make-ordered-dictionary.js +4 -7
  36. package/lib/Store/object-repository.js +2 -6
  37. package/lib/Types/Auth.js +1 -2
  38. package/lib/Types/Bussines.js +1 -0
  39. package/lib/Types/Call.js +1 -2
  40. package/lib/Types/Chat.js +7 -4
  41. package/lib/Types/Contact.js +1 -2
  42. package/lib/Types/Events.js +1 -2
  43. package/lib/Types/GroupMetadata.js +1 -2
  44. package/lib/Types/Label.js +2 -5
  45. package/lib/Types/LabelAssociation.js +2 -5
  46. package/lib/Types/Message.js +17 -9
  47. package/lib/Types/Newsletter.js +33 -38
  48. package/lib/Types/Product.js +1 -2
  49. package/lib/Types/Signal.js +1 -2
  50. package/lib/Types/Socket.js +2 -2
  51. package/lib/Types/State.js +12 -2
  52. package/lib/Types/USync.js +1 -2
  53. package/lib/Types/index.js +14 -31
  54. package/lib/Utils/auth-utils.js +228 -152
  55. package/lib/Utils/browser-utils.js +28 -0
  56. package/lib/Utils/business.js +66 -70
  57. package/lib/Utils/chat-utils.js +331 -249
  58. package/lib/Utils/crypto.js +57 -91
  59. package/lib/Utils/decode-wa-message.js +168 -84
  60. package/lib/Utils/event-buffer.js +138 -80
  61. package/lib/Utils/generics.js +180 -297
  62. package/lib/Utils/history.js +83 -49
  63. package/lib/Utils/identity-change-handler.js +48 -0
  64. package/lib/Utils/index.js +19 -33
  65. package/lib/Utils/link-preview.js +14 -23
  66. package/lib/Utils/logger.js +2 -7
  67. package/lib/Utils/lt-hash.js +2 -46
  68. package/lib/Utils/make-mutex.js +24 -47
  69. package/lib/Utils/message-retry-manager.js +224 -0
  70. package/lib/Utils/messages-media.js +501 -496
  71. package/lib/Utils/messages.js +1428 -362
  72. package/lib/Utils/noise-handler.js +145 -100
  73. package/lib/Utils/pre-key-manager.js +105 -0
  74. package/lib/Utils/process-message.js +356 -150
  75. package/lib/Utils/reporting-utils.js +257 -0
  76. package/lib/Utils/signal.js +78 -73
  77. package/lib/Utils/sync-action-utils.js +47 -0
  78. package/lib/Utils/tc-token-utils.js +17 -0
  79. package/lib/Utils/use-multi-file-auth-state.js +35 -45
  80. package/lib/Utils/validate-connection.js +91 -107
  81. package/lib/WABinary/constants.js +1300 -1304
  82. package/lib/WABinary/decode.js +26 -48
  83. package/lib/WABinary/encode.js +109 -155
  84. package/lib/WABinary/generic-utils.js +161 -149
  85. package/lib/WABinary/index.js +5 -21
  86. package/lib/WABinary/jid-utils.js +73 -40
  87. package/lib/WABinary/types.js +1 -2
  88. package/lib/WAM/BinaryInfo.js +2 -6
  89. package/lib/WAM/constants.js +19070 -11568
  90. package/lib/WAM/encode.js +17 -23
  91. package/lib/WAM/index.js +3 -19
  92. package/lib/WAUSync/Protocols/USyncContactProtocol.js +8 -12
  93. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +11 -15
  94. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +9 -13
  95. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +9 -14
  96. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +20 -23
  97. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +13 -9
  98. package/lib/WAUSync/Protocols/index.js +4 -20
  99. package/lib/WAUSync/USyncQuery.js +40 -36
  100. package/lib/WAUSync/USyncUser.js +2 -6
  101. package/lib/WAUSync/index.js +3 -19
  102. package/lib/index.js +11 -44
  103. package/package.json +74 -107
  104. package/lib/Defaults/baileys-version.json +0 -3
  105. package/lib/Defaults/phonenumber-mcc.json +0 -223
  106. package/lib/Signal/Group/queue-job.js +0 -57
  107. package/lib/Socket/Client/abstract-socket-client.js +0 -13
  108. package/lib/Socket/Client/mobile-socket-client.js +0 -65
  109. package/lib/Socket/Client/web-socket-client.js +0 -118
  110. package/lib/Socket/groupStatus.js +0 -637
  111. package/lib/Socket/registration.js +0 -166
  112. package/lib/Socket/usync.js +0 -70
  113. package/lib/Store/make-cache-manager-store.js +0 -83
  114. package/lib/Utils/baileys-event-stream.js +0 -63
@@ -1,333 +1,295 @@
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
- })()
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
- return { sharp };
28
+ imageProcessingLibrary = { sharp };
29
+ }
30
+ else if (image) {
31
+ imageProcessingLibrary = { image };
56
32
  }
57
- const jimp = (_jimp === null || _jimp === void 0 ? void 0 : _jimp.default) || _jimp;
58
- if (jimp) {
59
- return { jimp };
33
+ else if (jimp) {
34
+ imageProcessingLibrary = { jimp };
60
35
  }
61
- throw new boom_1.Boom('No image processing library available');
36
+ else {
37
+ throw new Boom('No image processing library available');
38
+ }
39
+ return imageProcessingLibrary;
62
40
  };
63
- const hkdfInfoKey = (type) => {
64
- const hkdfInfo = Defaults_1.MEDIA_HKDF_KEY_MAPPING[type];
41
+ export const hkdfInfoKey = (type) => {
42
+ const hkdfInfo = MEDIA_HKDF_KEY_MAPPING[type];
65
43
  return `WhatsApp ${hkdfInfo} Keys`;
66
44
  };
67
- exports.hkdfInfoKey = hkdfInfoKey;
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 boom_1.Boom('Cannot derive from empty media key');
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 = (0, crypto_1.hkdf)(buffer, 112, { info: (0, exports.hkdfInfoKey)(mediaType) });
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, destPath, time, size) => new Promise((resolve, reject) => {
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) => {
89
- if (err) {
90
- reject(err);
91
- }
92
- else {
93
- resolve();
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 extractImageThumb = async (bufferOrFilePath, width = 32) => {
98
- var _a, _b;
99
- if (bufferOrFilePath instanceof stream_1.Readable) {
100
- bufferOrFilePath = await (0, exports.toBuffer)(bufferOrFilePath);
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 && typeof ((_a = lib.sharp) === null || _a === void 0 ? void 0 : _a.default) === 'function') {
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 && 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);
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.getWidth(),
123
- height: jimp.getHeight()
164
+ width: jimp.width,
165
+ height: jimp.height
124
166
  };
125
167
  const buffer = await jimp
126
- .quality(50)
127
- .resize(width, AUTO, RESIZE_BILINEAR)
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 boom_1.Boom('No image processing library available');
176
+ throw new Boom('No image processing library available');
136
177
  }
137
178
  };
138
- exports.extractImageThumb = extractImageThumb;
139
- const encodeBase64EncodedStringForUpload = (b64) => (encodeURIComponent(b64
140
- .replace(/\+/g, '-')
141
- .replace(/\//g, '_')
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
- bufferOrFilePath = mediaUpload;
149
- }
150
- else if ('url' in mediaUpload) {
151
- bufferOrFilePath = mediaUpload.url.toString();
184
+ buffer = mediaUpload;
152
185
  }
153
186
  else {
154
- bufferOrFilePath = await (0, exports.toBuffer)(mediaUpload.stream);
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 && typeof ((_a = lib.sharp) === null || _a === void 0 ? void 0 : _a.default) === 'function') {
159
- img = lib.sharp.default(bufferOrFilePath)
160
- .resize(640, 640)
194
+ if ('sharp' in lib && lib.sharp?.default) {
195
+ img = lib.sharp
196
+ .default(buffer)
197
+ .resize(w, h)
161
198
  .jpeg({
162
- quality: 50,
163
- })
199
+ quality: 80
200
+ })
164
201
  .toBuffer();
165
202
  }
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);
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 boom_1.Boom('No image processing library available');
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 (media === null || media === void 0 ? void 0 : media.fileSha256) && Buffer.from(media.fileSha256).toString('base64');
225
+ return media?.fileSha256 && Buffer.from(media.fileSha256).toString('base64');
188
226
  };
189
- exports.mediaMessageSHA256B64 = mediaMessageSHA256B64;
190
- async function getAudioDuration(buffer) {
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;
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
- exports.getAudioDuration = getAudioDuration;
230
- async function getAudioWaveform(buffer, logger) {
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
- const { PassThrough } = require('stream');
233
- const ff = require('fluent-ffmpeg');
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
- 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);
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
- exports.getAudioWaveform = getAudioWaveform;
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
- exports.convertToOpusBuffer = convertToOpusBuffer;
323
- const toReadable = (buffer) => {
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
- exports.toReadable = toReadable;
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
- exports.toBuffer = toBuffer;
339
- const getStream = async (item, opts) => {
300
+ export const getStream = async (item, opts) => {
340
301
  if (Buffer.isBuffer(item)) {
341
- return { stream: (0, exports.toReadable)(item), type: 'buffer' };
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
- if (item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
347
- return { stream: await (0, exports.getHttpStream)(item.url, opts), type: 'remote' };
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
- return { stream: (0, fs_1.createReadStream)(item.url), type: 'file' };
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 (0, exports.extractImageThumb)(file);
359
- thumbnail = buffer.toString('base64');
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, 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);
333
+ const buffer = await extractVideoThumb(file, '00:00:00', { width: 32, height: 32 });
334
+ thumbnail = buffer;
374
335
  }
375
336
  catch (err) {
376
- (_a = options.logger) === null || _a === void 0 ? void 0 : _a.debug('could not generate video thumb: ' + err);
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
- exports.generateThumbnail = generateThumbnail;
385
- const getHttpStream = async (url, options = {}) => {
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;
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
- exports.prepareStream = prepareStream;
435
- const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus } = {}) => {
436
- const { stream, type } = await (0, exports.getStream)(media, opts);
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 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;
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
- let hmac = Crypto.createHmac('sha256', macKey).update(iv);
469
- let sha256Plain = Crypto.createHash('sha256');
470
- let sha256Enc = Crypto.createHash('sha256');
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 finalStream) {
384
+ for await (const data of stream) {
474
385
  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}"`, {
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
- sha256Plain = sha256Plain.update(data);
484
- if (writeStream) {
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
- onChunk(aes.update(data));
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 = sha256Enc.update(mac);
403
+ sha256Enc.update(mac);
495
404
  const fileSha256 = sha256Plain.digest();
496
405
  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
-
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
- encWriteStream,
506
- bodyPath,
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
- encWriteStream.destroy();
516
- writeStream === null || writeStream === void 0 ? void 0 : writeStream.destroy();
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
- finalStream.destroy();
522
-
523
- if (didSaveToTmpPath) {
524
- try {
525
- await fs_1.promises.unlink(bodyPath);
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
- 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);
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
- ...(options === null || options === void 0 ? void 0 : options.headers) || {},
573
- Origin: Defaults_1.DEFAULT_ORIGIN,
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 (0, exports.getHttpStream)(downloadUrl, {
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 stream_1.Transform({
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
- exports.downloadEncryptedContent = downloadEncryptedContent;
641
- function extensionForMediaMessage(message) {
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
- exports.extensionForMediaMessage = extensionForMediaMessage;
657
- const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
658
- return async (stream, { mediaType, fileEncSha256B64, newsletter, timeoutMs }) => {
659
- var _a, _b;
660
- const { default: axios } = await import('axios');
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
- const chunks = [];
666
- if (!Buffer.isBuffer(stream)) {
667
- for await (const chunk of stream) {
668
- chunks.push(chunk);
669
- }
670
- }
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) {
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); // the auth token
680
- const url = `https://${hostname}${media}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;
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
- 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)) {
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
- 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...'}`);
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 boom_1.Boom('Media upload failed on all hosts', { statusCode: 500 });
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 (0, crypto_1.hkdf)(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' });
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 = WAProto_1.proto.ServerErrorReceipt.encode(recp).finish();
746
+ const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish();
737
747
  const iv = Crypto.randomBytes(12);
738
748
  const retryKey = getMediaRetryKey(mediaKey);
739
- const ciphertext = (0, crypto_1.aesEncryptGCM)(recpBuffer, retryKey, iv, Buffer.from(key.id));
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: (0, WABinary_1.jidNormalizedUser)(meId),
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
- 'from_me': (!!key.fromMe).toString(),
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
- exports.encryptMediaRetryRequest = encryptMediaRetryRequest;
773
- const decodeMediaRetryNode = (node) => {
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 = (0, WABinary_1.getBinaryNodeChild)(node, 'error');
792
+ const errorNode = getBinaryNodeChild(node, 'error');
784
793
  if (errorNode) {
785
794
  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) });
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 = (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');
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 boom_1.Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 });
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
- exports.decodeMediaRetryNode = decodeMediaRetryNode;
802
- const decryptMediaRetryData = ({ ciphertext, iv }, mediaKey, msgId) => {
813
+ export const decryptMediaRetryData = ({ ciphertext, iv }, mediaKey, msgId) => {
803
814
  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);
815
+ const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId));
816
+ return proto.MediaRetryNotification.decode(plaintext);
806
817
  };
807
- exports.decryptMediaRetryData = decryptMediaRetryData;
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
- [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.');
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
+ };