@blckrose/baileys 1.2.6 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -205,7 +205,7 @@ export const makeMessagesRecvSocket = (config) => {
205
205
  message: messageProto,
206
206
  messageTimestamp: +child.attrs.t
207
207
  }).toJSON();
208
- await upsertMessage(fullMessage, 'append');
208
+ await upsertMessage(fullMessage, 'notify');
209
209
  logger.info('Processed plaintext newsletter message');
210
210
  }
211
211
  catch (error) {
@@ -562,7 +562,6 @@ export const makeMessagesSocket = (config) => {
562
562
  content: bytes
563
563
  });
564
564
  logger.debug({ msgId, extraAttrs }, `sending newsletter message to ${jid}`);
565
- logger.debug({ additionalAttributes }, '[blckrose-debug] newsletter stanza attrs');
566
565
  const stanza = {
567
566
  tag: 'message',
568
567
  attrs: {
@@ -1085,6 +1084,9 @@ export const makeMessagesSocket = (config) => {
1085
1084
  else if (message.stickerMessage) {
1086
1085
  return 'sticker';
1087
1086
  }
1087
+ else if (message.stickerPackMessage) {
1088
+ return 'sticker_pack';
1089
+ }
1088
1090
  else if (message.listMessage) {
1089
1091
  return 'list';
1090
1092
  }
@@ -1545,6 +1547,12 @@ export const makeMessagesSocket = (config) => {
1545
1547
  messageId: generateMessageIDV2(sock.user?.id),
1546
1548
  ...options
1547
1549
  });
1550
+ if (content?.audio && options?.contextInfo) {
1551
+ const msgContent = fullMsg.message;
1552
+ if (msgContent?.audioMessage) {
1553
+ msgContent.audioMessage.contextInfo = options.contextInfo;
1554
+ }
1555
+ }
1548
1556
  // Extract handle from newsletter upload (set by prepareWAMessageMedia)
1549
1557
  if (!mediaHandle) {
1550
1558
  const msgContent = fullMsg.message;
@@ -29,7 +29,7 @@ export const hkdfInfoKey = (type) => {
29
29
  };
30
30
  export const getRawMediaUploadData = async (media, mediaType, logger) => {
31
31
  const { stream } = await getStream(media);
32
- logger?.debug('got stream for raw upload');
32
+
33
33
  const hasher = Crypto.createHash('sha256');
34
34
  const filePath = join(tmpdir(), mediaType + generateMessageIDV2());
35
35
  const fileWriteStream = createWriteStream(filePath);
@@ -46,7 +46,7 @@ export const getRawMediaUploadData = async (media, mediaType, logger) => {
46
46
  await once(fileWriteStream, 'finish');
47
47
  stream.destroy();
48
48
  const fileSha256 = hasher.digest();
49
- logger?.debug('hashed data for raw upload');
49
+
50
50
  return {
51
51
  filePath: filePath,
52
52
  fileSha256,
@@ -210,7 +210,6 @@ export async function getAudioWaveform(buffer, logger) {
210
210
  }
211
211
  // Skip audio-decode for large buffers (> 3MB)
212
212
  if (audioData.length > 3 * 1024 * 1024) {
213
- logger?.debug('audio buffer too large for waveform decode, skipping');
214
213
  return undefined;
215
214
  }
216
215
  const audioBuffer = await decoder(audioData);
@@ -232,7 +231,6 @@ export async function getAudioWaveform(buffer, logger) {
232
231
  return waveform;
233
232
  }
234
233
  catch (e) {
235
- logger?.debug('Failed to generate waveform: ' + e);
236
234
  }
237
235
  }
238
236
 
@@ -250,45 +248,8 @@ export const toBuffer = async (stream) => {
250
248
  stream.destroy();
251
249
  return Buffer.concat(chunks);
252
250
  };
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
251
 
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
252
 
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
253
 
293
254
  export const getStream = async (item, opts) => {
294
255
  if (Buffer.isBuffer(item)) {
@@ -323,7 +284,6 @@ export async function generateThumbnail(file, mediaType, options) {
323
284
  }
324
285
  else if (mediaType === 'video') {
325
286
  // Video thumbnail generation skipped (ffmpeg removed)
326
- options.logger?.debug('video thumbnail generation skipped (no ffmpeg)');
327
287
  }
328
288
  return {
329
289
  thumbnail,
@@ -345,37 +305,9 @@ export const getHttpStream = async (url, options = {}) => {
345
305
 
346
306
  export const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus } = {}) => {
347
307
  const { stream, type } = await getStream(media, opts);
348
- logger?.debug('fetched media stream');
349
-
350
308
  let finalStream = stream;
351
309
  let opusConverted = false;
352
310
 
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) {
356
- try {
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;
375
- }
376
- }
377
- // ── End auto-convert ─────────────────────────────────────────────────────
378
-
379
311
  const mediaKey = Crypto.randomBytes(32);
380
312
  const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType);
381
313
  const encWriteStream = new Readable({ read: () => {}, highWaterMark: 64 * 1024 });
@@ -434,8 +366,6 @@ export const encryptedStream = async (media, mediaType, { logger, saveOriginalFi
434
366
  if (writeStream) await once(writeStream, 'finish');
435
367
  finalStream.destroy();
436
368
 
437
- logger?.debug('encrypted data successfully');
438
-
439
369
  return {
440
370
  mediaKey,
441
371
  encWriteStream,
@@ -682,11 +612,9 @@ const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) =>
682
612
  */
683
613
  const uploadMedia = async (params, logger) => {
684
614
  if (isNodeRuntime()) {
685
- logger?.debug('Using Node.js https module for upload (avoids undici buffering bug)');
686
615
  return uploadWithNodeHttp(params);
687
616
  }
688
617
  else {
689
- logger?.debug('Using web-standard Fetch API for upload');
690
618
  return uploadWithFetch(params);
691
619
  }
692
620
  };
@@ -725,12 +653,10 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, opt
725
653
  let mediaPath = MEDIA_PATH_MAP[mediaType];
726
654
  if (newsletter) {
727
655
  mediaPath = mediaPath?.replace('/mms/', '/newsletter/newsletter-');
728
- logger.debug(`[blckrose-debug] newsletter upload | mediaType=${mediaType} path=${mediaPath} bufferLen=${reqBuffer?.length}`);
729
656
  }
730
657
  for (const { hostname, maxContentLengthBytes } of hosts) {
731
658
  const auth = encodeURIComponent(uploadInfo.auth);
732
659
  const url = `https://${hostname}${mediaPath}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;
733
- logger.debug(`[blckrose-debug] uploading to url=${url.slice(0, 80)}...`);
734
660
 
735
661
  let result;
736
662
  try {
@@ -758,7 +684,6 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, opt
758
684
  fbid: result.fbid,
759
685
  ts: result.ts
760
686
  };
761
- logger.debug({ mediaUrl: result.url, directPath: result.direct_path, handle: result.handle }, '[blckrose-debug] upload success');
762
687
  break;
763
688
  }
764
689
  else {
@@ -868,8 +793,6 @@ const MEDIA_RETRY_STATUS_MAP = {
868
793
 
869
794
  export const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus } = {}) => {
870
795
  const { stream, type } = await getStream(media, opts);
871
- logger?.debug('fetched media stream');
872
-
873
796
  let buffer = await toBuffer(stream);
874
797
 
875
798
  let opusConverted = false;
@@ -887,7 +810,6 @@ export const prepareStream = async (media, mediaType, { logger, saveOriginalFile
887
810
  }
888
811
  const fileLength = buffer.length;
889
812
  const fileSha256 = Crypto.createHash('sha256').update(buffer).digest();
890
- logger?.debug('prepared plain stream successfully');
891
813
  return {
892
814
  mediaKey: undefined,
893
815
  encWriteStream: buffer,
@@ -172,16 +172,7 @@ export const prepareWAMessageMedia = async (message, options) => {
172
172
  uploadData.fileName = 'file';
173
173
  }
174
174
  if (!uploadData.mimetype) {
175
- if (mediaType === 'audio') {
176
- uploadData.mimetype = await detectAudioMimetype(uploadData.media);
177
- } else {
178
- uploadData.mimetype = MIMETYPE_MAP[mediaType];
179
- }
180
- }
181
- // Setelah auto-convert ke MP3, override mimetype ke audio/mpeg
182
- // Kecuali PTT (voice note) tetap ogg/opus
183
- if (mediaType === 'audio' && !uploadData.ptt) {
184
- uploadData.mimetype = 'audio/mpeg';
175
+ uploadData.mimetype = MIMETYPE_MAP[mediaType];
185
176
  }
186
177
  if (cacheableKey) {
187
178
  const mediaBuff = await options.mediaCache.get(cacheableKey);
@@ -195,13 +186,11 @@ export const prepareWAMessageMedia = async (message, options) => {
195
186
  }
196
187
  const isNewsletter = !!options.jid && isJidNewsletter(options.jid);
197
188
  if (isNewsletter) options.newsletter = true;
198
- console.log('[blckrose-debug] prepareWAMessageMedia mediaType=%s isNewsletter=%s ptt=%s mimetype=%s', mediaType, !!options.newsletter, uploadData.ptt, uploadData.mimetype);
199
189
  const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined';
200
190
  const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined';
201
191
  const requiresWaveformProcessing = mediaType === 'audio' && (uploadData.ptt === true || !!options.backgroundColor);
202
192
  const requiresAudioBackground = options.backgroundColor && mediaType === 'audio';
203
193
  const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation || requiresWaveformProcessing;
204
- console.log('[blckrose-debug] stream path selected newsletter=%s using=%s', !!options.newsletter, options.newsletter ? 'prepareStream' : 'encryptedStream');
205
194
  let streamResult;
206
195
  try {
207
196
  streamResult = await (options.newsletter ? prepareStream : encryptedStream)(
@@ -215,11 +204,9 @@ export const prepareWAMessageMedia = async (message, options) => {
215
204
  }
216
205
  );
217
206
  } catch (streamErr) {
218
- console.error('[blckrose-debug] stream ERROR:', streamErr?.message, streamErr?.stack);
219
207
  throw streamErr;
220
208
  }
221
209
  const { mediaKey, encWriteStream, bodyPath, fileEncSha256, fileSha256, fileLength, didSaveToTmpPath } = streamResult;
222
- console.log('[blckrose-debug] stream prepared fileLength=%s hasMediaKey=%s', fileLength, !!mediaKey);
223
210
 
224
211
  const fileEncSha256B64 = (options.newsletter ? fileSha256 : fileEncSha256 ?? fileSha256).toString('base64');
225
212
 
@@ -231,7 +218,6 @@ export const prepareWAMessageMedia = async (message, options) => {
231
218
  timeoutMs: options.mediaUploadTimeoutMs,
232
219
  newsletter: !!options.newsletter
233
220
  });
234
- console.log('[blckrose-debug] upload done mediaUrl=%s directPath=%s handle=%s', result?.mediaUrl, result?.directPath, result?.handle);
235
221
  return result;
236
222
  })(),
237
223
  (async () => {
@@ -261,12 +247,9 @@ export const prepareWAMessageMedia = async (message, options) => {
261
247
  logger?.debug('computed audio duration');
262
248
  }
263
249
  if (requiresWaveformProcessing) {
264
- console.log('[blckrose-debug] waveform processing bodyPath=%s', bodyPath);
265
250
  try {
266
- // newsletter: bodyPath undefined, pakai encWriteStream buffer langsung
267
251
  uploadData.waveform = await getAudioWaveform(bodyPath || encWriteStream, logger);
268
252
  } catch (err) {
269
- console.error('[blckrose-debug] waveform ERROR:', err?.message);
270
253
  }
271
254
  if (!uploadData.waveform) {
272
255
  uploadData.waveform = new Uint8Array([0,99,0,99,0,99,0,99,88,99,0,99,0,55,0,99,0,99,0,99,0,99,0,99,88,99,0,99,0,55,0,99]);
@@ -292,7 +275,6 @@ export const prepareWAMessageMedia = async (message, options) => {
292
275
  logger?.warn('failed to remove tmp file');
293
276
  }
294
277
  });
295
- console.log('[blckrose-debug] Promise.all done uploadHandle=%s mediaUrl=%s directPath=%s', uploadHandle, mediaUrl, directPath);
296
278
  const obj = WAProto.Message.fromObject({
297
279
  [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
298
280
  url: uploadHandle ? undefined : mediaUrl,
@@ -303,7 +285,8 @@ export const prepareWAMessageMedia = async (message, options) => {
303
285
  fileLength,
304
286
  mediaKeyTimestamp: uploadHandle ? undefined : unixTimestampSeconds(),
305
287
  ...uploadData,
306
- media: undefined
288
+ media: undefined,
289
+ ...(options?.contextInfo ? { contextInfo: options.contextInfo } : {})
307
290
  })
308
291
  });
309
292
  if (uploadData.ptv) {
@@ -315,8 +298,6 @@ export const prepareWAMessageMedia = async (message, options) => {
315
298
  obj._uploadHandle = uploadHandle;
316
299
  }
317
300
  if (mediaType === 'audio') {
318
- const am = obj.audioMessage;
319
- logger?.debug({ url: am?.url, directPath: am?.directPath, hasMediaKey: !!am?.mediaKey, seconds: am?.seconds, ptt: am?.ptt, mimetype: am?.mimetype, fileLength: am?.fileLength, uploadHandle }, '[blckrose-debug] audioMessage built');
320
301
  }
321
302
  if (cacheableKey) {
322
303
  logger?.debug({ cacheableKey }, 'set cache');
@@ -694,24 +675,99 @@ export const generateWAMessageContent = async (message, options) => {
694
675
  const { zip } = _require('fflate');
695
676
  const { stickers, cover, name, publisher, packId, description } = message.stickerPack;
696
677
 
697
- // ── Step 1: fetch & zip all stickers ─────────────────────────────────
678
+ // ── Validasi jumlah sticker ───────────────────────────────────────────
679
+ if (stickers.length > 60) {
680
+ throw new Boom('Sticker pack exceeds the maximum limit of 60 stickers', { statusCode: 400 });
681
+ }
682
+ if (stickers.length === 0) {
683
+ throw new Boom('Sticker pack must contain at least one sticker', { statusCode: 400 });
684
+ }
685
+
686
+ const stickerPackId = packId || generateMessageIDV2();
687
+ const [_sharp, _jimp] = await Promise.all([import('sharp').catch(() => null), import('jimp').catch(() => null)]);
688
+ const lib = _sharp ? { sharp: _sharp } : _jimp ? { jimp: _jimp } : null;
689
+ if (!lib) throw new Boom('No image processing library available (install sharp or jimp)');
690
+
691
+ // ── Helper: deteksi WebP dari magic bytes ─────────────────────────────
692
+ const isWebPBuffer = (buf) => (
693
+ buf.length >= 12 &&
694
+ buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 &&
695
+ buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50
696
+ );
697
+
698
+ // ── Helper: deteksi animasi WebP (VP8X/ANIM/ANMF chunks) ─────────────
699
+ const isAnimatedWebP = (buf) => {
700
+ if (!isWebPBuffer(buf)) return false;
701
+ let offset = 12;
702
+ while (offset < buf.length - 8) {
703
+ const fourCC = buf.toString('ascii', offset, offset + 4);
704
+ const chunkSize = buf.readUInt32LE(offset + 4);
705
+ if (fourCC === 'VP8X') {
706
+ const flagsOffset = offset + 8;
707
+ if (flagsOffset < buf.length && (buf[flagsOffset] & 0x02)) return true;
708
+ } else if (fourCC === 'ANIM' || fourCC === 'ANMF') {
709
+ return true;
710
+ }
711
+ offset += 8 + chunkSize + (chunkSize % 2);
712
+ }
713
+ return false;
714
+ };
715
+
716
+ // ── Step 1: proses & zip semua sticker ────────────────────────────────
698
717
  const stickerData = {};
699
718
  const stickerPromises = stickers.map(async (s, i) => {
700
- const { stream } = await getStream(s.sticker);
719
+ const { stream } = await getStream(s.data || s.sticker);
701
720
  const buffer = await toBuffer(stream);
702
- const hash = sha256(buffer).toString('base64url');
703
- const fileName = `${i.toString().padStart(2, '0')}_${hash}.webp`;
704
- stickerData[fileName] = [new Uint8Array(buffer), { level: 0 }];
721
+
722
+ let webpBuffer;
723
+ let isAnimated = false;
724
+ if (isWebPBuffer(buffer)) {
725
+ webpBuffer = buffer;
726
+ isAnimated = isAnimatedWebP(buffer);
727
+ } else if ('sharp' in lib && lib.sharp) {
728
+ webpBuffer = await lib.sharp.default(buffer).webp().toBuffer();
729
+ } else {
730
+ throw new Boom(
731
+ 'No image processing library (sharp) available for converting sticker to WebP. ' +
732
+ 'Either install sharp or provide stickers in WebP format.'
733
+ );
734
+ }
735
+
736
+ if (webpBuffer.length > 1024 * 1024) {
737
+ throw new Boom(`Sticker at index ${i} exceeds the 1MB size limit`, { statusCode: 400 });
738
+ }
739
+
740
+ const hash = sha256(webpBuffer).toString('base64').replace(/\//g, '-');
741
+ const fileName = `${hash}.webp`;
742
+ stickerData[fileName] = [new Uint8Array(webpBuffer), { level: 0 }];
705
743
  return {
706
744
  fileName,
707
745
  mimetype: 'image/webp',
708
- isAnimated: s.isAnimated || false,
709
- isLottie: s.isLottie || false,
746
+ isAnimated,
710
747
  emojis: s.emojis || [],
711
748
  accessibilityLabel: s.accessibilityLabel || ''
712
749
  };
713
750
  });
714
751
  const stickerMetadata = await Promise.all(stickerPromises);
752
+
753
+ // ── Step 2: proses cover & masukkan ke dalam ZIP ──────────────────────
754
+ const trayIconFileName = `${stickerPackId}.webp`;
755
+ const coverBuffer = await toBuffer((await getStream(cover)).stream);
756
+
757
+ let coverWebpBuffer;
758
+ if (isWebPBuffer(coverBuffer)) {
759
+ coverWebpBuffer = coverBuffer;
760
+ } else if ('sharp' in lib && lib.sharp) {
761
+ coverWebpBuffer = await lib.sharp.default(coverBuffer).webp().toBuffer();
762
+ } else {
763
+ throw new Boom(
764
+ 'No image processing library (sharp) available for converting cover to WebP. ' +
765
+ 'Either install sharp or provide cover in WebP format.'
766
+ );
767
+ }
768
+ stickerData[trayIconFileName] = [new Uint8Array(coverWebpBuffer), { level: 0 }];
769
+
770
+ // ── Step 3: buat ZIP buffer ───────────────────────────────────────────
715
771
  const zipBuffer = await new Promise((resolve, reject) => {
716
772
  zip(stickerData, (err, data) => {
717
773
  if (err) reject(err);
@@ -719,83 +775,77 @@ export const generateWAMessageContent = async (message, options) => {
719
775
  });
720
776
  });
721
777
 
722
- // ── Step 2: fetch cover buffer ────────────────────────────────────────
723
- const coverBuffer = await toBuffer((await getStream(cover)).stream);
724
-
725
- // ── Step 3: encrypt zip (generates random mediaKey) ───────────────────
778
+ // ── Step 4: encrypt ZIP (generate random mediaKey) ────────────────────
726
779
  const stickerPackUpload = await encryptedStream(zipBuffer, 'sticker-pack', {
727
780
  logger: options.logger,
728
781
  opts: options.options
729
782
  });
730
783
 
731
- // ── Step 4: encrypt cover with the SAME mediaKey as the zip ──────────
732
- // StickerPackMessage proto has no thumbnailMediaKey field, so WA uses the
733
- // pack's mediaKey to decrypt the thumbnail too.
734
- const { getMediaKeys: _getMediaKeys } = await import('./messages-media.js');
735
- const _Crypto = (await import('crypto')).default;
736
- const { cipherKey: covCipherKey, iv: covIv, macKey: covMacKey } = await _getMediaKeys(
737
- stickerPackUpload.mediaKey, 'sticker-pack'
738
- );
739
- const covAes = _Crypto.createCipheriv('aes-256-cbc', covCipherKey, covIv);
740
- let covHmac = _Crypto.createHmac('sha256', covMacKey).update(covIv);
741
- const covSha256Plain = _Crypto.createHash('sha256').update(coverBuffer).digest();
742
- const covEncPart1 = covAes.update(coverBuffer);
743
- const covEncPart2 = covAes.final();
744
- covHmac.update(covEncPart1).update(covEncPart2);
745
- const covMac = covHmac.digest().slice(0, 10);
746
- const covEncBody = Buffer.concat([covEncPart1, covEncPart2, covMac]);
747
- const covFileEncSha256 = _Crypto.createHash('sha256').update(covEncBody).digest();
748
-
749
- // ── Step 5: upload zip and cover in parallel ──────────────────────────
750
- const [stickerPackUploadResult, coverUploadResult] = await Promise.all([
751
- options.upload(stickerPackUpload.encWriteStream, {
752
- fileEncSha256B64: stickerPackUpload.fileEncSha256.toString('base64'),
753
- mediaType: 'sticker-pack',
754
- timeoutMs: options.mediaUploadTimeoutMs
755
- }),
756
- options.upload(covEncBody, {
757
- fileEncSha256B64: covFileEncSha256.toString('base64'),
758
- mediaType: 'sticker-pack',
759
- timeoutMs: options.mediaUploadTimeoutMs
760
- })
761
- ]);
762
-
763
- // ── Step 6: get thumbnail dimensions ─────────────────────────────────
764
- let thumbWidth = 320, thumbHeight = 320;
765
- try {
766
- const { extractImageThumb } = await import('./messages-media.js');
767
- const { original } = await extractImageThumb(coverBuffer);
768
- if (original?.width) thumbWidth = original.width;
769
- if (original?.height) thumbHeight = original.height;
770
- } catch (_) {}
784
+ // ── Step 5: upload ZIP ────────────────────────────────────────────────
785
+ const stickerPackUploadResult = await options.upload(stickerPackUpload.encWriteStream, {
786
+ fileEncSha256B64: stickerPackUpload.fileEncSha256.toString('base64'),
787
+ mediaType: 'sticker-pack',
788
+ timeoutMs: options.mediaUploadTimeoutMs
789
+ });
771
790
 
772
- // ── Step 7: build stickerPackMessage ─────────────────────────────────
773
- const imageDataHash = sha256(coverBuffer).toString('base64');
774
- const stickerPackId = packId || generateMessageIDV2();
791
+ // ── Step 6: build stickerPackMessage ──────────────────────────────────
775
792
  m.stickerPackMessage = {
776
793
  name,
777
794
  publisher,
778
795
  stickerPackId,
779
796
  packDescription: description,
780
797
  stickerPackOrigin: WAProto.Message.StickerPackMessage.StickerPackOrigin.THIRD_PARTY,
781
- stickerPackSize: stickerPackUpload.fileLength,
798
+ stickerPackSize: zipBuffer.length,
782
799
  stickers: stickerMetadata,
783
- // main zip encryption fields
784
800
  fileSha256: stickerPackUpload.fileSha256,
785
801
  fileEncSha256: stickerPackUpload.fileEncSha256,
786
802
  mediaKey: stickerPackUpload.mediaKey,
787
803
  directPath: stickerPackUploadResult.directPath,
788
804
  fileLength: stickerPackUpload.fileLength,
789
805
  mediaKeyTimestamp: unixTimestampSeconds(),
790
- trayIconFileName: `${stickerPackId}.png`,
791
- imageDataHash,
792
- // thumbnail fields: correct proto names, encrypted with SAME mediaKey as zip
793
- thumbnailDirectPath: coverUploadResult.directPath,
794
- thumbnailSha256: covSha256Plain,
795
- thumbnailEncSha256: covFileEncSha256,
796
- thumbnailHeight: thumbHeight,
797
- thumbnailWidth: thumbWidth
806
+ trayIconFileName
798
807
  };
808
+
809
+ // ── Step 7: generate & upload thumbnail (pakai mediaKey yang sama) ────
810
+ try {
811
+ let thumbnailBuffer;
812
+ if ('sharp' in lib && lib.sharp) {
813
+ thumbnailBuffer = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer();
814
+ } else if ('jimp' in lib && lib.jimp) {
815
+ const jimpImage = await (lib.jimp.Jimp || lib.jimp.default).read(coverBuffer);
816
+ thumbnailBuffer = await jimpImage.resize({ w: 252, h: 252 }).getBuffer('image/jpeg');
817
+ } else {
818
+ throw new Error('No image processing library available for thumbnail generation');
819
+ }
820
+
821
+ if (!thumbnailBuffer || thumbnailBuffer.length === 0) {
822
+ throw new Error('Failed to generate thumbnail buffer');
823
+ }
824
+
825
+ const thumbUpload = await encryptedStream(thumbnailBuffer, 'thumbnail-sticker-pack', {
826
+ logger: options.logger,
827
+ opts: options.options,
828
+ mediaKey: stickerPackUpload.mediaKey
829
+ });
830
+
831
+ const thumbUploadResult = await options.upload(thumbUpload.encWriteStream, {
832
+ fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'),
833
+ mediaType: 'thumbnail-sticker-pack',
834
+ timeoutMs: options.mediaUploadTimeoutMs
835
+ });
836
+
837
+ Object.assign(m.stickerPackMessage, {
838
+ thumbnailDirectPath: thumbUploadResult.directPath,
839
+ thumbnailSha256: thumbUpload.fileSha256,
840
+ thumbnailEncSha256: thumbUpload.fileEncSha256,
841
+ thumbnailHeight: 252,
842
+ thumbnailWidth: 252,
843
+ imageDataHash: sha256(thumbnailBuffer).toString('base64')
844
+ });
845
+ } catch (e) {
846
+ options.logger?.warn?.(`Thumbnail generation failed: ${e}`);
847
+ }
848
+
799
849
  m.stickerPackMessage.contextInfo = {
800
850
  ...(message.contextInfo || {}),
801
851
  ...(message.mentions ? { mentionedJid: message.mentions } : {})
@@ -1225,7 +1275,6 @@ export const generateWAMessage = async (jid, content, options) => {
1225
1275
  options.logger = options?.logger?.child({ msgId: options.messageId });
1226
1276
  // Pass jid + newsletter flag to generateWAMessageContent (like wiley)
1227
1277
  const _isNewsletter = typeof jid === 'string' && jid.endsWith('@newsletter');
1228
- console.log('[blckrose-debug] generateWAMessage jid=%s isNewsletter=%s contentKeys=%s', jid, _isNewsletter, Object.keys(content || {}).join(','));
1229
1278
  return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { newsletter: _isNewsletter, ...options, jid }), options);
1230
1279
  };
1231
1280
  /** Get the key to access the true type of content */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blckrose/baileys",
3
3
  "type": "module",
4
- "version": "1.2.6",
4
+ "version": "1.2.8",
5
5
  "description": "A WebSockets library for interacting with WhatsApp Web",
6
6
  "keywords": [
7
7
  "whatsapp",
@@ -48,7 +48,13 @@
48
48
  "@cacheable/node-cache": "^1.4.0",
49
49
  "@hapi/boom": "^9.1.3",
50
50
  "async-mutex": "^0.5.0",
51
+ "audio-decode": "^3.0.0",
52
+ "axios": "^1.13.6",
51
53
  "cache-manager": "^5.7.6",
54
+ "chalk": "^5.3.0",
55
+ "cheerio": "^1.2.0",
56
+ "fflate": "^0.8.2",
57
+ "gradient-string": "^3.0.0",
52
58
  "libsignal": "git+https://github.com/whiskeysockets/libsignal-node",
53
59
  "lru-cache": "^11.1.0",
54
60
  "music-metadata": "^11.7.0",
@@ -56,20 +62,14 @@
56
62
  "pino": "^9.6",
57
63
  "protobufjs": "^7.2.4",
58
64
  "whatsapp-rust-bridge": "0.5.2",
59
- "ws": "^8.13.0",
60
- "chalk": "^5.3.0",
61
- "fflate": "^0.8.2"
65
+ "ws": "^8.13.0"
62
66
  },
63
67
  "peerDependencies": {
64
- "audio-decode": "^2.1.3",
65
68
  "jimp": "^1.6.0",
66
69
  "link-preview-js": "^3.0.0",
67
70
  "sharp": "*"
68
71
  },
69
72
  "peerDependenciesMeta": {
70
- "audio-decode": {
71
- "optional": true
72
- },
73
73
  "jimp": {
74
74
  "optional": true
75
75
  },