@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.
@@ -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, WriteStream } from '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 finalStream) {
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
- /** Extracts video thumb using FFMPEG */
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
- metadata = await musicMetadata.parseFile(buffer, options);
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
- return metadata.format.duration;
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); // We only need to work with one channel of data
215
- const samples = 64; // Number of samples we want to have in our final data set
216
- const blockSize = Math.floor(rawData.length / samples); // the number of samples in each subdivision
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; // the location of the first sample in the block
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]); // find the sum of all the samples in the block
225
+ sum = sum + Math.abs(rawData[blockStart + j]);
223
226
  }
224
- filteredData.push(sum / blockSize); // divide the sum by the block size to get the average
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
- const imgFilename = join(getTmpFilesDirectory(), generateMessageIDV2() + '.jpg');
284
- try {
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
- // Auto-convert to Opus if PTT or forceOpus
345
- if (mediaType === 'audio' && (isPtt === true || forceOpus === true)) {
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 buf = await toBuffer(finalStream);
348
- const opusBuf = await convertToOpusBuffer(buf, logger);
349
- finalStream = toReadable(opusBuf);
350
- opusConverted = true;
351
- logger?.debug('converted audio to Opus for PTT');
352
- } catch (error) {
353
- logger?.error('failed to convert audio to Opus, fallback to original');
354
- const { stream: newStream } = await getStream(media, opts);
355
- finalStream = newStream;
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 encFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2() + '-enc');
361
- const encFileWriteStream = createWriteStream(encFilePath);
362
- let originalFileStream;
363
- let originalFilePath;
364
- if (saveOriginalFileIfRequired) {
365
- originalFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2() + '-original');
366
- originalFileStream = createWriteStream(originalFilePath);
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
- const hmac = Crypto.createHmac('sha256', macKey).update(iv);
371
- const sha256Plain = Crypto.createHash('sha256');
372
- const sha256Enc = Crypto.createHash('sha256');
373
- const onChunk = async (buff) => {
374
- sha256Enc.update(buff);
375
- hmac.update(buff);
376
- // Handle backpressure: if write returns false, wait for drain
377
- if (!encFileWriteStream.write(buff)) {
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 stream) {
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
- if (originalFileStream) {
392
- if (!originalFileStream.write(data)) {
393
- await once(originalFileStream, 'drain');
416
+ sha256Plain = sha256Plain.update(data);
417
+ if (writeStream) {
418
+ if (!writeStream.write(data)) {
419
+ await once(writeStream, 'drain');
394
420
  }
395
421
  }
396
- sha256Plain.update(data);
397
- await onChunk(aes.update(data));
422
+ onChunk(aes.update(data));
398
423
  }
399
- await onChunk(aes.final());
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
- encFileWriteStream.write(mac);
405
- const encFinishPromise = once(encFileWriteStream, 'finish');
406
- const originalFinishPromise = originalFileStream ? once(originalFileStream, 'finish') : Promise.resolve();
407
- encFileWriteStream.end();
408
- originalFileStream?.end?.();
409
- stream.destroy();
410
- // Wait for write streams to fully flush to disk
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
- originalFilePath,
418
- encFilePath,
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
- catch (error) {
427
- // destroy all streams with error
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
- try {
436
- await fs.unlink(encFilePath);
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 (filePath, { mediaType, fileEncSha256B64, timeoutMs }) => {
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
- for (const { hostname } of hosts) {
696
- logger.debug(`uploading to "${hostname}"`);
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}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;
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
- result = await uploadMedia({
702
- url,
703
- filePath,
704
- headers,
705
- timeoutMs,
706
- agent: fetchAgent
707
- }, logger);
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
- let opusConverted = false;
872
+
826
873
  let buffer = await toBuffer(stream);
827
- // Auto-convert to Opus if PTT or forceOpus
828
- if (mediaType === 'audio' && (isPtt === true || forceOpus === true)) {
829
- try {
830
- const opusBuf = await convertToOpusBuffer(buffer, logger);
831
- buffer = opusBuf;
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
- // Use buffer (possibly opus-converted) directly
852
- fileLength = buffer.length;
853
- hashCtx.update(buffer);
854
- encFileWriteStream.write(buffer);
855
- if (originalFileStream) {
856
- originalFileStream.write(buffer);
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 fileSha256 = hashCtx.digest();
859
- encFileWriteStream.end();
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 { mediaKey: undefined, originalFilePath, encFilePath, mac: undefined, fileEncSha256: undefined, fileSha256, fileLength, opusConverted };
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
- encFileWriteStream.destroy();
865
- originalFileStream?.destroy?.call(originalFileStream);
866
- stream.destroy();
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;