@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, '
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/lib/Utils/messages.js
CHANGED
|
@@ -172,16 +172,7 @@ export const prepareWAMessageMedia = async (message, options) => {
|
|
|
172
172
|
uploadData.fileName = 'file';
|
|
173
173
|
}
|
|
174
174
|
if (!uploadData.mimetype) {
|
|
175
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
|
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
|
|
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
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
|
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:
|
|
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
|
|
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.
|
|
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
|
},
|