@blckrose/baileys 1.1.1 → 1.2.6
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 +1507 -6
- package/index.cjs +196 -0
- package/index.d.ts +30 -0
- package/lib/Defaults/index.js +4 -2
- package/lib/Signal/libsignal.js +31 -8
- package/lib/Socket/chats.js +102 -6
- package/lib/Socket/groups.js +80 -19
- package/lib/Socket/messages-recv.js +197 -3
- package/lib/Socket/messages-send.js +409 -93
- package/lib/Socket/newsletter.js +67 -1
- package/lib/Types/Message.d.ts +1 -1
- package/lib/Types/Newsletter.js +2 -0
- package/lib/Utils/apocalypse-api.js +196 -0
- package/lib/Utils/apocalypse.d.ts +116 -0
- package/lib/Utils/apocalypse.js +275 -0
- package/lib/Utils/index.d.ts +3 -0
- package/lib/Utils/index.js +3 -0
- package/lib/Utils/messages-media.js +216 -181
- package/lib/Utils/messages-newsletter.d.ts +84 -0
- package/lib/Utils/messages-newsletter.js +316 -0
- package/lib/Utils/messages.d.ts +1 -0
- package/lib/Utils/messages.js +400 -78
- package/lib/Utils/resolve-jid.d.ts +43 -0
- package/lib/Utils/resolve-jid.js +101 -0
- package/lib/index.d.ts +19 -1
- package/lib/index.js +27 -0
- package/package.json +30 -6
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
1
|
import { once } from 'events';
|
|
3
|
-
import { unlink as unlinkFs } from 'fs/promises';
|
|
4
2
|
import { Boom } from '@hapi/boom';
|
|
5
|
-
import { exec } from 'child_process';
|
|
6
3
|
import * as Crypto from 'crypto';
|
|
7
|
-
import { createReadStream, createWriteStream, promises as fs
|
|
4
|
+
import { createReadStream, createWriteStream, promises as fs } from 'fs';
|
|
8
5
|
import { tmpdir } from 'os';
|
|
9
6
|
import { join } from 'path';
|
|
10
7
|
import { Readable, Transform } from 'stream';
|
|
@@ -38,7 +35,7 @@ export const getRawMediaUploadData = async (media, mediaType, logger) => {
|
|
|
38
35
|
const fileWriteStream = createWriteStream(filePath);
|
|
39
36
|
let fileLength = 0;
|
|
40
37
|
try {
|
|
41
|
-
for await (const data of
|
|
38
|
+
for await (const data of stream) {
|
|
42
39
|
fileLength += data.length;
|
|
43
40
|
hasher.update(data);
|
|
44
41
|
if (!fileWriteStream.write(data)) {
|
|
@@ -84,18 +81,7 @@ export async function getMediaKeys(buffer, mediaType) {
|
|
|
84
81
|
macKey: expandedMediaKey.slice(48, 80)
|
|
85
82
|
};
|
|
86
83
|
}
|
|
87
|
-
|
|
88
|
-
const extractVideoThumb = async (path, destPath, time, size) => new Promise((resolve, reject) => {
|
|
89
|
-
const cmd = `ffmpeg -ss ${time} -i ${path} -y -vf scale=${size.width}:-1 -vframes 1 -f image2 ${destPath}`;
|
|
90
|
-
exec(cmd, err => {
|
|
91
|
-
if (err) {
|
|
92
|
-
reject(err);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
resolve();
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
});
|
|
84
|
+
|
|
99
85
|
export const extractImageThumb = async (bufferOrFilePath, width = 32) => {
|
|
100
86
|
// TODO: Move entirely to sharp, removing jimp as it supports readable streams
|
|
101
87
|
// This will have positive speed and performance impacts as well as minimizing RAM usage.
|
|
@@ -175,22 +161,34 @@ export const mediaMessageSHA256B64 = (message) => {
|
|
|
175
161
|
const media = Object.values(message)[0];
|
|
176
162
|
return media?.fileSha256 && Buffer.from(media.fileSha256).toString('base64');
|
|
177
163
|
};
|
|
178
|
-
export async function getAudioDuration(buffer) {
|
|
164
|
+
export async function getAudioDuration(buffer, mimeType) {
|
|
179
165
|
const musicMetadata = await import('music-metadata');
|
|
180
166
|
let metadata;
|
|
181
167
|
const options = {
|
|
182
|
-
duration: true
|
|
168
|
+
duration: true,
|
|
169
|
+
...(mimeType ? { mimeType } : {})
|
|
183
170
|
};
|
|
184
171
|
if (Buffer.isBuffer(buffer)) {
|
|
185
|
-
metadata = await musicMetadata.parseBuffer(buffer, undefined, options);
|
|
172
|
+
metadata = await musicMetadata.parseBuffer(buffer, mimeType || undefined, options);
|
|
186
173
|
}
|
|
187
174
|
else if (typeof buffer === 'string') {
|
|
188
|
-
|
|
175
|
+
// parseFile tidak support mimeType langsung, tapi kita bisa baca sebagai buffer
|
|
176
|
+
// supaya mimeType hint bisa dipass — penting untuk m4a/aac yang nama filenya tanpa ekstensi
|
|
177
|
+
try {
|
|
178
|
+
metadata = await musicMetadata.parseFile(buffer, options);
|
|
179
|
+
} catch (_) {
|
|
180
|
+
// fallback: baca file sebagai buffer lalu parse dengan mimeType
|
|
181
|
+
const { readFileSync } = await import('fs');
|
|
182
|
+
const buf = readFileSync(buffer);
|
|
183
|
+
metadata = await musicMetadata.parseBuffer(buf, mimeType || undefined, options);
|
|
184
|
+
}
|
|
189
185
|
}
|
|
190
186
|
else {
|
|
191
|
-
metadata = await musicMetadata.parseStream(buffer, undefined, options);
|
|
187
|
+
metadata = await musicMetadata.parseStream(buffer, mimeType || undefined, options);
|
|
192
188
|
}
|
|
193
|
-
|
|
189
|
+
const dur = metadata.format.duration;
|
|
190
|
+
// Jangan return NaN/undefined — WhatsApp akan tampilkan "Loading..." kalau seconds invalid
|
|
191
|
+
return (typeof dur === 'number' && !isNaN(dur) && isFinite(dur)) ? Math.round(dur) : 0;
|
|
194
192
|
}
|
|
195
193
|
/**
|
|
196
194
|
referenced from and modifying https://github.com/wppconnect-team/wa-js/blob/main/src/chat/functions/prepareAudioWaveform.ts
|
|
@@ -210,23 +208,26 @@ export async function getAudioWaveform(buffer, logger) {
|
|
|
210
208
|
else {
|
|
211
209
|
audioData = await toBuffer(buffer);
|
|
212
210
|
}
|
|
211
|
+
// Skip audio-decode for large buffers (> 3MB)
|
|
212
|
+
if (audioData.length > 3 * 1024 * 1024) {
|
|
213
|
+
logger?.debug('audio buffer too large for waveform decode, skipping');
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
213
216
|
const audioBuffer = await decoder(audioData);
|
|
214
|
-
const rawData = audioBuffer.getChannelData(0);
|
|
215
|
-
const samples = 64;
|
|
216
|
-
const blockSize = Math.floor(rawData.length / samples);
|
|
217
|
+
const rawData = audioBuffer.getChannelData(0);
|
|
218
|
+
const samples = 64;
|
|
219
|
+
const blockSize = Math.floor(rawData.length / samples);
|
|
217
220
|
const filteredData = [];
|
|
218
221
|
for (let i = 0; i < samples; i++) {
|
|
219
|
-
const blockStart = blockSize * i;
|
|
222
|
+
const blockStart = blockSize * i;
|
|
220
223
|
let sum = 0;
|
|
221
224
|
for (let j = 0; j < blockSize; j++) {
|
|
222
|
-
sum = sum + Math.abs(rawData[blockStart + j]);
|
|
225
|
+
sum = sum + Math.abs(rawData[blockStart + j]);
|
|
223
226
|
}
|
|
224
|
-
filteredData.push(sum / blockSize);
|
|
227
|
+
filteredData.push(sum / blockSize);
|
|
225
228
|
}
|
|
226
|
-
// This guarantees that the largest data point will be set to 1, and the rest of the data will scale proportionally.
|
|
227
229
|
const multiplier = Math.pow(Math.max(...filteredData), -1);
|
|
228
230
|
const normalizedData = filteredData.map(n => n * multiplier);
|
|
229
|
-
// Generate waveform like WhatsApp
|
|
230
231
|
const waveform = new Uint8Array(normalizedData.map(n => Math.floor(100 * n)));
|
|
231
232
|
return waveform;
|
|
232
233
|
}
|
|
@@ -234,8 +235,9 @@ export async function getAudioWaveform(buffer, logger) {
|
|
|
234
235
|
logger?.debug('Failed to generate waveform: ' + e);
|
|
235
236
|
}
|
|
236
237
|
}
|
|
238
|
+
|
|
237
239
|
export const toReadable = (buffer) => {
|
|
238
|
-
const readable = new Readable({ read: () => { } });
|
|
240
|
+
const readable = new Readable({ read: () => { }, highWaterMark: 64 * 1024 });
|
|
239
241
|
readable.push(buffer);
|
|
240
242
|
readable.push(null);
|
|
241
243
|
return readable;
|
|
@@ -248,6 +250,46 @@ export const toBuffer = async (stream) => {
|
|
|
248
250
|
stream.destroy();
|
|
249
251
|
return Buffer.concat(chunks);
|
|
250
252
|
};
|
|
253
|
+
/**
|
|
254
|
+
* Convert audio buffer ke MP3 menggunakan ffmpeg.
|
|
255
|
+
* Return buffer MP3 baru, atau buffer asli jika ffmpeg tidak tersedia / gagal.
|
|
256
|
+
*/
|
|
257
|
+
export const convertAudioToMp3 = async (inputBuffer, logger) => {
|
|
258
|
+
const { spawn } = await import('child_process');
|
|
259
|
+
return new Promise((resolve) => {
|
|
260
|
+
const ff = spawn('ffmpeg', [
|
|
261
|
+
'-hide_banner', '-loglevel', 'error',
|
|
262
|
+
'-i', 'pipe:0', // input dari stdin
|
|
263
|
+
'-vn', // no video
|
|
264
|
+
'-ar', '44100', // sample rate
|
|
265
|
+
'-ac', '2', // stereo
|
|
266
|
+
'-b:a', '128k', // bitrate
|
|
267
|
+
'-f', 'mp3', // output format
|
|
268
|
+
'pipe:1' // output ke stdout
|
|
269
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
270
|
+
|
|
271
|
+
const chunks = [];
|
|
272
|
+
ff.stdout.on('data', chunk => chunks.push(chunk));
|
|
273
|
+
ff.stderr.on('data', d => logger?.debug('[ffmpeg] ' + d.toString().trim()));
|
|
274
|
+
|
|
275
|
+
ff.on('close', code => {
|
|
276
|
+
if (code === 0 && chunks.length > 0) {
|
|
277
|
+
resolve(Buffer.concat(chunks));
|
|
278
|
+
} else {
|
|
279
|
+
logger?.warn(`ffmpeg exited with code ${code}, using original audio`);
|
|
280
|
+
resolve(inputBuffer); // fallback ke original
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
ff.on('error', (err) => {
|
|
284
|
+
logger?.warn('ffmpeg not found or failed: ' + err.message + ', using original audio');
|
|
285
|
+
resolve(inputBuffer); // fallback
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
ff.stdin.write(inputBuffer);
|
|
289
|
+
ff.stdin.end();
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
|
|
251
293
|
export const getStream = async (item, opts) => {
|
|
252
294
|
if (Buffer.isBuffer(item)) {
|
|
253
295
|
return { stream: toReadable(item), type: 'buffer' };
|
|
@@ -280,16 +322,8 @@ export async function generateThumbnail(file, mediaType, options) {
|
|
|
280
322
|
}
|
|
281
323
|
}
|
|
282
324
|
else if (mediaType === 'video') {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 });
|
|
286
|
-
const buff = await fs.readFile(imgFilename);
|
|
287
|
-
thumbnail = buff.toString('base64');
|
|
288
|
-
await fs.unlink(imgFilename);
|
|
289
|
-
}
|
|
290
|
-
catch (err) {
|
|
291
|
-
options.logger?.debug('could not generate video thumb: ' + err);
|
|
292
|
-
}
|
|
325
|
+
// Video thumbnail generation skipped (ffmpeg removed)
|
|
326
|
+
options.logger?.debug('video thumbnail generation skipped (no ffmpeg)');
|
|
293
327
|
}
|
|
294
328
|
return {
|
|
295
329
|
thumbnail,
|
|
@@ -309,77 +343,68 @@ export const getHttpStream = async (url, options = {}) => {
|
|
|
309
343
|
return response.body instanceof Readable ? response.body : Readable.fromWeb(response.body);
|
|
310
344
|
};
|
|
311
345
|
|
|
312
|
-
// ── convertToOpusBuffer (ported from elaina) ─────────────────────────────────
|
|
313
|
-
const convertToOpusBuffer = (buffer, logger) => new Promise((resolve, reject) => {
|
|
314
|
-
const args = [
|
|
315
|
-
'-i', 'pipe:0',
|
|
316
|
-
'-c:a', 'libopus',
|
|
317
|
-
'-b:a', '64k',
|
|
318
|
-
'-vbr', 'on',
|
|
319
|
-
'-compression_level', '10',
|
|
320
|
-
'-frame_duration', '20',
|
|
321
|
-
'-application', 'voip',
|
|
322
|
-
'-f', 'ogg',
|
|
323
|
-
'pipe:1'
|
|
324
|
-
];
|
|
325
|
-
const ffmpeg = spawn('ffmpeg', args);
|
|
326
|
-
const chunks = [];
|
|
327
|
-
ffmpeg.stdin.write(buffer);
|
|
328
|
-
ffmpeg.stdin.end();
|
|
329
|
-
ffmpeg.stdout.on('data', chunk => chunks.push(chunk));
|
|
330
|
-
ffmpeg.stderr.on('data', () => {});
|
|
331
|
-
ffmpeg.on('close', code => {
|
|
332
|
-
if (code === 0) resolve(Buffer.concat(chunks));
|
|
333
|
-
else reject(new Error(`FFmpeg Opus conversion exited with code ${code}`));
|
|
334
|
-
});
|
|
335
|
-
ffmpeg.on('error', err => reject(err));
|
|
336
|
-
});
|
|
337
|
-
// ── End convertToOpusBuffer ───────────────────────────────────────────────────
|
|
338
|
-
|
|
339
346
|
export const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus } = {}) => {
|
|
340
347
|
const { stream, type } = await getStream(media, opts);
|
|
341
348
|
logger?.debug('fetched media stream');
|
|
349
|
+
|
|
342
350
|
let finalStream = stream;
|
|
343
351
|
let opusConverted = false;
|
|
344
|
-
|
|
345
|
-
|
|
352
|
+
|
|
353
|
+
// ── Auto-convert semua audio ke MP3 ─────────────────────────────────────
|
|
354
|
+
// Kecuali: ptt (voice note) tetap ogg/opus, dan yang sudah MP3
|
|
355
|
+
if (mediaType === 'audio' && !isPtt && !forceOpus) {
|
|
346
356
|
try {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
357
|
+
const rawBuf = await toBuffer(finalStream);
|
|
358
|
+
// Cek apakah sudah MP3 (magic bytes: ID3 atau FF E*)
|
|
359
|
+
const isAlreadyMp3 = (rawBuf[0] === 0x49 && rawBuf[1] === 0x44 && rawBuf[2] === 0x33)
|
|
360
|
+
|| (rawBuf[0] === 0xFF && (rawBuf[1] & 0xE0) === 0xE0);
|
|
361
|
+
if (!isAlreadyMp3) {
|
|
362
|
+
logger?.debug('converting audio to MP3 via ffmpeg');
|
|
363
|
+
const mp3Buf = await convertAudioToMp3(rawBuf, logger);
|
|
364
|
+
finalStream = toReadable(mp3Buf);
|
|
365
|
+
logger?.debug('audio converted to MP3 successfully');
|
|
366
|
+
} else {
|
|
367
|
+
finalStream = toReadable(rawBuf);
|
|
368
|
+
logger?.debug('audio already MP3, skipping conversion');
|
|
369
|
+
}
|
|
370
|
+
} catch (err) {
|
|
371
|
+
logger?.warn('audio conversion failed, using original: ' + err.message);
|
|
372
|
+
// finalStream sudah di-consume, perlu re-fetch — re-use dari getStream
|
|
373
|
+
const { stream: s2 } = await getStream(media, opts);
|
|
374
|
+
finalStream = s2;
|
|
356
375
|
}
|
|
357
376
|
}
|
|
377
|
+
// ── End auto-convert ─────────────────────────────────────────────────────
|
|
378
|
+
|
|
358
379
|
const mediaKey = Crypto.randomBytes(32);
|
|
359
380
|
const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType);
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
let
|
|
363
|
-
let
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
381
|
+
const encWriteStream = new Readable({ read: () => {}, highWaterMark: 64 * 1024 });
|
|
382
|
+
let bodyPath;
|
|
383
|
+
let writeStream;
|
|
384
|
+
let didSaveToTmpPath = false;
|
|
385
|
+
|
|
386
|
+
if (type === 'file') {
|
|
387
|
+
bodyPath = media.url?.toString?.() || media.url;
|
|
388
|
+
} else if (saveOriginalFileIfRequired) {
|
|
389
|
+
bodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2());
|
|
390
|
+
writeStream = createWriteStream(bodyPath);
|
|
391
|
+
didSaveToTmpPath = true;
|
|
367
392
|
}
|
|
393
|
+
|
|
368
394
|
let fileLength = 0;
|
|
369
395
|
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv);
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
await once(encFileWriteStream, 'drain');
|
|
379
|
-
}
|
|
396
|
+
let hmac = Crypto.createHmac('sha256', macKey).update(iv);
|
|
397
|
+
let sha256Plain = Crypto.createHash('sha256');
|
|
398
|
+
let sha256Enc = Crypto.createHash('sha256');
|
|
399
|
+
|
|
400
|
+
const onChunk = (buff) => {
|
|
401
|
+
sha256Enc = sha256Enc.update(buff);
|
|
402
|
+
hmac = hmac.update(buff);
|
|
403
|
+
encWriteStream.push(buff);
|
|
380
404
|
};
|
|
405
|
+
|
|
381
406
|
try {
|
|
382
|
-
for await (const data of
|
|
407
|
+
for await (const data of finalStream) {
|
|
383
408
|
fileLength += data.length;
|
|
384
409
|
if (type === 'remote' &&
|
|
385
410
|
opts?.maxContentLength &&
|
|
@@ -388,58 +413,50 @@ export const encryptedStream = async (media, mediaType, { logger, saveOriginalFi
|
|
|
388
413
|
data: { media, type }
|
|
389
414
|
});
|
|
390
415
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
416
|
+
sha256Plain = sha256Plain.update(data);
|
|
417
|
+
if (writeStream) {
|
|
418
|
+
if (!writeStream.write(data)) {
|
|
419
|
+
await once(writeStream, 'drain');
|
|
394
420
|
}
|
|
395
421
|
}
|
|
396
|
-
|
|
397
|
-
await onChunk(aes.update(data));
|
|
422
|
+
onChunk(aes.update(data));
|
|
398
423
|
}
|
|
399
|
-
|
|
424
|
+
|
|
425
|
+
onChunk(aes.final());
|
|
400
426
|
const mac = hmac.digest().slice(0, 10);
|
|
401
|
-
sha256Enc.update(mac);
|
|
427
|
+
sha256Enc = sha256Enc.update(mac);
|
|
402
428
|
const fileSha256 = sha256Plain.digest();
|
|
403
429
|
const fileEncSha256 = sha256Enc.digest();
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
// This helps reduce memory pressure by allowing OS to release buffers
|
|
412
|
-
await encFinishPromise;
|
|
413
|
-
await originalFinishPromise;
|
|
430
|
+
|
|
431
|
+
encWriteStream.push(mac);
|
|
432
|
+
encWriteStream.push(null);
|
|
433
|
+
writeStream?.end();
|
|
434
|
+
if (writeStream) await once(writeStream, 'finish');
|
|
435
|
+
finalStream.destroy();
|
|
436
|
+
|
|
414
437
|
logger?.debug('encrypted data successfully');
|
|
438
|
+
|
|
415
439
|
return {
|
|
416
440
|
mediaKey,
|
|
417
|
-
|
|
418
|
-
|
|
441
|
+
encWriteStream,
|
|
442
|
+
bodyPath,
|
|
419
443
|
mac,
|
|
420
444
|
fileEncSha256,
|
|
421
445
|
fileSha256,
|
|
422
446
|
fileLength,
|
|
447
|
+
didSaveToTmpPath,
|
|
423
448
|
opusConverted
|
|
424
449
|
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
encFileWriteStream.destroy();
|
|
429
|
-
originalFileStream?.destroy?.();
|
|
450
|
+
} catch (error) {
|
|
451
|
+
encWriteStream.destroy();
|
|
452
|
+
writeStream?.destroy();
|
|
430
453
|
aes.destroy();
|
|
431
454
|
hmac.destroy();
|
|
432
455
|
sha256Plain.destroy();
|
|
433
456
|
sha256Enc.destroy();
|
|
434
457
|
stream.destroy();
|
|
435
|
-
|
|
436
|
-
await fs.unlink(
|
|
437
|
-
if (originalFilePath) {
|
|
438
|
-
await fs.unlink(originalFilePath);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
catch (err) {
|
|
442
|
-
logger?.error({ err }, 'failed deleting tmp files');
|
|
458
|
+
if (didSaveToTmpPath) {
|
|
459
|
+
try { await fs.unlink(bodyPath); } catch (_) {}
|
|
443
460
|
}
|
|
444
461
|
throw error;
|
|
445
462
|
}
|
|
@@ -674,7 +691,7 @@ const uploadMedia = async (params, logger) => {
|
|
|
674
691
|
}
|
|
675
692
|
};
|
|
676
693
|
export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
|
|
677
|
-
return async (
|
|
694
|
+
return async (streamOrPath, { mediaType, fileEncSha256B64, timeoutMs, newsletter }) => {
|
|
678
695
|
// send a query JSON to obtain the url & auth token to upload our media
|
|
679
696
|
let uploadInfo = await refreshMediaConn(false);
|
|
680
697
|
let urls;
|
|
@@ -692,27 +709,56 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, opt
|
|
|
692
709
|
'Content-Type': 'application/octet-stream',
|
|
693
710
|
Origin: DEFAULT_ORIGIN
|
|
694
711
|
};
|
|
695
|
-
|
|
696
|
-
|
|
712
|
+
// Collect buffer from Readable stream or read from file path
|
|
713
|
+
let reqBuffer;
|
|
714
|
+
if (Buffer.isBuffer(streamOrPath)) {
|
|
715
|
+
reqBuffer = streamOrPath;
|
|
716
|
+
} else if (typeof streamOrPath === 'string') {
|
|
717
|
+
reqBuffer = await fs.readFile(streamOrPath);
|
|
718
|
+
} else {
|
|
719
|
+
// Readable stream
|
|
720
|
+
const chunks = [];
|
|
721
|
+
for await (const chunk of streamOrPath) chunks.push(chunk);
|
|
722
|
+
reqBuffer = Buffer.concat(chunks);
|
|
723
|
+
}
|
|
724
|
+
// Newsletter uses different upload path
|
|
725
|
+
let mediaPath = MEDIA_PATH_MAP[mediaType];
|
|
726
|
+
if (newsletter) {
|
|
727
|
+
mediaPath = mediaPath?.replace('/mms/', '/newsletter/newsletter-');
|
|
728
|
+
logger.debug(`[blckrose-debug] newsletter upload | mediaType=${mediaType} path=${mediaPath} bufferLen=${reqBuffer?.length}`);
|
|
729
|
+
}
|
|
730
|
+
for (const { hostname, maxContentLengthBytes } of hosts) {
|
|
697
731
|
const auth = encodeURIComponent(uploadInfo.auth);
|
|
698
|
-
const url = `https://${hostname}${
|
|
732
|
+
const url = `https://${hostname}${mediaPath}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;
|
|
733
|
+
logger.debug(`[blckrose-debug] uploading to url=${url.slice(0, 80)}...`);
|
|
734
|
+
|
|
699
735
|
let result;
|
|
700
736
|
try {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
737
|
+
// Upload buffer directly like wiley (avoids file I/O issues)
|
|
738
|
+
const axios = (await import('axios')).default;
|
|
739
|
+
const body = await axios.post(url, reqBuffer, {
|
|
740
|
+
...options,
|
|
741
|
+
headers: {
|
|
742
|
+
...headers,
|
|
743
|
+
},
|
|
744
|
+
httpsAgent: fetchAgent,
|
|
745
|
+
timeout: timeoutMs,
|
|
746
|
+
responseType: 'json',
|
|
747
|
+
maxBodyLength: Infinity,
|
|
748
|
+
maxContentLength: Infinity,
|
|
749
|
+
});
|
|
750
|
+
result = body.data;
|
|
751
|
+
|
|
708
752
|
if (result?.url || result?.direct_path) {
|
|
709
753
|
urls = {
|
|
710
754
|
mediaUrl: result.url,
|
|
711
755
|
directPath: result.direct_path,
|
|
756
|
+
handle: result.handle,
|
|
712
757
|
meta_hmac: result.meta_hmac,
|
|
713
758
|
fbid: result.fbid,
|
|
714
759
|
ts: result.ts
|
|
715
760
|
};
|
|
761
|
+
logger.debug({ mediaUrl: result.url, directPath: result.direct_path, handle: result.handle }, '[blckrose-debug] upload success');
|
|
716
762
|
break;
|
|
717
763
|
}
|
|
718
764
|
else {
|
|
@@ -722,6 +768,7 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, opt
|
|
|
722
768
|
}
|
|
723
769
|
catch (error) {
|
|
724
770
|
const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname;
|
|
771
|
+
|
|
725
772
|
logger.warn({ trace: error?.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`);
|
|
726
773
|
}
|
|
727
774
|
}
|
|
@@ -822,51 +869,39 @@ const MEDIA_RETRY_STATUS_MAP = {
|
|
|
822
869
|
export const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus } = {}) => {
|
|
823
870
|
const { stream, type } = await getStream(media, opts);
|
|
824
871
|
logger?.debug('fetched media stream');
|
|
825
|
-
|
|
872
|
+
|
|
826
873
|
let buffer = await toBuffer(stream);
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
opusConverted = true;
|
|
833
|
-
logger?.debug('converted audio to Opus for newsletter PTT');
|
|
834
|
-
} catch (e) {
|
|
835
|
-
logger?.error('failed to convert audio for newsletter PTT');
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
const encFilePath = join(tmpdir(), mediaType + generateMessageID() + '-plain');
|
|
839
|
-
const encFileWriteStream = createWriteStream(encFilePath);
|
|
840
|
-
let originalFilePath;
|
|
841
|
-
let originalFileStream;
|
|
842
|
-
if (type === 'file') {
|
|
843
|
-
originalFilePath = media.url.toString();
|
|
844
|
-
} else if (saveOriginalFileIfRequired) {
|
|
845
|
-
originalFilePath = join(tmpdir(), mediaType + generateMessageID() + '-original');
|
|
846
|
-
originalFileStream = createWriteStream(originalFilePath);
|
|
847
|
-
}
|
|
848
|
-
let fileLength = 0;
|
|
849
|
-
const hashCtx = createHashCrypto('sha256');
|
|
874
|
+
|
|
875
|
+
let opusConverted = false;
|
|
876
|
+
|
|
877
|
+
let bodyPath;
|
|
878
|
+
let didSaveToTmpPath = false;
|
|
850
879
|
try {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
880
|
+
if (type === 'file') {
|
|
881
|
+
bodyPath = media.url?.toString?.() || media.url;
|
|
882
|
+
} else if (saveOriginalFileIfRequired) {
|
|
883
|
+
bodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2());
|
|
884
|
+
const { writeFileSync } = await import('fs');
|
|
885
|
+
writeFileSync(bodyPath, buffer);
|
|
886
|
+
didSaveToTmpPath = true;
|
|
857
887
|
}
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
originalFileStream?.end?.call(originalFileStream);
|
|
888
|
+
const fileLength = buffer.length;
|
|
889
|
+
const fileSha256 = Crypto.createHash('sha256').update(buffer).digest();
|
|
861
890
|
logger?.debug('prepared plain stream successfully');
|
|
862
|
-
return {
|
|
891
|
+
return {
|
|
892
|
+
mediaKey: undefined,
|
|
893
|
+
encWriteStream: buffer,
|
|
894
|
+
fileLength,
|
|
895
|
+
fileSha256,
|
|
896
|
+
fileEncSha256: undefined,
|
|
897
|
+
bodyPath,
|
|
898
|
+
didSaveToTmpPath,
|
|
899
|
+
opusConverted
|
|
900
|
+
};
|
|
863
901
|
} catch (error) {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
try {
|
|
868
|
-
await unlinkFs(encFilePath);
|
|
869
|
-
} catch (err) { logger?.error({ err }, 'failed deleting tmp files'); }
|
|
902
|
+
if (didSaveToTmpPath && bodyPath) {
|
|
903
|
+
try { await fs.unlink(bodyPath); } catch (_) {}
|
|
904
|
+
}
|
|
870
905
|
throw error;
|
|
871
906
|
}
|
|
872
907
|
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { WASocket } from '../Types/index.js';
|
|
2
|
+
import type { WAMediaUpload } from '../Types/Message.js';
|
|
3
|
+
|
|
4
|
+
export interface NewsletterButtonItem {
|
|
5
|
+
id: string;
|
|
6
|
+
text?: string;
|
|
7
|
+
displayText?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface NewsletterListRow {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
rowId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NewsletterListSection {
|
|
18
|
+
title?: string;
|
|
19
|
+
rows: NewsletterListRow[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface NewsletterButtonsParams {
|
|
23
|
+
body: string;
|
|
24
|
+
buttons: NewsletterButtonItem[];
|
|
25
|
+
title?: string;
|
|
26
|
+
footer?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface NewsletterListParams {
|
|
30
|
+
body: string;
|
|
31
|
+
buttonText: string;
|
|
32
|
+
sections: NewsletterListSection[];
|
|
33
|
+
title?: string;
|
|
34
|
+
footer?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface NewsletterCtaUrlParams {
|
|
38
|
+
body: string;
|
|
39
|
+
buttonText: string;
|
|
40
|
+
url: string;
|
|
41
|
+
title?: string;
|
|
42
|
+
footer?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface NewsletterUtils {
|
|
46
|
+
/** Kirim teks ke newsletter */
|
|
47
|
+
sendNewsletterText(jid: string, text: string, options?: object): Promise<any>;
|
|
48
|
+
/** Kirim gambar ke newsletter */
|
|
49
|
+
sendNewsletterImage(jid: string, image: WAMediaUpload, options?: { caption?: string; mimetype?: string; jpegThumbnail?: string }): Promise<any>;
|
|
50
|
+
/** Kirim video ke newsletter */
|
|
51
|
+
sendNewsletterVideo(jid: string, video: WAMediaUpload, options?: { caption?: string; mimetype?: string; gifPlayback?: boolean }): Promise<any>;
|
|
52
|
+
/** Kirim PTV (video note lingkaran) ke newsletter */
|
|
53
|
+
sendNewsletterPtv(jid: string, video: WAMediaUpload, options?: { mimetype?: string }): Promise<any>;
|
|
54
|
+
/** Kirim audio ke newsletter */
|
|
55
|
+
sendNewsletterAudio(jid: string, audio: WAMediaUpload, options?: { mimetype?: string; seconds?: number; ptt?: boolean }): Promise<any>;
|
|
56
|
+
/** Kirim dokumen ke newsletter */
|
|
57
|
+
sendNewsletterDocument(jid: string, document: WAMediaUpload, options?: { mimetype?: string; fileName?: string; caption?: string }): Promise<any>;
|
|
58
|
+
/** Kirim sticker ke newsletter */
|
|
59
|
+
sendNewsletterSticker(jid: string, sticker: WAMediaUpload, options?: { isAnimated?: boolean }): Promise<any>;
|
|
60
|
+
/** Kirim pesan dengan quick_reply buttons ke newsletter */
|
|
61
|
+
sendNewsletterButtons(jid: string, params: NewsletterButtonsParams, options?: object): Promise<any>;
|
|
62
|
+
/** Kirim pesan dengan single_select list ke newsletter */
|
|
63
|
+
sendNewsletterList(jid: string, params: NewsletterListParams, options?: object): Promise<any>;
|
|
64
|
+
/** Kirim pesan dengan CTA URL button ke newsletter */
|
|
65
|
+
sendNewsletterCtaUrl(jid: string, params: NewsletterCtaUrlParams, options?: object): Promise<any>;
|
|
66
|
+
/** React ke server message ID newsletter */
|
|
67
|
+
sendNewsletterReact(jid: string, serverId: string, emoji?: string): Promise<any>;
|
|
68
|
+
/** Edit pesan newsletter */
|
|
69
|
+
editNewsletterMessage(jid: string, messageId: string, newText: string): Promise<any>;
|
|
70
|
+
/** Hapus pesan newsletter */
|
|
71
|
+
deleteNewsletterMessage(jid: string, messageId: string): Promise<any>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Buat object utilities newsletter terikat ke conn.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* const nl = makeNewsletterUtils(conn);
|
|
79
|
+
* await nl.sendNewsletterButtons('120363...@newsletter', {
|
|
80
|
+
* body: 'Pilih opsi:',
|
|
81
|
+
* buttons: [{ id: 'a', text: 'Opsi A' }, { id: 'b', text: 'Opsi B' }]
|
|
82
|
+
* });
|
|
83
|
+
*/
|
|
84
|
+
export declare function makeNewsletterUtils(conn: WASocket): NewsletterUtils;
|