@blckrose/baileys 2.0.3 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -90,7 +90,8 @@ export const MEDIA_PATH_MAP = {
90
90
  'md-app-state': '',
91
91
  'md-msg-hist': '/mms/md-app-state',
92
92
  'biz-cover-photo': '/pps/biz-cover-photo',
93
- 'sticker-pack': '/mms/sticker'
93
+ 'sticker-pack': '/mms/sticker',
94
+ 'thumbnail-sticker-pack': '/mms/sticker'
94
95
  };
95
96
  export const MEDIA_HKDF_KEY_MAPPING = {
96
97
  audio: 'Audio',
@@ -112,7 +113,8 @@ export const MEDIA_HKDF_KEY_MAPPING = {
112
113
  'payment-bg-image': 'Payment Background',
113
114
  ptv: 'Video',
114
115
  'biz-cover-photo': 'Image',
115
- 'sticker-pack': 'Sticker Pack'
116
+ 'sticker-pack': 'Sticker Pack',
117
+ 'thumbnail-sticker-pack': 'Sticker Pack'
116
118
  };
117
119
  export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP);
118
120
  export const MIN_PREKEY_COUNT = 5;
@@ -132,4 +134,12 @@ export const TimeMs = {
132
134
  Week: 7 * 24 * 60 * 60 * 1000
133
135
  };
134
136
  export const PHONENUMBER_MCC = phoneNumberMcc;
137
+
138
+ export const DONATE_URL = 'https://saweria.co/itsliaaa';
139
+ export const LEXER_REGEX = /(\/\/.*|\/\*[\s\S]*?\*\/|#.*)|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`[\s\S]*?`)|(\b[a-zA-Z_]\w*\b)(?=\s*\()|(\b[a-zA-Z_]\w*\b)|(\b\d+(?:\.\d+)?\b)|(\s+|[^\w\s]+)/g;
140
+ export const BOT_RENDERING_CONFIG_METADATA = {
141
+ bloksVersioningId: '0903aa5f7f47de66789d5f4c86d3bd6e05e4bc3ff85e454a9f907d5ed7fef97c',
142
+ pixelDensity: 2.75
143
+ };
144
+
135
145
  //# sourceMappingURL=index.js.map
@@ -1501,7 +1501,7 @@ export const makeMessagesSocket = (config) => {
1501
1501
  messageContent.contextInfo = { mentionedJid: options.mentionedJid };
1502
1502
  }
1503
1503
  const payload = proto.Message.InteractiveMessage.create(messageContent);
1504
- const msg = generateWAMessageFromContent(jid, { viewOnceMessage: { message: { interactiveMessage: payload } } }, { userJid, quoted: options?.quoted || null });
1504
+ const msg = generateWAMessageFromContent(jid, { interactiveMessage: payload }, { userJid, quoted: options?.quoted || null });
1505
1505
  const additionalNodes = [{ tag: 'biz', attrs: {}, content: [{ tag: 'interactive', attrs: { type: 'native_flow', v: '1' }, content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }] }] }];
1506
1506
  await relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes });
1507
1507
  return msg;
@@ -0,0 +1,22 @@
1
+ export var CodeHighlightType;
2
+ (function (CodeHighlightType) {
3
+ CodeHighlightType[CodeHighlightType["DEFAULT"] = 0] = "DEFAULT";
4
+ CodeHighlightType[CodeHighlightType["KEYWORD"] = 1] = "KEYWORD";
5
+ CodeHighlightType[CodeHighlightType["METHOD"] = 2] = "METHOD";
6
+ CodeHighlightType[CodeHighlightType["STRING"] = 3] = "STRING";
7
+ CodeHighlightType[CodeHighlightType["NUMBER"] = 4] = "NUMBER";
8
+ CodeHighlightType[CodeHighlightType["COMMENT"] = 5] = "COMMENT";
9
+ })(CodeHighlightType || (CodeHighlightType = {}));
10
+ export var RichSubMessageType;
11
+ (function (RichSubMessageType) {
12
+ RichSubMessageType[RichSubMessageType["UNKNOWN"] = 0] = "UNKNOWN";
13
+ RichSubMessageType[RichSubMessageType["GRID_IMAGE"] = 1] = "GRID_IMAGE";
14
+ RichSubMessageType[RichSubMessageType["TEXT"] = 2] = "TEXT";
15
+ RichSubMessageType[RichSubMessageType["INLINE_IMAGE"] = 3] = "INLINE_IMAGE";
16
+ RichSubMessageType[RichSubMessageType["TABLE"] = 4] = "TABLE";
17
+ RichSubMessageType[RichSubMessageType["CODE"] = 5] = "CODE";
18
+ RichSubMessageType[RichSubMessageType["DYNAMIC"] = 6] = "DYNAMIC";
19
+ RichSubMessageType[RichSubMessageType["MAP"] = 7] = "MAP";
20
+ RichSubMessageType[RichSubMessageType["LATEX"] = 8] = "LATEX";
21
+ RichSubMessageType[RichSubMessageType["CONTENT_ITEMS"] = 9] = "CONTENT_ITEMS";
22
+ })(RichSubMessageType || (RichSubMessageType = {}));
@@ -1,4 +1,5 @@
1
1
  export * from './Auth.js';
2
+ export * from './RichType.js';
2
3
  export * from './MexUpdates.js';
3
4
  export * from './GroupMetadata.js';
4
5
  export * from './Chat.js';
@@ -0,0 +1,32 @@
1
+ export const CompanionWebClientType = {
2
+ UNKNOWN: 0,
3
+ CHROME: 1,
4
+ EDGE: 2,
5
+ FIREFOX: 3,
6
+ IE: 4,
7
+ OPERA: 5,
8
+ SAFARI: 6,
9
+ ELECTRON: 7,
10
+ UWP: 8,
11
+ OTHER_WEB_CLIENT: 9
12
+ };
13
+ const BROWSER_TO_COMPANION_WEB_CLIENT = {
14
+ Chrome: CompanionWebClientType.CHROME,
15
+ Edge: CompanionWebClientType.EDGE,
16
+ Firefox: CompanionWebClientType.FIREFOX,
17
+ IE: CompanionWebClientType.IE,
18
+ Opera: CompanionWebClientType.OPERA,
19
+ Safari: CompanionWebClientType.SAFARI
20
+ };
21
+ export const getCompanionWebClientType = ([os, browserName]) => {
22
+ if (browserName === 'Desktop') {
23
+ return os === 'Windows' ? CompanionWebClientType.UWP : CompanionWebClientType.ELECTRON;
24
+ }
25
+ return BROWSER_TO_COMPANION_WEB_CLIENT[browserName] || CompanionWebClientType.OTHER_WEB_CLIENT;
26
+ };
27
+ export const getCompanionPlatformId = (browser) => {
28
+ return getCompanionWebClientType(browser).toString();
29
+ };
30
+ export const buildPairingQRData = (ref, noiseKeyB64, identityKeyB64, advB64, browser) => {
31
+ return [ref, noiseKeyB64, identityKeyB64, advB64, getCompanionPlatformId(browser)].join(',');
32
+ };
@@ -24,4 +24,8 @@ export * from './browser-utils.js';
24
24
  export * from './identity-change-handler.js';
25
25
  export * from './messages-newsletter.js';
26
26
  export * from './resolve-jid.js';
27
+ export * from './rich-message-utils.js';
28
+ export * from './companion-reg-client-utils.js';
29
+ export * from './offline-node-processor.js';
30
+ export * from './stanza-ack.js';
27
31
  //# sourceMappingURL=index.js.map
@@ -635,7 +635,8 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, opt
635
635
  const headers = {
636
636
  ...customHeaders,
637
637
  'Content-Type': 'application/octet-stream',
638
- Origin: DEFAULT_ORIGIN
638
+ 'Origin': DEFAULT_ORIGIN,
639
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
639
640
  };
640
641
  // Collect buffer from Readable stream or read from file path
641
642
  let reqBuffer;
@@ -654,50 +655,64 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, opt
654
655
  if (newsletter) {
655
656
  mediaPath = mediaPath?.replace('/mms/', '/newsletter/newsletter-');
656
657
  }
657
- for (const { hostname, maxContentLengthBytes } of hosts) {
658
- const auth = encodeURIComponent(uploadInfo.auth);
659
- const url = `https://${hostname}${mediaPath}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;
658
+
659
+ // Retry logic: try all hosts, then refresh connection and retry
660
+ const maxRetries = 2;
661
+ for (let attempt = 0; attempt < maxRetries && !urls; attempt++) {
662
+ if (attempt > 0) {
663
+ logger?.info?.(`Retrying upload (attempt ${attempt + 1}/${maxRetries})...`);
664
+ uploadInfo = await refreshMediaConn(true);
665
+ }
660
666
 
661
- let result;
662
- try {
663
- // Upload buffer directly like wiley (avoids file I/O issues)
664
- const axios = (await import('axios')).default;
665
- const body = await axios.post(url, reqBuffer, {
666
- ...options,
667
- headers: {
668
- ...headers,
669
- },
670
- httpsAgent: fetchAgent,
671
- timeout: timeoutMs,
672
- responseType: 'json',
673
- maxBodyLength: Infinity,
674
- maxContentLength: Infinity,
675
- });
676
- result = body.data;
667
+ for (const { hostname, maxContentLengthBytes } of hosts) {
668
+ const auth = encodeURIComponent(uploadInfo.auth);
669
+ const url = `https://${hostname}${mediaPath}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;
670
+
671
+ let result;
672
+ try {
673
+ // Upload buffer directly like wiley (avoids file I/O issues)
674
+ const axios = (await import('axios')).default;
675
+ const body = await axios.post(url, reqBuffer, {
676
+ ...options,
677
+ headers: {
678
+ ...headers,
679
+ 'Accept': '*/*',
680
+ },
681
+ httpsAgent: fetchAgent,
682
+ timeout: timeoutMs || 60000, // Default 60s timeout
683
+ responseType: 'json',
684
+ maxBodyLength: Infinity,
685
+ maxContentLength: Infinity,
686
+ validateStatus: () => true, // Don't throw on any status code
687
+ });
688
+ result = body.data;
677
689
 
678
- if (result?.url || result?.direct_path) {
679
- urls = {
680
- mediaUrl: result.url,
681
- directPath: result.direct_path,
682
- handle: result.handle,
683
- meta_hmac: result.meta_hmac,
684
- fbid: result.fbid,
685
- ts: result.ts
686
- };
687
- break;
690
+ if (result?.url || result?.direct_path) {
691
+ urls = {
692
+ mediaUrl: result.url,
693
+ directPath: result.direct_path,
694
+ handle: result.handle,
695
+ meta_hmac: result.meta_hmac,
696
+ fbid: result.fbid,
697
+ ts: result.ts
698
+ };
699
+ logger?.info?.(`Upload successful to host: ${hostname}`);
700
+ break;
701
+ }
702
+ else {
703
+ logger?.warn?.(`Upload to ${hostname} failed, reason: ${JSON.stringify(result)}`);
704
+ // Refresh media conn on failure
705
+ uploadInfo = await refreshMediaConn(true);
706
+ }
688
707
  }
689
- else {
690
- uploadInfo = await refreshMediaConn(true);
691
- throw new Error(`upload failed, reason: ${JSON.stringify(result)}`);
708
+ catch (error) {
709
+ const isLast = hostname === hosts[hosts.length - 1]?.hostname;
710
+ logger?.warn?.(`Error uploading to ${hostname} ${isLast ? '' : ', retrying...'}: ${error?.message}`);
692
711
  }
693
712
  }
694
- catch (error) {
695
- const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname;
696
-
697
- logger.warn({ trace: error?.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`);
698
- }
699
713
  }
700
714
  if (!urls) {
715
+ logger?.error?.('Media upload failed on all hosts after all retries');
701
716
  throw new Boom('Media upload failed on all hosts', { statusCode: 500 });
702
717
  }
703
718
  return urls;
@@ -20,104 +20,6 @@ const MIMETYPE_MAP = {
20
20
  sticker: 'image/webp',
21
21
  'product-catalog-image': 'image/jpeg'
22
22
  };
23
- /** Map ekstensi audio ke mimetype */
24
- const AUDIO_MIMETYPE_MAP = {
25
- ogg: 'audio/ogg; codecs=opus',
26
- oga: 'audio/ogg; codecs=opus',
27
- opus: 'audio/ogg; codecs=opus',
28
- mp3: 'audio/mpeg',
29
- mpeg: 'audio/mpeg',
30
- mp4: 'audio/mp4',
31
- m4a: 'audio/mp4',
32
- aac: 'audio/aac',
33
- wav: 'audio/wav',
34
- wave: 'audio/wav',
35
- flac: 'audio/flac',
36
- webm: 'audio/webm',
37
- amr: 'audio/amr',
38
- '3gp': 'audio/3gpp',
39
- '3gpp': 'audio/3gpp',
40
- wma: 'audio/x-ms-wma',
41
- caf: 'audio/x-caf',
42
- aiff: 'audio/aiff',
43
- aif: 'audio/aiff',
44
- };
45
- /**
46
- * Deteksi mimetype audio dari magic bytes buffer.
47
- * Return null jika tidak dikenali.
48
- */
49
- const detectAudioMimetypeFromBuffer = (buf) => {
50
- if (!buf || buf.length < 12) return null;
51
- // OGG
52
- if (buf[0] === 0x4F && buf[1] === 0x67 && buf[2] === 0x67 && buf[3] === 0x53)
53
- return 'audio/ogg; codecs=opus';
54
- // MP3 (ID3 tag atau sync bits)
55
- if ((buf[0] === 0x49 && buf[1] === 0x44 && buf[2] === 0x33) ||
56
- (buf[0] === 0xFF && (buf[1] & 0xE0) === 0xE0))
57
- return 'audio/mpeg';
58
- // MP4/M4A (ftyp box)
59
- if (buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70)
60
- return 'audio/mp4';
61
- // RIFF/WAV
62
- if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 &&
63
- buf[8] === 0x57 && buf[9] === 0x41 && buf[10] === 0x56 && buf[11] === 0x45)
64
- return 'audio/wav';
65
- // FLAC
66
- if (buf[0] === 0x66 && buf[1] === 0x4C && buf[2] === 0x61 && buf[3] === 0x43)
67
- return 'audio/flac';
68
- // WEBM/MKV
69
- if (buf[0] === 0x1A && buf[1] === 0x45 && buf[2] === 0xDF && buf[3] === 0xA3)
70
- return 'audio/webm';
71
- // AMR
72
- if (buf[0] === 0x23 && buf[1] === 0x21 && buf[2] === 0x41 && buf[3] === 0x4D &&
73
- buf[4] === 0x52)
74
- return 'audio/amr';
75
- return null;
76
- };
77
- /**
78
- * Deteksi mimetype audio secara otomatis dari media input.
79
- * Cek: 1) ekstensi URL/path, 2) magic bytes buffer, 3) fallback ke ogg/opus.
80
- */
81
- const detectAudioMimetype = async (media) => {
82
- // Cek ekstensi dari URL atau path string
83
- if (typeof media === 'string' || (media && typeof media === 'object' && 'url' in media)) {
84
- const urlStr = typeof media === 'string' ? media : media.url?.toString?.() ?? '';
85
- // Ambil path tanpa query string, lalu cari semua ekstensi
86
- const pathOnly = urlStr.split('?')[0];
87
- // Cek ekstensi terakhir (.m4a, .mp3, dst)
88
- const extMatch = pathOnly.match(/\.([a-zA-Z0-9]{2,5})(?:[^/]*)?$/);
89
- if (extMatch) {
90
- const ext = extMatch[1].toLowerCase();
91
- if (AUDIO_MIMETYPE_MAP[ext]) return AUDIO_MIMETYPE_MAP[ext];
92
- }
93
- // Fallback: scan semua segmen path untuk ekstensi audio yang dikenal
94
- // Contoh: ".plus.aac.ep.m4a" → cek tiap segment dari belakang
95
- const segments = pathOnly.split('.');
96
- for (let i = segments.length - 1; i >= 0; i--) {
97
- const seg = segments[i].toLowerCase().split('/')[0].split('?')[0];
98
- if (AUDIO_MIMETYPE_MAP[seg]) return AUDIO_MIMETYPE_MAP[seg];
99
- }
100
- }
101
- // Cek magic bytes jika Buffer
102
- if (Buffer.isBuffer(media)) {
103
- const detected = detectAudioMimetypeFromBuffer(media);
104
- if (detected) return detected;
105
- }
106
- // Fallback: default ogg/opus
107
- return MIMETYPE_MAP.audio;
108
- };
109
- const MessageTypeProto = {
110
- image: WAProto.Message.ImageMessage,
111
- video: WAProto.Message.VideoMessage,
112
- audio: WAProto.Message.AudioMessage,
113
- sticker: WAProto.Message.StickerMessage,
114
- document: WAProto.Message.DocumentMessage
115
- };
116
- /**
117
- * Uses a regex to test whether the string contains a URL, and returns the URL if it does.
118
- * @param text eg. hello https://google.com
119
- * @returns the URL, eg. https://google.com
120
- */
121
23
  export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0];
122
24
  export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
123
25
  const url = extractUrlFromText(text);
@@ -127,7 +29,6 @@ export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) =>
127
29
  return urlInfo;
128
30
  }
129
31
  catch (error) {
130
- // ignore if fails
131
32
  logger?.warn({ trace: error.stack }, 'url generation failed');
132
33
  }
133
34
  }
@@ -146,6 +47,13 @@ const assertColor = async (color) => {
146
47
  return assertedColor;
147
48
  }
148
49
  };
50
+ const MessageTypeProto = {
51
+ image: WAProto.Message.ImageMessage,
52
+ video: WAProto.Message.VideoMessage,
53
+ audio: WAProto.Message.AudioMessage,
54
+ sticker: WAProto.Message.StickerMessage,
55
+ document: WAProto.Message.DocumentMessage
56
+ };
149
57
  export const prepareWAMessageMedia = async (message, options) => {
150
58
  const logger = options.logger;
151
59
  let mediaType;
@@ -676,8 +584,8 @@ export const generateWAMessageContent = async (message, options) => {
676
584
  const { stickers, cover, name, publisher, packId, description } = message.stickerPack;
677
585
 
678
586
  // ── Validasi jumlah sticker ───────────────────────────────────────────
679
- if (stickers.length > 60) {
680
- throw new Boom('Sticker pack exceeds the maximum limit of 60 stickers', { statusCode: 400 });
587
+ if (stickers.length > 120) {
588
+ throw new Boom('Sticker pack exceeds the maximum limit of 120 stickers', { statusCode: 400 });
681
589
  }
682
590
  if (stickers.length === 0) {
683
591
  throw new Boom('Sticker pack must contain at least one sticker', { statusCode: 400 });
@@ -715,40 +623,87 @@ export const generateWAMessageContent = async (message, options) => {
715
623
 
716
624
  // ── Step 1: proses & zip semua sticker ────────────────────────────────
717
625
  const stickerData = {};
718
- const stickerPromises = stickers.map(async (s, i) => {
719
- const { stream } = await getStream(s.data || s.sticker);
720
- const buffer = await toBuffer(stream);
626
+ const stickerMetadata = [];
627
+
628
+ // Process stickers sequentially to avoid memory issues
629
+ for (let i = 0; i < stickers.length; i++) {
630
+ const s = stickers[i];
631
+ try {
632
+ const { stream } = await getStream(s.data || s.sticker);
633
+ const buffer = await toBuffer(stream);
721
634
 
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
- }
635
+ if (!buffer || buffer.length === 0) {
636
+ continue;
637
+ }
638
+
639
+ let webpBuffer;
640
+ let isAnimated = false;
641
+ if (isWebPBuffer(buffer)) {
642
+ isAnimated = isAnimatedWebP(buffer);
643
+ // Compress animated WebP with sharp
644
+ if ('sharp' in lib && lib.sharp) {
645
+ webpBuffer = await lib.sharp.default(buffer)
646
+ .resize(512, 512, {
647
+ fit: 'inside',
648
+ withoutEnlargement: true
649
+ })
650
+ .webp({
651
+ quality: 75,
652
+ effort: 6 // Maximum compression effort (0-6)
653
+ })
654
+ .toBuffer();
655
+ } else {
656
+ webpBuffer = buffer;
657
+ }
658
+ } else if ('sharp' in lib && lib.sharp) {
659
+ // Convert and compress static images
660
+ webpBuffer = await lib.sharp.default(buffer)
661
+ .resize(512, 512, {
662
+ fit: 'inside',
663
+ withoutEnlargement: true
664
+ })
665
+ .webp({
666
+ quality: 75,
667
+ effort: 6 // Maximum compression effort (0-6)
668
+ })
669
+ .toBuffer();
670
+ } else {
671
+ throw new Boom(
672
+ 'No image processing library (sharp) available for converting sticker to WebP. ' +
673
+ 'Either install sharp or provide stickers in WebP format.'
674
+ );
675
+ }
676
+
677
+ // Stricter size limit per sticker (1MB for larger packs)
678
+ const MAX_STICKER_SIZE = 1024 * 1024; // 1MB
679
+ if (webpBuffer.length > MAX_STICKER_SIZE) {
680
+ continue;
681
+ }
735
682
 
736
- if (webpBuffer.length > 1024 * 1024) {
737
- throw new Boom(`Sticker at index ${i} exceeds the 1MB size limit`, { statusCode: 400 });
683
+ const hash = sha256(webpBuffer).toString('base64').replace(/\//g, '-');
684
+ const fileName = `${hash}.webp`;
685
+ // Use compression level 6 for individual stickers in ZIP (balanced)
686
+ stickerData[fileName] = [new Uint8Array(webpBuffer), { level: 6 }];
687
+ stickerMetadata.push({
688
+ fileName,
689
+ mimetype: 'image/webp',
690
+ isAnimated,
691
+ emojis: s.emojis || [],
692
+ accessibilityLabel: s.accessibilityLabel || ''
693
+ });
694
+ } catch (err) {
695
+ // Continue with next sticker instead of failing completely
738
696
  }
697
+ }
739
698
 
740
- const hash = sha256(webpBuffer).toString('base64').replace(/\//g, '-');
741
- const fileName = `${hash}.webp`;
742
- stickerData[fileName] = [new Uint8Array(webpBuffer), { level: 0 }];
743
- return {
744
- fileName,
745
- mimetype: 'image/webp',
746
- isAnimated,
747
- emojis: s.emojis || [],
748
- accessibilityLabel: s.accessibilityLabel || ''
749
- };
750
- });
751
- const stickerMetadata = await Promise.all(stickerPromises);
699
+ // Check if we have at least one valid sticker
700
+ if (stickerMetadata.length === 0) {
701
+ throw new Boom('No valid stickers could be processed', { statusCode: 400 });
702
+ }
703
+
704
+ if (stickerMetadata.length < stickers.length) {
705
+ // Some stickers were skipped
706
+ }
752
707
 
753
708
  // ── Step 2: proses cover & masukkan ke dalam ZIP ──────────────────────
754
709
  const trayIconFileName = `${stickerPackId}.webp`;
@@ -756,37 +711,79 @@ export const generateWAMessageContent = async (message, options) => {
756
711
 
757
712
  let coverWebpBuffer;
758
713
  if (isWebPBuffer(coverBuffer)) {
759
- coverWebpBuffer = coverBuffer;
714
+ // Compress cover WebP
715
+ if ('sharp' in lib && lib.sharp) {
716
+ coverWebpBuffer = await lib.sharp.default(coverBuffer)
717
+ .resize(512, 512, {
718
+ fit: 'inside',
719
+ withoutEnlargement: true
720
+ })
721
+ .webp({
722
+ quality: 75,
723
+ effort: 6
724
+ })
725
+ .toBuffer();
726
+ } else {
727
+ coverWebpBuffer = coverBuffer;
728
+ }
760
729
  } else if ('sharp' in lib && lib.sharp) {
761
- coverWebpBuffer = await lib.sharp.default(coverBuffer).webp().toBuffer();
730
+ coverWebpBuffer = await lib.sharp.default(coverBuffer)
731
+ .resize(512, 512, {
732
+ fit: 'inside',
733
+ withoutEnlargement: true
734
+ })
735
+ .webp({
736
+ quality: 75,
737
+ effort: 6
738
+ })
739
+ .toBuffer();
762
740
  } else {
763
741
  throw new Boom(
764
742
  'No image processing library (sharp) available for converting cover to WebP. ' +
765
743
  'Either install sharp or provide cover in WebP format.'
766
744
  );
767
745
  }
768
- stickerData[trayIconFileName] = [new Uint8Array(coverWebpBuffer), { level: 0 }];
746
+ // Compress cover in ZIP as well (level 6 for balanced compression)
747
+ stickerData[trayIconFileName] = [new Uint8Array(coverWebpBuffer), { level: 6 }];
769
748
 
770
749
  // ── Step 3: buat ZIP buffer ───────────────────────────────────────────
771
750
  const zipBuffer = await new Promise((resolve, reject) => {
772
- zip(stickerData, (err, data) => {
751
+ zip(stickerData, { level: 6, memLevel: 9 }, (err, data) => {
773
752
  if (err) reject(err);
774
753
  else resolve(Buffer.from(data));
775
754
  });
776
755
  });
777
756
 
757
+ // ── Validasi ukuran ZIP (WhatsApp limit ~10MB untuk sticker pack) ───
758
+ const MAX_STICKER_PACK_SIZE = 10 * 1024 * 1024; // 10MB
759
+ if (zipBuffer.length > MAX_STICKER_PACK_SIZE) {
760
+ throw new Boom(`Sticker pack too large: ${(zipBuffer.length / 1024 / 1024).toFixed(2)}MB (max: ${(MAX_STICKER_PACK_SIZE / 1024 / 1024).toFixed(2)}MB). Try reducing sticker count or size`, { statusCode: 400 });
761
+ }
762
+
778
763
  // ── Step 4: encrypt ZIP (generate random mediaKey) ────────────────────
779
764
  const stickerPackUpload = await encryptedStream(zipBuffer, 'sticker-pack', {
780
765
  logger: options.logger,
781
766
  opts: options.options
782
767
  });
783
768
 
769
+ options.logger?.info?.(`Sticker pack encrypted, fileSha256: ${stickerPackUpload.fileSha256.toString('base64')}`);
770
+
784
771
  // ── 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
- });
772
+ let stickerPackUploadResult;
773
+ try {
774
+ stickerPackUploadResult = await options.upload(stickerPackUpload.encWriteStream, {
775
+ fileEncSha256B64: stickerPackUpload.fileEncSha256.toString('base64'),
776
+ mediaType: 'sticker-pack',
777
+ timeoutMs: options.mediaUploadTimeoutMs || 300000 // 300s (5 menit) untuk pack besar
778
+ });
779
+ options.logger?.info?.(`Sticker pack uploaded successfully: ${stickerPackUploadResult.directPath}`);
780
+ } catch (uploadError) {
781
+ options.logger?.error?.(`Sticker pack upload failed: ${uploadError.message}`);
782
+ throw new Boom(`Failed to upload sticker pack: ${uploadError.message}`, {
783
+ statusCode: 500,
784
+ data: { originalError: uploadError }
785
+ });
786
+ }
790
787
 
791
788
  // ── Step 6: build stickerPackMessage ──────────────────────────────────
792
789
  m.stickerPackMessage = {
@@ -810,7 +807,7 @@ export const generateWAMessageContent = async (message, options) => {
810
807
  try {
811
808
  let thumbnailBuffer;
812
809
  if ('sharp' in lib && lib.sharp) {
813
- thumbnailBuffer = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer();
810
+ thumbnailBuffer = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg({ quality: 80 }).toBuffer();
814
811
  } else if ('jimp' in lib && lib.jimp) {
815
812
  const jimpImage = await (lib.jimp.Jimp || lib.jimp.default).read(coverBuffer);
816
813
  thumbnailBuffer = await jimpImage.resize({ w: 252, h: 252 }).getBuffer('image/jpeg');
@@ -831,7 +828,7 @@ export const generateWAMessageContent = async (message, options) => {
831
828
  const thumbUploadResult = await options.upload(thumbUpload.encWriteStream, {
832
829
  fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'),
833
830
  mediaType: 'thumbnail-sticker-pack',
834
- timeoutMs: options.mediaUploadTimeoutMs
831
+ timeoutMs: options.mediaUploadTimeoutMs || 60000
835
832
  });
836
833
 
837
834
  Object.assign(m.stickerPackMessage, {
@@ -842,8 +839,10 @@ export const generateWAMessageContent = async (message, options) => {
842
839
  thumbnailWidth: 252,
843
840
  imageDataHash: sha256(thumbnailBuffer).toString('base64')
844
841
  });
842
+
843
+ options.logger?.info?.(`Thumbnail uploaded successfully`);
845
844
  } catch (e) {
846
- options.logger?.warn?.(`Thumbnail generation failed: ${e}`);
845
+ options.logger?.warn?.(`Thumbnail generation/upload failed: ${e.message}`);
847
846
  }
848
847
 
849
848
  m.stickerPackMessage.contextInfo = {
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Creates a processor for offline stanza nodes that:
3
+ * - Queues nodes for sequential processing
4
+ * - Yields to the event loop periodically to avoid blocking
5
+ * - Catches handler errors to prevent the processing loop from crashing
6
+ */
7
+ export function makeOfflineNodeProcessor(nodeProcessorMap, deps, batchSize = 10) {
8
+ const nodes = [];
9
+ let isProcessing = false;
10
+ const enqueue = (type, node) => {
11
+ nodes.push({ type, node });
12
+ if (isProcessing) {
13
+ return;
14
+ }
15
+ isProcessing = true;
16
+ const promise = async () => {
17
+ let processedInBatch = 0;
18
+ while (nodes.length && deps.isWsOpen()) {
19
+ const { type, node } = nodes.shift();
20
+ const nodeProcessor = nodeProcessorMap.get(type);
21
+ if (!nodeProcessor) {
22
+ deps.onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node');
23
+ continue;
24
+ }
25
+ await nodeProcessor(node).catch(err => deps.onUnexpectedError(err, `processing offline ${type}`));
26
+ processedInBatch++;
27
+ // Yield to event loop after processing a batch
28
+ // This prevents blocking the event loop for too long when there are many offline nodes
29
+ if (processedInBatch >= batchSize) {
30
+ processedInBatch = 0;
31
+ await deps.yieldToEventLoop();
32
+ }
33
+ }
34
+ isProcessing = false;
35
+ };
36
+ promise().catch(error => deps.onUnexpectedError(error, 'processing offline nodes'));
37
+ };
38
+ return { enqueue };
39
+ }