@fazzcode/baileys 2.5.3 → 2.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.MD CHANGED
@@ -40,7 +40,31 @@
40
40
 
41
41
  ## 🔧 Requirement
42
42
 
43
- - **Node.js** >= 20.0.0 (direkomendasikan LTS terbaru)
43
+ ### Versi Node.js yang Didukung
44
+
45
+ | Versi Node.js | Status | Catatan |
46
+ |---|---|---|
47
+ | < 20.0.0 | ❌ Tidak didukung | ESM & fitur JS modern yang dipakai library ini butuh minimal v20 |
48
+ | 20.x (LTS) | ✅ Didukung | Minimum version, paling stabil untuk produksi |
49
+ | 22.x (LTS) | ✅ Didukung | Direkomendasikan |
50
+ | 23.x | ✅ Didukung | |
51
+ | 24.x (LTS) | ✅ Didukung | |
52
+ | 25.x / 26.x dan seterusnya | ✅ Didukung | Versi Current/terbaru, selama masih kompatibel dengan ESM Node standar |
53
+
54
+ Singkatnya: **selama Node.js >= 20.0.0, baik versi LTS maupun Current/terbaru, library ini bisa dipakai.** Tidak ada batas atas versi — cukup pastikan minimal v20 karena `package.json` menggunakan `"type": "module"` dan sintaks ES2022+.
55
+
56
+ Cek versi Node.js yang terpasang:
57
+
58
+ ```bash
59
+ node -v
60
+ ```
61
+
62
+ Kalau masih di bawah v20 (misalnya v16/v18), update Node.js dulu sebelum install package ini. Di Termux (Android) bisa update dengan:
63
+
64
+ ```bash
65
+ pkg update && pkg upgrade nodejs
66
+ ```
67
+
44
68
  - **npm** atau **yarn**
45
69
  - **WhatsApp Account** (hanya 1 account per instance)
46
70
  - **System Requirements**:
@@ -66,17 +90,16 @@ yarn add @fazzcode/baileys
66
90
 
67
91
  ### Dependency Tambahan (Optional)
68
92
 
93
+ `jimp` sudah otomatis terpasang sebagai dependency utama (dibutuhkan untuk crop gambar profil), jadi tidak perlu diinstal manual. Yang masih opsional hanya:
94
+
69
95
  ```bash
70
96
  # Untuk link preview
71
97
  npm install link-preview-js
72
98
 
73
- # Untuk image processing
74
- npm install jimp@latest
75
-
76
- # Untuk audio processing
99
+ # Untuk audio processing (durasi audio, dsb)
77
100
  npm install audio-decode
78
101
 
79
- # Logging
102
+ # Logging (pino sudah ikut terpasang otomatis, opsional kalau mau versi/konfigurasi sendiri)
80
103
  npm install pino
81
104
  ```
82
105
 
@@ -879,6 +902,41 @@ await sock.groupParticipantsUpdate(
879
902
 
880
903
  ## ⚠️ Error Handling
881
904
 
905
+ ### Troubleshooting Instalasi (Cannot find module)
906
+
907
+ Kalau muncul error seperti ini saat menjalankan bot:
908
+
909
+ ```
910
+ Error: Cannot find module '@protobufjs/inquire'
911
+ Require stack:
912
+ - node_modules/libsignal-xeuka/node_modules/protobufjs/...
913
+ ```
914
+
915
+ atau error `Cannot find module` lain yang nyebut folder di dalam `node_modules/<package-lain>/node_modules/...`, itu artinya **instalasi `node_modules` tidak lengkap/corrupt** — biasanya karena koneksi putus saat `npm install`, atau (sering terjadi di Termux/Android) penyimpanan eksternal (`/storage/emulated/0/...`) yang bermasalah saat npm menulis banyak file kecil bersarang.
916
+
917
+ **Solusi (install ulang bersih):**
918
+
919
+ ```bash
920
+ # 1. Hapus instalasi yang rusak
921
+ rm -rf node_modules package-lock.json
922
+
923
+ # 2. Bersihkan cache npm
924
+ npm cache clean --force
925
+
926
+ # 3. Install ulang
927
+ npm install
928
+ ```
929
+
930
+ **Khusus pengguna Termux/Android**, kalau masih sering gagal di tengah jalan, coba install di internal storage Termux dulu (lebih stabil daripada `/storage/emulated/0/...`), baru pindahkan foldernya kalau perlu:
931
+
932
+ ```bash
933
+ cd ~ # masuk ke home Termux, bukan /storage/emulated/0
934
+ mkdir -p project && cd project
935
+ npm install @fazzcode/baileys
936
+ ```
937
+
938
+ Pastikan juga koneksi internet stabil selama proses install karena beberapa dependency (terutama yang diambil dari GitHub seperti `libsignal-xeuka`) butuh proses clone/download yang agak besar.
939
+
882
940
  ### Basic Error Handling
883
941
 
884
942
  ```javascript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fazzcode/baileys",
3
- "version": "2.5.3",
3
+ "version": "2.5.5",
4
4
  "description": "Websocket Whatsapp API for Node.js",
5
5
  "keywords": [
6
6
  "whatsapp",
@@ -35,7 +35,6 @@
35
35
  "changelog:update": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
36
36
  "changelog:preview": "conventional-changelog -p angular -u",
37
37
  "changelog:last": "conventional-changelog -p angular -r 2",
38
- "gen:protobuf": "sh WAProto/GenerateStatics.sh",
39
38
  "example": "node --inspect Example/example.js",
40
39
  "lint": "eslint src --ext .js",
41
40
  "lint:fix": "yarn lint --fix",
@@ -43,35 +42,25 @@
43
42
  "test": "jest"
44
43
  },
45
44
  "dependencies": {
46
- "@whiskeysockets/eslint-config": "github:whiskeysockets/eslint-config",
47
45
  "@cacheable/node-cache": "*",
48
- "gradient-string": "2.0.2",
49
- "libsignal-xeuka": "github:tskiofc/libsignal",
50
- "moment-timezone": "*",
46
+ "libsignal-xeuka": "github:tskiofc/libsignal#250643990ae58c9e16dcfb34b1c8feaf7da41b06",
51
47
  "music-metadata": "*",
52
48
  "cache-manager": "*",
53
49
  "async-mutex": "*",
54
50
  "@hapi/boom": "*",
55
51
  "jimp": "*",
56
- "pbjs": "^0.0.14",
57
- "protobufjs": "*",
52
+ "protobufjs": "7.5.0",
58
53
  "lru-cache": "*",
59
- "readline": "*",
60
54
  "p-queue": "*",
61
- "lodash": "*",
62
- "figlet": "*",
63
55
  "axios": "*",
64
- "path": "*",
56
+ "long": "*",
65
57
  "pino": "*",
66
- "ws": "*",
67
- "fs": "*",
68
- "os": "*"
58
+ "ws": "*"
69
59
  },
70
60
  "devDependencies": {
61
+ "@whiskeysockets/eslint-config": "github:whiskeysockets/eslint-config",
71
62
  "conventional-changelog-cli": "*",
72
63
  "link-preview-js": "*",
73
- "protobufjs-cli": "*",
74
- "cache-manager": "*",
75
64
  "@types/jest": "*",
76
65
  "@types/node": "*",
77
66
  "release-it": "*",
@@ -83,16 +72,12 @@
83
72
  },
84
73
  "peerDependencies": {
85
74
  "link-preview-js": "*",
86
- "audio-decode": "*",
87
- "jimp": ">=0.16.0"
75
+ "audio-decode": "*"
88
76
  },
89
77
  "peerDependenciesMeta": {
90
78
  "audio-decode": {
91
79
  "optional": true
92
80
  },
93
- "jimp": {
94
- "optional": true
95
- },
96
81
  "link-preview-js": {
97
82
  "optional": true
98
83
  }
@@ -1,33 +0,0 @@
1
- //=======================================================//
2
- export var XWAPaths;
3
- (function (XWAPaths) {
4
- XWAPaths["xwa2_newsletter_create"] = "xwa2_newsletter_create";
5
- XWAPaths["xwa2_newsletter_subscribers"] = "xwa2_newsletter_subscribers";
6
- XWAPaths["xwa2_newsletter_view"] = "xwa2_newsletter_view";
7
- XWAPaths["xwa2_newsletter_metadata"] = "xwa2_newsletter";
8
- XWAPaths["xwa2_newsletter_admin_count"] = "xwa2_newsletter_admin";
9
- XWAPaths["xwa2_newsletter_mute_v2"] = "xwa2_newsletter_mute_v2";
10
- XWAPaths["xwa2_newsletter_unmute_v2"] = "xwa2_newsletter_unmute_v2";
11
- XWAPaths["xwa2_newsletter_follow"] = "xwa2_newsletter_follow";
12
- XWAPaths["xwa2_newsletter_unfollow"] = "xwa2_newsletter_unfollow";
13
- XWAPaths["xwa2_newsletter_change_owner"] = "xwa2_newsletter_change_owner";
14
- XWAPaths["xwa2_newsletter_demote"] = "xwa2_newsletter_demote";
15
- XWAPaths["xwa2_newsletter_delete_v2"] = "xwa2_newsletter_delete_v2";
16
- })(XWAPaths || (XWAPaths = {}));
17
- //=======================================================//
18
- export var QueryIds;
19
- (function (QueryIds) {
20
- QueryIds["CREATE"] = "8823471724422422";
21
- QueryIds["UPDATE_METADATA"] = "24250201037901610";
22
- QueryIds["METADATA"] = "6620195908089573";
23
- QueryIds["SUBSCRIBERS"] = "6388546374527196";
24
- QueryIds["FOLLOW"] = "7871414976211147";
25
- QueryIds["UNFOLLOW"] = "7238632346214362";
26
- QueryIds["MUTE"] = "29766401636284406";
27
- QueryIds["UNMUTE"] = "9864994326891137";
28
- QueryIds["ADMIN_COUNT"] = "7130823597031706";
29
- QueryIds["CHANGE_OWNER"] = "7341777602580933";
30
- QueryIds["DEMOTE"] = "6551828931592903";
31
- QueryIds["DELETE"] = "30062808666639665";
32
- })(QueryIds || (QueryIds = {}));
33
- //=======================================================//
@@ -1,601 +0,0 @@
1
- //=======================================================//
2
- import { getBinaryNodeChild, getBinaryNodeChildBuffer, jidNormalizedUser } from "../WABinary/index.js";
3
- import { DEFAULT_ORIGIN, MEDIA_HKDF_KEY_MAPPING, MEDIA_PATH_MAP } from "../Defaults/index.js";
4
- import { createReadStream, createWriteStream, promises as fs, WriteStream } from "fs";
5
- import { aesDecryptGCM, aesEncryptGCM, hkdf } from "./crypto.js";
6
- import { generateMessageIDV2 } from "./generics.js";
7
- import { proto } from "../../WAProto/index.js";
8
- import { Readable, Transform } from "stream";
9
- import { exec } from "child_process";
10
- import { Boom } from "@hapi/boom";
11
- import * as Crypto from "crypto";
12
- import { once } from "events";
13
- import { tmpdir } from "os";
14
- import { join } from "path";
15
- import { URL } from "url";
16
- import Jimp from "jimp";
17
- //=======================================================//
18
- const getTmpFilesDirectory = () => tmpdir();
19
- //=======================================================//
20
- export const hkdfInfoKey = (type) => {
21
- const hkdfInfo = MEDIA_HKDF_KEY_MAPPING[type];
22
- return `WhatsApp ${hkdfInfo} Keys`;
23
- };
24
- //=======================================================//
25
- export const getRawMediaUploadData = async (media, mediaType, logger) => {
26
- const { stream } = await getStream(media);
27
- logger?.debug("got stream for raw upload");
28
- const hasher = Crypto.createHash("sha256");
29
- const filePath = join(tmpdir(), mediaType + generateMessageIDV2());
30
- const fileWriteStream = createWriteStream(filePath);
31
- let fileLength = 0;
32
- try {
33
- for await (const data of stream) {
34
- fileLength += data.length;
35
- hasher.update(data);
36
- if (!fileWriteStream.write(data)) {
37
- await once(fileWriteStream, "drain");
38
- }
39
- }
40
- fileWriteStream.end();
41
- await once(fileWriteStream, "finish");
42
- stream.destroy();
43
- const fileSha256 = hasher.digest();
44
- logger?.debug("hashed data for raw upload");
45
- return {
46
- filePath: filePath,
47
- fileSha256,
48
- fileLength
49
- };
50
- }
51
- catch (error) {
52
- fileWriteStream.destroy();
53
- stream.destroy();
54
- try {
55
- await fs.unlink(filePath);
56
- }
57
- catch {
58
- }
59
- throw error;
60
- }
61
- };
62
- //=======================================================//
63
- export async function getMediaKeys(buffer, mediaType) {
64
- if (!buffer) {
65
- throw new Boom("Cannot derive from empty media key");
66
- }
67
- if (typeof buffer === "string") {
68
- buffer = Buffer.from(buffer.replace("data:;base64,", ""), "base64");
69
- }
70
- const expandedMediaKey = await hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) });
71
- return {
72
- iv: expandedMediaKey.slice(0, 16),
73
- cipherKey: expandedMediaKey.slice(16, 48),
74
- macKey: expandedMediaKey.slice(48, 80)
75
- };
76
- }
77
- //=======================================================//
78
- const extractVideoThumb = async (path, destPath, time, size) => new Promise((resolve, reject) => {
79
- const cmd = `ffmpeg -ss ${time} -i ${path} -y -vf scale=${size.width}:-1 -vframes 1 -f image2 ${destPath}`;
80
- exec(cmd, err => {
81
- if (err) {
82
- reject(err);
83
- }
84
- else {
85
- resolve();
86
- }
87
- });
88
- });
89
- //=======================================================//
90
- export const extractImageThumb = async (bufferOrFilePath, width = 32) => {
91
- if (bufferOrFilePath instanceof Readable) {
92
- bufferOrFilePath = await toBuffer(bufferOrFilePath);
93
- }
94
- const image = await Jimp.read(bufferOrFilePath);
95
- const dimensions = { width: image.bitmap.width, height: image.bitmap.height };
96
- const resized = image.resize(width, Jimp.RESIZE_BILINEAR).quality(50);
97
- const buffer = await resized.getBufferAsync(Jimp.MIME_JPEG);
98
- return { buffer, original: dimensions };
99
- };
100
- //=======================================================//
101
- export const encodeBase64EncodedStringForUpload = (b64) => encodeURIComponent(b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/\=+$/, ""));
102
- export const generateProfilePicture = async (mediaUpload, dimensions) => {
103
- let buffer;
104
- const { width: w = 640, height: h = 640 } = dimensions || {};
105
- if (Buffer.isBuffer(mediaUpload)) {
106
- buffer = mediaUpload;
107
- } else {
108
- const { stream } = await getStream(mediaUpload);
109
- buffer = await toBuffer(stream);
110
- }
111
- const jimp = await Jimp.read(buffer);
112
- const min = Math.min(jimp.bitmap.width, jimp.bitmap.height);
113
- const cropped = jimp.crop(0, 0, min, min);
114
- const resized = cropped.resize(w, h, Jimp.RESIZE_BILINEAR).quality(50);
115
- const img = await resized.getBufferAsync(Jimp.MIME_JPEG);
116
- return { img };
117
- };
118
- //=======================================================//
119
- export const mediaMessageSHA256B64 = (message) => {
120
- const media = Object.values(message)[0];
121
- return media?.fileSha256 && Buffer.from(media.fileSha256).toString("base64");
122
- };
123
- //=======================================================//
124
- export async function getAudioDuration(buffer) {
125
- const musicMetadata = await import("music-metadata");
126
- let metadata;
127
- const options = {
128
- duration: true
129
- };
130
- if (Buffer.isBuffer(buffer)) {
131
- metadata = await musicMetadata.parseBuffer(buffer, undefined, options);
132
- }
133
- else if (typeof buffer === "string") {
134
- metadata = await musicMetadata.parseFile(buffer, options);
135
- }
136
- else {
137
- metadata = await musicMetadata.parseStream(buffer, undefined, options);
138
- }
139
- return metadata.format.duration;
140
- }
141
- //=======================================================//
142
- export async function getAudioWaveform(buffer, logger) {
143
- try {
144
- const { default: decoder } = await import("audio-decode");
145
- let audioData;
146
- if (Buffer.isBuffer(buffer)) {
147
- audioData = buffer;
148
- }
149
- else if (typeof buffer === "string") {
150
- const rStream = createReadStream(buffer);
151
- audioData = await toBuffer(rStream);
152
- }
153
- else {
154
- audioData = await toBuffer(buffer);
155
- }
156
- const audioBuffer = await decoder(audioData);
157
- const rawData = audioBuffer.getChannelData(0);
158
- const samples = 64;
159
- const blockSize = Math.floor(rawData.length / samples);
160
- const filteredData = [];
161
- for (let i = 0; i < samples; i++) {
162
- const blockStart = blockSize * i;
163
- let sum = 0;
164
- for (let j = 0; j < blockSize; j++) {
165
- sum = sum + Math.abs(rawData[blockStart + j]);
166
- }
167
- filteredData.push(sum / blockSize);
168
- }
169
- const multiplier = Math.pow(Math.max(...filteredData), -1);
170
- const normalizedData = filteredData.map(n => n * multiplier);
171
- const waveform = new Uint8Array(normalizedData.map(n => Math.floor(100 * n)));
172
- return waveform;
173
- }
174
- catch (e) {
175
- logger?.debug("Failed to generate waveform: " + e);
176
- }
177
- }
178
- //=======================================================//
179
- export const toReadable = (buffer) => {
180
- const readable = new Readable({ read: () => { } });
181
- readable.push(buffer);
182
- readable.push(null);
183
- return readable;
184
- };
185
- //=======================================================//
186
- export const toBuffer = async (stream) => {
187
- const chunks = [];
188
- for await (const chunk of stream) {
189
- chunks.push(chunk);
190
- }
191
- stream.destroy();
192
- return Buffer.concat(chunks);
193
- };
194
- //=======================================================//
195
- export const getStream = async (item, opts) => {
196
- if (Buffer.isBuffer(item)) {
197
- return { stream: toReadable(item), type: "buffer" };
198
- }
199
- if ("stream" in item) {
200
- return { stream: item.stream, type: "readable" };
201
- }
202
- const urlStr = item.url.toString();
203
- if (urlStr.startsWith("data:")) {
204
- const buffer = Buffer.from(urlStr.split(",")[1], "base64");
205
- return { stream: toReadable(buffer), type: "buffer" };
206
- }
207
- if (urlStr.startsWith("http://") || urlStr.startsWith("https://")) {
208
- return { stream: await getHttpStream(item.url, opts), type: "remote" };
209
- }
210
- return { stream: createReadStream(item.url), type: "file" };
211
- };
212
- //=======================================================//
213
- export async function generateThumbnail(file, mediaType, options) {
214
- let thumbnail;
215
- let originalImageDimensions;
216
- if (mediaType === "image") {
217
- const { buffer, original } = await extractImageThumb(file);
218
- thumbnail = buffer.toString("base64");
219
- if (original.width && original.height) {
220
- originalImageDimensions = {
221
- width: original.width,
222
- height: original.height
223
- };
224
- }
225
- }
226
- else if (mediaType === "video") {
227
- const imgFilename = join(getTmpFilesDirectory(), generateMessageIDV2() + ".jpg");
228
- try {
229
- await extractVideoThumb(file, imgFilename, "00:00:00", { width: 32, height: 32 });
230
- const buff = await fs.readFile(imgFilename);
231
- thumbnail = buff.toString("base64");
232
- await fs.unlink(imgFilename);
233
- }
234
- catch (err) {
235
- options.logger?.debug("could not generate video thumb: " + err);
236
- }
237
- }
238
- return {
239
- thumbnail,
240
- originalImageDimensions
241
- };
242
- }
243
- //=======================================================//
244
- export const getHttpStream = async (url, options = {}) => {
245
- const response = await fetch(url.toString(), {
246
- dispatcher: options.dispatcher,
247
- method: "GET",
248
- headers: options.headers
249
- });
250
- if (!response.ok) {
251
- throw new Boom(`Failed to fetch stream from ${url}`, { statusCode: response.status, data: { url } });
252
- }
253
- return Readable.fromWeb(response.body);
254
- };
255
- //=======================================================//
256
- export const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
257
- const { stream, type } = await getStream(media, opts);
258
- logger?.debug("fetched media stream");
259
- const mediaKey = Crypto.randomBytes(32);
260
- const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType);
261
- const encFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2() + "-enc");
262
- const encFileWriteStream = createWriteStream(encFilePath);
263
- let originalFileStream;
264
- let originalFilePath;
265
- if (saveOriginalFileIfRequired) {
266
- originalFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2() + "-original");
267
- originalFileStream = createWriteStream(originalFilePath);
268
- }
269
- let fileLength = 0;
270
- const aes = Crypto.createCipheriv("aes-256-cbc", cipherKey, iv);
271
- const hmac = Crypto.createHmac("sha256", macKey).update(iv);
272
- const sha256Plain = Crypto.createHash("sha256");
273
- const sha256Enc = Crypto.createHash("sha256");
274
- const onChunk = (buff) => {
275
- sha256Enc.update(buff);
276
- hmac.update(buff);
277
- encFileWriteStream.write(buff);
278
- };
279
- try {
280
- for await (const data of stream) {
281
- fileLength += data.length;
282
- if (type === "remote" &&
283
- opts?.maxContentLength &&
284
- fileLength + data.length > opts.maxContentLength) {
285
- throw new Boom(`content length exceeded when encrypting "${type}"`, {
286
- data: { media, type }
287
- });
288
- }
289
- if (originalFileStream) {
290
- if (!originalFileStream.write(data)) {
291
- await once(originalFileStream, "drain");
292
- }
293
- }
294
- sha256Plain.update(data);
295
- onChunk(aes.update(data));
296
- }
297
- onChunk(aes.final());
298
- const mac = hmac.digest().slice(0, 10);
299
- sha256Enc.update(mac);
300
- const fileSha256 = sha256Plain.digest();
301
- const fileEncSha256 = sha256Enc.digest();
302
- encFileWriteStream.write(mac);
303
- encFileWriteStream.end();
304
- originalFileStream?.end?.();
305
- stream.destroy();
306
- logger?.debug("encrypted data successfully");
307
- return {
308
- mediaKey,
309
- originalFilePath,
310
- encFilePath,
311
- mac,
312
- fileEncSha256,
313
- fileSha256,
314
- fileLength
315
- };
316
- }
317
- catch (error) {
318
- encFileWriteStream.destroy();
319
- originalFileStream?.destroy?.();
320
- aes.destroy();
321
- hmac.destroy();
322
- sha256Plain.destroy();
323
- sha256Enc.destroy();
324
- stream.destroy();
325
- try {
326
- await fs.unlink(encFilePath);
327
- if (originalFilePath) {
328
- await fs.unlink(originalFilePath);
329
- }
330
- }
331
- catch (err) {
332
- logger?.error({ err }, "failed deleting tmp files");
333
- }
334
- throw error;
335
- }
336
- };
337
- //=======================================================//
338
- const DEF_HOST = "mmg.whatsapp.net";
339
- const AES_CHUNK_SIZE = 16;
340
- const toSmallestChunkSize = (num) => {
341
- return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE;
342
- };
343
- //=======================================================//
344
- export const getUrlFromDirectPath = (directPath) => `https://${DEF_HOST}${directPath}`;
345
- export const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
346
- const isValidMediaUrl = url?.startsWith("https://mmg.whatsapp.net/");
347
- const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath);
348
- if (!downloadUrl) {
349
- throw new Boom("No valid media URL or directPath present in message", { statusCode: 400 });
350
- }
351
- const keys = await getMediaKeys(mediaKey, type);
352
- return downloadEncryptedContent(downloadUrl, keys, opts);
353
- };
354
- //=======================================================//
355
- export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
356
- let bytesFetched = 0;
357
- let startChunk = 0;
358
- let firstBlockIsIV = false;
359
- if (startByte) {
360
- const chunk = toSmallestChunkSize(startByte || 0);
361
- if (chunk) {
362
- startChunk = chunk - AES_CHUNK_SIZE;
363
- bytesFetched = chunk;
364
- firstBlockIsIV = true;
365
- }
366
- }
367
- const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined;
368
- const headersInit = options?.headers ? options.headers : undefined;
369
- const headers = {
370
- ...(headersInit
371
- ? Array.isArray(headersInit)
372
- ? Object.fromEntries(headersInit)
373
- : headersInit
374
- : {}),
375
- Origin: DEFAULT_ORIGIN
376
- };
377
- if (startChunk || endChunk) {
378
- headers.Range = `bytes=${startChunk}-`;
379
- if (endChunk) {
380
- headers.Range += endChunk;
381
- }
382
- }
383
- const fetched = await getHttpStream(downloadUrl, {
384
- ...(options || {}),
385
- headers
386
- });
387
- let remainingBytes = Buffer.from([]);
388
- let aes;
389
- const pushBytes = (bytes, push) => {
390
- if (startByte || endByte) {
391
- const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0);
392
- const end = bytesFetched + bytes.length < endByte ? undefined : Math.max(endByte - bytesFetched, 0);
393
- push(bytes.slice(start, end));
394
- bytesFetched += bytes.length;
395
- }
396
- else {
397
- push(bytes);
398
- }
399
- };
400
- const output = new Transform({
401
- transform(chunk, _, callback) {
402
- let data = Buffer.concat([remainingBytes, chunk]);
403
- const decryptLength = toSmallestChunkSize(data.length);
404
- remainingBytes = data.slice(decryptLength);
405
- data = data.slice(0, decryptLength);
406
- if (!aes) {
407
- let ivValue = iv;
408
- if (firstBlockIsIV) {
409
- ivValue = data.slice(0, AES_CHUNK_SIZE);
410
- data = data.slice(AES_CHUNK_SIZE);
411
- }
412
- aes = Crypto.createDecipheriv("aes-256-cbc", cipherKey, ivValue);
413
- if (endByte) {
414
- aes.setAutoPadding(false);
415
- }
416
- }
417
- try {
418
- pushBytes(aes.update(data), b => this.push(b));
419
- callback();
420
- }
421
- catch (error) {
422
- callback(error);
423
- }
424
- },
425
- final(callback) {
426
- try {
427
- pushBytes(aes.final(), b => this.push(b));
428
- callback();
429
- }
430
- catch (error) {
431
- callback(error);
432
- }
433
- }
434
- });
435
- return fetched.pipe(output, { end: true });
436
- };
437
- //=======================================================//
438
- export function extensionForMediaMessage(message) {
439
- const getExtension = (mimetype) => mimetype.split(";")[0]?.split("/")[1];
440
- const type = Object.keys(message)[0];
441
- let extension;
442
- if (type === "locationMessage" || type === "liveLocationMessage" || type === "productMessage") {
443
- extension = ".jpeg";
444
- }
445
- else {
446
- const messageContent = message[type];
447
- extension = getExtension(messageContent.mimetype);
448
- }
449
- return extension;
450
- }
451
- //=======================================================//
452
- export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
453
- return async (filePath, { mediaType, fileEncSha256B64, timeoutMs }) => {
454
- let uploadInfo = await refreshMediaConn(false);
455
- let urls;
456
- const hosts = [...customUploadHosts, ...uploadInfo.hosts];
457
- fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64);
458
- for (const { hostname } of hosts) {
459
- logger.debug(`uploading to "${hostname}"`);
460
- const auth = encodeURIComponent(uploadInfo.auth);
461
- const url = `https://${hostname}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;
462
- let result;
463
- try {
464
- const stream = createReadStream(filePath);
465
- const response = await fetch(url, {
466
- dispatcher: fetchAgent,
467
- method: "POST",
468
- body: stream,
469
- headers: {
470
- ...(() => {
471
- const hdrs = options?.headers;
472
- if (!hdrs)
473
- return {};
474
- return Array.isArray(hdrs) ? Object.fromEntries(hdrs) : hdrs;
475
- })(),
476
- "Content-Type": "application/octet-stream",
477
- Origin: DEFAULT_ORIGIN
478
- },
479
- duplex: "half",
480
- signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
481
- });
482
- let parsed = undefined;
483
- try {
484
- parsed = await response.json();
485
- }
486
- catch {
487
- parsed = undefined;
488
- }
489
- result = parsed;
490
- if (result?.url || result?.directPath) {
491
- urls = {
492
- mediaUrl: result.url,
493
- directPath: result.direct_path,
494
- meta_hmac: result.meta_hmac,
495
- fbid: result.fbid,
496
- ts: result.ts
497
- };
498
- break;
499
- }
500
- else {
501
- uploadInfo = await refreshMediaConn(true);
502
- throw new Error(`upload failed, reason: ${JSON.stringify(result)}`);
503
- }
504
- }
505
- catch (error) {
506
- const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname;
507
- logger.warn({ trace: error?.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? "" : ", retrying..."}`);
508
- }
509
- }
510
- if (!urls) {
511
- throw new Boom("Media upload failed on all hosts", { statusCode: 500 });
512
- }
513
- return urls;
514
- };
515
- };
516
- //=======================================================//
517
- const getMediaRetryKey = (mediaKey) => {
518
- return hkdf(mediaKey, 32, { info: "WhatsApp Media Retry Notification" });
519
- };
520
- //=======================================================//
521
- export const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
522
- const recp = { stanzaId: key.id };
523
- const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish();
524
- const iv = Crypto.randomBytes(12);
525
- const retryKey = await getMediaRetryKey(mediaKey);
526
- const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id));
527
- const req = {
528
- tag: "receipt",
529
- attrs: {
530
- id: key.id,
531
- to: jidNormalizedUser(meId),
532
- type: "server-error"
533
- },
534
- content: [
535
- {
536
- tag: "encrypt",
537
- attrs: {},
538
- content: [
539
- { tag: "enc_p", attrs: {}, content: ciphertext },
540
- { tag: "enc_iv", attrs: {}, content: iv }
541
- ]
542
- },
543
- {
544
- tag: "rmr",
545
- attrs: {
546
- jid: key.remoteJid,
547
- from_me: (!!key.fromMe).toString(),
548
- participant: key.participant || undefined
549
- }
550
- }
551
- ]
552
- };
553
- return req;
554
- };
555
- //=======================================================//
556
- export const decodeMediaRetryNode = (node) => {
557
- const rmrNode = getBinaryNodeChild(node, "rmr");
558
- const event = {
559
- key: {
560
- id: node.attrs.id,
561
- remoteJid: rmrNode.attrs.jid,
562
- fromMe: rmrNode.attrs.from_me === "true",
563
- participant: rmrNode.attrs.participant
564
- }
565
- };
566
- const errorNode = getBinaryNodeChild(node, "error");
567
- if (errorNode) {
568
- const errorCode = +errorNode.attrs.code;
569
- event.error = new Boom(`Failed to re-upload media (${errorCode})`, {
570
- data: errorNode.attrs,
571
- statusCode: getStatusCodeForMediaRetry(errorCode)
572
- });
573
- }
574
- else {
575
- const encryptedInfoNode = getBinaryNodeChild(node, "encrypt");
576
- const ciphertext = getBinaryNodeChildBuffer(encryptedInfoNode, "enc_p");
577
- const iv = getBinaryNodeChildBuffer(encryptedInfoNode, "enc_iv");
578
- if (ciphertext && iv) {
579
- event.media = { ciphertext, iv };
580
- }
581
- else {
582
- event.error = new Boom("Failed to re-upload media (missing ciphertext)", { statusCode: 404 });
583
- }
584
- }
585
- return event;
586
- };
587
- //=======================================================//
588
- export const decryptMediaRetryData = async ({ ciphertext, iv }, mediaKey, msgId) => {
589
- const retryKey = await getMediaRetryKey(mediaKey);
590
- const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId));
591
- return proto.MediaRetryNotification.decode(plaintext);
592
- };
593
- //=======================================================//
594
- export const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code];
595
- const MEDIA_RETRY_STATUS_MAP = {
596
- [proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
597
- [proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
598
- [proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
599
- [proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
600
- };
601
- //=======================================================//