@nexustechpro/baileys 1.1.2 → 1.1.4

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.
@@ -8,11 +8,15 @@ import { WAMessageStatus, WAProto } from '../Types/index.js';
8
8
  import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
9
9
  import { sha256 } from './crypto.js';
10
10
  import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
11
- import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData, getStream, toBuffer, getImageProcessingLibrary } from './messages-media.js';
11
+ import { downloadContentFromMessage, encryptedStream, prepareStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData, getStream, toBuffer, getImageProcessingLibrary } from './messages-media.js';
12
12
 
13
13
  const MIMETYPE_MAP = { image: 'image/jpeg', video: 'video/mp4', document: 'application/pdf', audio: 'audio/ogg; codecs=opus', sticker: 'image/webp', 'product-catalog-image': 'image/jpeg' };
14
14
  const MessageTypeProto = { image: WAProto.Message.ImageMessage, video: WAProto.Message.VideoMessage, audio: WAProto.Message.AudioMessage, sticker: WAProto.Message.StickerMessage, document: WAProto.Message.DocumentMessage };
15
15
 
16
+ // High-level content keys that need processing (not raw WAProto)
17
+ const HIGH_LEVEL_KEYS = ['text', 'image', 'video', 'audio', 'document', 'sticker', 'contacts', 'location', 'react', 'delete', 'forward', 'disappearingMessagesInChat', 'groupInvite', 'stickerPack', 'pin', 'buttonReply', 'ptv', 'product', 'listReply', 'event', 'poll', 'inviteAdmin', 'requestPayment', 'sharePhoneNumber', 'requestPhoneNumber', 'limitSharing', 'viewOnce', 'mentions', 'edit', 'buttons', 'templateButtons', 'sections', 'interactiveButtons', 'album', 'call', 'paymentInvite', 'order', 'keep', 'shop'];
18
+
19
+ // ===== UTILITIES =====
16
20
  export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0];
17
21
 
18
22
  export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
@@ -28,112 +32,133 @@ const assertColor = (color) => {
28
32
  return parseInt((hex.length <= 6 ? 'FF' + hex.padStart(6, '0') : hex), 16);
29
33
  };
30
34
 
31
- const createMediaMessage = async (uploadData, mediaType, options, cacheableKey) => {
32
- const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } =
33
- await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, {
34
- logger: options.logger,
35
- saveOriginalFileIfRequired: ['audio', 'image', 'video'].includes(mediaType),
36
- opts: options.options
37
- });
38
-
39
- const [{ mediaUrl, directPath }] = await Promise.all([
40
- options.upload(encFilePath, {
41
- fileEncSha256B64: fileEncSha256.toString('base64'),
42
- mediaType,
43
- timeoutMs: options.mediaUploadTimeoutMs
44
- }),
45
- (async () => {
46
- try {
47
- if (['image', 'video'].includes(mediaType) && !uploadData.jpegThumbnail) {
48
- const { thumbnail, originalImageDimensions } = await generateThumbnail(originalFilePath, mediaType, options);
49
- uploadData.jpegThumbnail = thumbnail;
50
- if (originalImageDimensions && !uploadData.width) {
51
- uploadData.width = originalImageDimensions.width;
52
- uploadData.height = originalImageDimensions.height;
53
- }
54
- }
55
- if (mediaType === 'audio' && !uploadData.seconds)
56
- uploadData.seconds = await getAudioDuration(originalFilePath);
57
- if (mediaType === 'audio' && uploadData.ptt)
58
- uploadData.waveform = await getAudioWaveform(originalFilePath, options.logger);
59
- if (options.backgroundColor && mediaType === 'audio' && uploadData.ptt)
60
- uploadData.backgroundArgb = assertColor(options.backgroundColor);
61
- } catch (e) { options.logger?.warn({ trace: e.stack }, 'failed to obtain extra info'); }
62
- })()
63
- ]).finally(async () => {
64
- try {
65
- await fs.unlink(encFilePath);
66
- if (originalFilePath) await fs.unlink(originalFilePath);
67
- } catch { }
68
- });
35
+ export const getContentType = (content) => {
36
+ if (!content) return;
37
+ const keys = Object.keys(content);
38
+ return keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
39
+ };
69
40
 
70
- const obj = WAProto.Message.fromObject({
71
- [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
72
- url: mediaUrl, directPath, mediaKey, fileEncSha256, fileSha256, fileLength,
73
- mediaKeyTimestamp: unixTimestampSeconds(),
74
- ...uploadData, media: undefined
75
- })
76
- });
41
+ export const normalizeMessageContent = (content) => {
42
+ if (!content) return;
43
+ for (let i = 0; i < 5; i++) {
44
+ const inner = content?.ephemeralMessage || content?.viewOnceMessage || content?.documentWithCaptionMessage || content?.viewOnceMessageV2 || content?.viewOnceMessageV2Extension || content?.editedMessage;
45
+ if (!inner) break;
46
+ content = inner.message;
47
+ }
48
+ return content;
49
+ };
77
50
 
78
- if (uploadData.ptv) { obj.ptvMessage = obj.videoMessage; delete obj.videoMessage; }
79
- if (cacheableKey) await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish());
80
- return obj;
51
+ export const extractMessageContent = (content) => {
52
+ content = normalizeMessageContent(content);
53
+ const extractTemplate = (msg) => msg.imageMessage ? { imageMessage: msg.imageMessage } : msg.documentMessage ? { documentMessage: msg.documentMessage } : msg.videoMessage ? { videoMessage: msg.videoMessage } : msg.locationMessage ? { locationMessage: msg.locationMessage } : { conversation: msg.contentText || msg.hydratedContentText || '' };
54
+ return content?.buttonsMessage ? extractTemplate(content.buttonsMessage) : content?.templateMessage?.hydratedFourRowTemplate ? extractTemplate(content.templateMessage.hydratedFourRowTemplate) : content?.templateMessage?.hydratedTemplate ? extractTemplate(content.templateMessage.hydratedTemplate) : content?.templateMessage?.fourRowTemplate ? extractTemplate(content.templateMessage.fourRowTemplate) : content;
81
55
  };
82
56
 
57
+ // ===== MEDIA PREPARATION =====
83
58
  export const prepareWAMessageMedia = async (message, options) => {
84
- let mediaType = MEDIA_KEYS.find(key => key in message);
85
- if (!mediaType) throw new Boom('Invalid media type', { statusCode: 400 });
59
+ let mediaType = MEDIA_KEYS.find(key => key in message)
60
+ if (!mediaType) throw new Boom('Invalid media type', { statusCode: 400 })
86
61
 
87
- const uploadData = { ...message, media: message[mediaType] };
88
- delete uploadData[mediaType];
62
+ const uploadData = { ...message, media: message[mediaType] }
63
+ delete uploadData[mediaType]
89
64
 
90
- const cacheableKey = typeof uploadData.media === 'object' && 'url' in uploadData.media && uploadData.media.url && options.mediaCache
91
- ? `${mediaType}:${uploadData.media.url.toString()}` : null;
65
+ const cacheableKey = typeof uploadData.media === 'object' && 'url' in uploadData.media && uploadData.media.url && options.mediaCache ? `${mediaType}:${uploadData.media.url.toString()}` : null
92
66
 
93
- if (mediaType === 'document' && !uploadData.fileName) uploadData.fileName = 'file';
94
- if (!uploadData.mimetype) uploadData.mimetype = MIMETYPE_MAP[mediaType];
67
+ if (mediaType === 'document' && !uploadData.fileName) uploadData.fileName = 'file'
68
+ if (!uploadData.mimetype) uploadData.mimetype = MIMETYPE_MAP[mediaType]
95
69
 
96
70
  if (cacheableKey) {
97
- const cached = await options.mediaCache?.get(cacheableKey);
71
+ const cached = await options.mediaCache?.get(cacheableKey)
98
72
  if (cached) {
99
- const obj = proto.Message.decode(cached);
100
- Object.assign(obj[`${mediaType}Message`], { ...uploadData, media: undefined });
101
- return obj;
73
+ const obj = proto.Message.decode(cached)
74
+ Object.assign(obj[`${mediaType}Message`], { ...uploadData, media: undefined })
75
+ return obj
102
76
  }
103
77
  }
104
78
 
105
- if (isJidNewsletter(options.jid)) {
106
- const { filePath, fileSha256, fileLength } = await getRawMediaUploadData(uploadData.media, options.mediaTypeOverride || mediaType, options.logger);
107
- const { mediaUrl, directPath } = await options.upload(filePath, {
108
- fileEncSha256B64: fileSha256.toString('base64'),
109
- mediaType, timeoutMs: options.mediaUploadTimeoutMs
110
- });
111
- await fs.unlink(filePath);
112
- const obj = WAProto.Message.fromObject({
113
- [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
114
- url: mediaUrl, directPath, fileSha256, fileLength, ...uploadData, media: undefined
115
- })
116
- });
117
- if (uploadData.ptv) { obj.ptvMessage = obj.videoMessage; delete obj.videoMessage; }
118
- if (obj.stickerMessage) obj.stickerMessage.stickerSentTs = Date.now();
119
- if (cacheableKey) await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish());
120
- return obj;
121
- }
79
+ const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
80
+ const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData.jpegThumbnail === 'undefined'
81
+ const requiresWaveformProcessing = mediaType === 'audio' && (uploadData.ptt === true || !!options.backgroundColor)
82
+ const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation
122
83
 
123
- return createMediaMessage(uploadData, mediaType, options, cacheableKey);
124
- };
84
+ const encryptionResult = await (options.newsletter ? prepareStream : encryptedStream)(uploadData.media, options.mediaTypeOverride || mediaType, {
85
+ logger: options.logger,
86
+ saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
87
+ opts: options.options,
88
+ isPtt: uploadData.ptt,
89
+ forceOpus: mediaType === 'audio' && uploadData.mimetype && uploadData.mimetype.includes('opus'),
90
+ convertVideo: mediaType === 'video'
91
+ })
92
+
93
+ // ✅ FIX: Extract the correct values based on encryption method
94
+ const { mediaKey, encWriteStream, bodyPath, fileEncSha256, fileSha256, fileLength, didSaveToTmpPath, opusConverted, encFilePath } = encryptionResult
125
95
 
126
- export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) =>
127
- WAProto.Message.fromObject({
128
- ephemeralMessage: {
129
- message: {
130
- protocolMessage: {
131
- type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
132
- ephemeralExpiration: ephemeralExpiration || 0
96
+ if (mediaType === 'audio' && opusConverted) uploadData.mimetype = 'audio/ogg; codecs=opus'
97
+
98
+ const fileEncSha256B64 = (options.newsletter ? fileSha256 : fileEncSha256 ?? fileSha256).toString('base64')
99
+
100
+ // ✅ FIX: Determine what to upload - use encFilePath for encrypted, encWriteStream for newsletter
101
+ const uploadStream = options.newsletter ? encWriteStream : (encFilePath || encWriteStream)
102
+
103
+ const [{ mediaUrl, directPath, handle }] = await Promise.all([
104
+ (async () => {
105
+ const result = await options.upload(uploadStream, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs })
106
+ options.logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
107
+ return result
108
+ })(),
109
+ (async () => {
110
+ try {
111
+ if (requiresThumbnailComputation) {
112
+ const { thumbnail, originalImageDimensions } = await generateThumbnail(bodyPath, mediaType, options)
113
+ uploadData.jpegThumbnail = thumbnail
114
+ if (!uploadData.width && originalImageDimensions) {
115
+ uploadData.width = originalImageDimensions.width
116
+ uploadData.height = originalImageDimensions.height
117
+ }
133
118
  }
134
- }
119
+ if (requiresDurationComputation) uploadData.seconds = await getAudioDuration(bodyPath)
120
+ if (requiresWaveformProcessing) {
121
+ try {
122
+ uploadData.waveform = await getAudioWaveform(bodyPath, options.logger)
123
+ } catch (err) {
124
+ options.logger?.warn('Failed to generate waveform, using fallback')
125
+ 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])
126
+ }
127
+ }
128
+ if (options.backgroundColor && mediaType === 'audio') uploadData.backgroundArgb = assertColor(options.backgroundColor)
129
+ } catch (e) { options.logger?.warn({ trace: e.stack }, 'failed to obtain extra info') }
130
+ })()
131
+ ]).finally(async () => {
132
+ if (encWriteStream && !Buffer.isBuffer(encWriteStream)) encWriteStream.destroy?.()
133
+ // ✅ FIX: Clean up encrypted file path
134
+ if (encFilePath && typeof encFilePath === 'string') {
135
+ try {
136
+ await fs.unlink(encFilePath)
137
+ } catch {}
135
138
  }
136
- });
139
+ if (didSaveToTmpPath && bodyPath) {
140
+ try {
141
+ await fs.access(bodyPath)
142
+ await fs.unlink(bodyPath)
143
+ } catch { }
144
+ }
145
+ })
146
+
147
+ const obj = WAProto.Message.fromObject({
148
+ [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
149
+ url: handle ? undefined : mediaUrl, directPath, mediaKey, fileEncSha256, fileSha256, fileLength,
150
+ mediaKeyTimestamp: handle ? undefined : unixTimestampSeconds(), ...uploadData, media: undefined
151
+ })
152
+ })
153
+
154
+ if (uploadData.ptv) { obj.ptvMessage = obj.videoMessage; delete obj.videoMessage }
155
+ if (cacheableKey) await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish())
156
+ return obj
157
+ }
158
+
159
+ export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => WAProto.Message.fromObject({
160
+ ephemeralMessage: { message: { protocolMessage: { type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: ephemeralExpiration || 0 } } }
161
+ });
137
162
 
138
163
  export const generateForwardMessageContent = (message, forceForward) => {
139
164
  const content = proto.Message.decode(proto.Message.encode(normalizeMessageContent(message.message)).finish());
@@ -150,33 +175,26 @@ export const generateForwardMessageContent = (message, forceForward) => {
150
175
  return content;
151
176
  };
152
177
 
178
+ // ===== MESSAGE HANDLERS =====
153
179
  const handleTextMessage = async (message, options) => {
154
180
  const extContent = { text: message.text };
155
181
  let urlInfo = message.linkPreview || await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger);
156
182
 
157
183
  if (urlInfo) {
158
184
  Object.assign(extContent, {
159
- matchedText: urlInfo['matched-text'],
160
- jpegThumbnail: urlInfo.jpegThumbnail,
161
- description: urlInfo.description,
162
- title: urlInfo.title,
163
- previewType: 0
185
+ matchedText: urlInfo['matched-text'], jpegThumbnail: urlInfo.jpegThumbnail,
186
+ description: urlInfo.description, title: urlInfo.title, previewType: 0
164
187
  });
165
188
  if (urlInfo.highQualityThumbnail) {
166
189
  const img = urlInfo.highQualityThumbnail;
167
190
  Object.assign(extContent, {
168
- thumbnailDirectPath: img.directPath,
169
- mediaKey: img.mediaKey,
170
- mediaKeyTimestamp: img.mediaKeyTimestamp,
171
- thumbnailWidth: img.width,
172
- thumbnailHeight: img.height,
173
- thumbnailSha256: img.fileSha256,
174
- thumbnailEncSha256: img.fileEncSha256
191
+ thumbnailDirectPath: img.directPath, mediaKey: img.mediaKey, mediaKeyTimestamp: img.mediaKeyTimestamp,
192
+ thumbnailWidth: img.width, thumbnailHeight: img.height, thumbnailSha256: img.fileSha256, thumbnailEncSha256: img.fileEncSha256
175
193
  });
176
194
  }
177
195
  }
178
196
 
179
- if (options.backgroundColor) extContent.backgroundArgb = await assertColor(options.backgroundColor);
197
+ if (options.backgroundColor) extContent.backgroundArgb = assertColor(options.backgroundColor);
180
198
  if (options.font) extContent.font = options.font;
181
199
  return { extendedTextMessage: extContent };
182
200
  };
@@ -185,9 +203,7 @@ const handleSpecialMessages = async (message, options) => {
185
203
  if ('contacts' in message) {
186
204
  const { contacts } = message.contacts;
187
205
  if (!contacts.length) throw new Boom('require atleast 1 contact', { statusCode: 400 });
188
- return contacts.length === 1
189
- ? { contactMessage: WAProto.Message.ContactMessage.create(contacts[0]) }
190
- : { contactsArrayMessage: WAProto.Message.ContactsArrayMessage.create(message.contacts) };
206
+ return contacts.length === 1 ? { contactMessage: WAProto.Message.ContactMessage.create(contacts[0]) } : { contactsArrayMessage: WAProto.Message.ContactsArrayMessage.create(message.contacts) };
191
207
  }
192
208
  if ('location' in message) return { locationMessage: WAProto.Message.LocationMessage.create(message.location) };
193
209
  if ('react' in message) {
@@ -197,9 +213,7 @@ const handleSpecialMessages = async (message, options) => {
197
213
  if ('delete' in message) return { protocolMessage: { key: message.delete, type: WAProto.Message.ProtocolMessage.Type.REVOKE } };
198
214
  if ('forward' in message) return generateForwardMessageContent(message.forward, message.force);
199
215
  if ('disappearingMessagesInChat' in message) {
200
- const exp = typeof message.disappearingMessagesInChat === 'boolean'
201
- ? message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0
202
- : message.disappearingMessagesInChat;
216
+ const exp = typeof message.disappearingMessagesInChat === 'boolean' ? (message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : message.disappearingMessagesInChat;
203
217
  return prepareDisappearingMessageSettingContent(exp);
204
218
  }
205
219
  return null;
@@ -208,11 +222,8 @@ const handleSpecialMessages = async (message, options) => {
208
222
  const handleGroupInvite = async (message, options) => {
209
223
  const m = {
210
224
  groupInviteMessage: {
211
- inviteCode: message.groupInvite.inviteCode,
212
- inviteExpiration: message.groupInvite.inviteExpiration,
213
- caption: message.groupInvite.text,
214
- groupJid: message.groupInvite.jid,
215
- groupName: message.groupInvite.subject
225
+ inviteCode: message.groupInvite.inviteCode, inviteExpiration: message.groupInvite.inviteExpiration,
226
+ caption: message.groupInvite.text, groupJid: message.groupInvite.jid, groupName: message.groupInvite.subject
216
227
  }
217
228
  };
218
229
 
@@ -230,14 +241,10 @@ const handleEventMessage = (message, options) => {
230
241
  const startTime = Math.floor(message.event.startDate.getTime() / 1000);
231
242
  const m = {
232
243
  eventMessage: {
233
- name: message.event.name,
234
- description: message.event.description,
235
- startTime,
244
+ name: message.event.name, description: message.event.description, startTime,
236
245
  endTime: message.event.endDate ? message.event.endDate.getTime() / 1000 : undefined,
237
- isCanceled: message.event.isCancelled ?? false,
238
- extraGuestsAllowed: message.event.extraGuestsAllowed,
239
- isScheduleCall: message.event.isScheduleCall ?? false,
240
- location: message.event.location
246
+ isCanceled: message.event.isCancelled ?? false, extraGuestsAllowed: message.event.extraGuestsAllowed,
247
+ isScheduleCall: message.event.isScheduleCall ?? false, location: message.event.location
241
248
  },
242
249
  messageContextInfo: { messageSecret: message.event.messageSecret || randomBytes(32) }
243
250
  };
@@ -258,12 +265,7 @@ const handlePollMessage = (message) => {
258
265
  if (message.poll.selectableCount < 0 || message.poll.selectableCount > message.poll.values.length)
259
266
  throw new Boom(`poll.selectableCount should be >= 0 and <= ${message.poll.values.length}`, { statusCode: 400 });
260
267
 
261
- const pollMsg = {
262
- name: message.poll.name,
263
- selectableOptionsCount: message.poll.selectableCount,
264
- options: message.poll.values.map(optionName => ({ optionName }))
265
- };
266
-
268
+ const pollMsg = { name: message.poll.name, selectableOptionsCount: message.poll.selectableCount, options: message.poll.values.map(optionName => ({ optionName })) };
267
269
  const m = { messageContextInfo: { messageSecret: message.poll.messageSecret || randomBytes(32) } };
268
270
  if (message.poll.toAnnouncementGroup) m.pollCreationMessageV2 = pollMsg;
269
271
  else if (message.poll.selectableCount === 1) m.pollCreationMessageV3 = pollMsg;
@@ -273,24 +275,14 @@ const handlePollMessage = (message) => {
273
275
 
274
276
  const handleProductMessage = async (message, options) => {
275
277
  const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options);
276
- return {
277
- productMessage: WAProto.Message.ProductMessage.create({
278
- ...message,
279
- product: { ...message.product, productImage: imageMessage }
280
- })
281
- };
278
+ return { productMessage: WAProto.Message.ProductMessage.create({ ...message, product: { ...message.product, productImage: imageMessage } }) };
282
279
  };
283
280
 
284
281
  const handleRequestPayment = async (message, options) => {
285
- const sticker = message.requestPayment.sticker
286
- ? await prepareWAMessageMedia({ sticker: message.requestPayment.sticker }, options)
287
- : null;
288
-
282
+ const sticker = message.requestPayment.sticker ? await prepareWAMessageMedia({ sticker: message.requestPayment.sticker }, options) : null;
289
283
  let notes = message.requestPayment.sticker
290
284
  ? { stickerMessage: { ...sticker.stickerMessage, contextInfo: message.requestPayment.contextInfo } }
291
- : message.requestPayment.note
292
- ? { extendedTextMessage: { text: message.requestPayment.note, contextInfo: message.requestPayment.contextInfo } }
293
- : null;
285
+ : message.requestPayment.note ? { extendedTextMessage: { text: message.requestPayment.note, contextInfo: message.requestPayment.contextInfo } } : null;
294
286
 
295
287
  if (!notes) throw new Boom('Invalid request payment', { statusCode: 400 });
296
288
 
@@ -300,8 +292,7 @@ const handleRequestPayment = async (message, options) => {
300
292
  amount1000: message.requestPayment.amount1000 || message.requestPayment.amount,
301
293
  currencyCodeIso4217: message.requestPayment.currencyCodeIso4217 || message.requestPayment.currency,
302
294
  requestFrom: message.requestPayment.requestFrom || message.requestPayment.from,
303
- noteMessage: notes,
304
- background: message.requestPayment.background
295
+ noteMessage: notes, background: message.requestPayment.background
305
296
  })
306
297
  };
307
298
 
@@ -314,90 +305,199 @@ const handleRequestPayment = async (message, options) => {
314
305
  return m;
315
306
  };
316
307
 
317
- export const generateWAMessageContent = async (message, options) => {
308
+ // ===== MAIN GENERATOR =====
309
+ export const generateWAMessageContent = async (message, options = {}) => {
310
+ const messageKeys = Object.keys(message);
311
+
312
+ // ===== SMART DETECTION =====
313
+ const isRawProtoMessage = messageKeys.some(key =>
314
+ key.endsWith('Message') &&
315
+ typeof message[key] === 'object' &&
316
+ !HIGH_LEVEL_KEYS.includes(key)
317
+ );
318
+
319
+ const isWrapperMessage = ['viewOnceMessage', 'ephemeralMessage', 'viewOnceMessageV2', 'documentWithCaptionMessage'].some(k => k in message);
320
+
321
+ // Pass through raw protocol messages directly
322
+ if ((isRawProtoMessage || isWrapperMessage) && messageKeys.length === 1) {
323
+ return WAProto.Message.create(message);
324
+ }
325
+
326
+ // If no high-level keys AND has proto message keys, pass through
327
+ if (!messageKeys.some(k => HIGH_LEVEL_KEYS.includes(k)) && isRawProtoMessage) {
328
+ return WAProto.Message.create(message);
329
+ }
330
+
318
331
  let m = {};
319
332
 
320
- if ('text' in message) m = await handleTextMessage(message, options);
333
+ // ===== HANDLE TEXT =====
334
+ if ('text' in message && !('buttons' in message) && !('templateButtons' in message) && !('sections' in message) && !('interactiveButtons' in message) && !('shop' in message)) {
335
+ m = await handleTextMessage(message, options);
336
+ }
337
+
338
+ // ===== HANDLE SPECIAL MESSAGES =====
321
339
  else {
322
340
  const special = await handleSpecialMessages(message, options);
323
341
  if (special) m = special;
324
342
  else if ('groupInvite' in message) m = await handleGroupInvite(message, options);
325
343
  else if ('stickerPack' in message) return await prepareStickerPackMessage(message.stickerPack, options);
326
- else if ('pin' in message) m = {
327
- pinInChatMessage: { key: message.pin, type: message.type, senderTimestampMs: Date.now() },
328
- messageContextInfo: { messageAddOnDurationInSecs: message.type === 1 ? message.time || 86400 : 0 }
329
- };
330
- else if ('buttonReply' in message) {
331
- m = message.type === 'template'
332
- ? { templateButtonReplyMessage: { selectedDisplayText: message.buttonReply.displayText, selectedId: message.buttonReply.id, selectedIndex: message.buttonReply.index } }
333
- : { buttonsResponseMessage: { selectedButtonId: message.buttonReply.id, selectedDisplayText: message.buttonReply.displayText, type: 0 } };
344
+ else if ('pin' in message) {
345
+ const messageKey = typeof message.pin === 'boolean' ? (options.quoted?.key || (() => { throw new Boom('No quoted message key found for pin operation'); })()) : (message.pin && typeof message.pin === 'object') ? (message.pin.key || message.pin.stanzaId || (message.pin.id ? { remoteJid: options.jid, fromMe: message.pin.fromMe || false, id: message.pin.id, participant: message.pin.participant || message.pin.sender } : null)) : message.pin;
346
+ const shouldPin = typeof message.pin === 'boolean' ? message.pin : (message.pin && typeof message.pin === 'object' ? message.pin.unpin !== true : true);
347
+ const pinTime = message.pin && typeof message.pin === 'object' ? message.pin.time : message.time;
348
+ if (!messageKey || !messageKey.id) throw new Boom('Invalid message key for pin operation');
349
+ m = { pinInChatMessage: { key: messageKey, type: shouldPin ? 1 : 2, senderTimestampMs: Date.now().toString() }, messageContextInfo: { messageAddOnDurationInSecs: shouldPin ? (pinTime || 86400) : 0 } };
334
350
  }
351
+ else if ('keep' in message) m = { keepInChatMessage: { key: message.keep, keepType: message.type, timestampMs: Date.now() } };
352
+ else if ('call' in message) m = { scheduledCallCreationMessage: { scheduledTimestampMs: message.call.time || Date.now(), callType: message.call.type || 1, title: message.call.title } };
353
+ else if ('paymentInvite' in message) m = { paymentInviteMessage: { serviceType: message.paymentInvite.type, expiryTimestamp: message.paymentInvite.expiry } };
354
+ else if ('buttonReply' in message) m = message.type === 'template' ? { templateButtonReplyMessage: { selectedDisplayText: message.buttonReply.displayText, selectedId: message.buttonReply.id, selectedIndex: message.buttonReply.index } } : { buttonsResponseMessage: { selectedButtonId: message.buttonReply.id, selectedDisplayText: message.buttonReply.displayText, type: 0 } };
335
355
  else if ('ptv' in message && message.ptv) {
336
356
  const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options);
337
357
  m = { ptvMessage: videoMessage };
338
358
  }
339
359
  else if ('product' in message) m = await handleProductMessage(message, options);
360
+ else if ('order' in message) m = { orderMessage: WAProto.Message.OrderMessage.fromObject({ orderId: message.order.id, thumbnail: message.order.thumbnail, itemCount: message.order.itemCount, status: message.order.status, surface: message.order.surface, orderTitle: message.order.title, message: message.order.text, sellerJid: message.order.seller, token: message.order.token, totalAmount1000: message.order.amount, totalCurrencyCode: message.order.currency }) };
340
361
  else if ('listReply' in message) m = { listResponseMessage: { ...message.listReply } };
341
362
  else if ('event' in message) m = handleEventMessage(message, options);
342
363
  else if ('poll' in message) m = handlePollMessage(message);
343
- else if ('inviteAdmin' in message) m = {
344
- newsletterAdminInviteMessage: {
345
- inviteExpiration: message.inviteAdmin.inviteExpiration,
346
- caption: message.inviteAdmin.text,
347
- newsletterJid: message.inviteAdmin.jid,
348
- newsletterName: message.inviteAdmin.subject,
349
- jpegThumbnail: message.inviteAdmin.thumbnail
350
- }
351
- };
364
+ else if ('inviteAdmin' in message) m = { newsletterAdminInviteMessage: { inviteExpiration: message.inviteAdmin.inviteExpiration, caption: message.inviteAdmin.text, newsletterJid: message.inviteAdmin.jid, newsletterName: message.inviteAdmin.subject, jpegThumbnail: message.inviteAdmin.thumbnail } };
352
365
  else if ('requestPayment' in message) m = await handleRequestPayment(message, options);
366
+ else if ('extendedTextMessage' in message) m = { extendedTextMessage: WAProto.Message.ExtendedTextMessage.create(message.extendedTextMessage) };
367
+ else if ('interactiveMessage' in message) m = { interactiveMessage: WAProto.Message.InteractiveMessage.create(message.interactiveMessage) };
353
368
  else if ('sharePhoneNumber' in message) m = { protocolMessage: { type: 4 } };
354
369
  else if ('requestPhoneNumber' in message) m = { requestPhoneNumberMessage: {} };
355
- else if ('limitSharing' in message) m = {
356
- protocolMessage: {
357
- type: 3,
358
- limitSharing: {
359
- sharingLimited: message.limitSharing === true,
360
- trigger: 1,
361
- limitSharingSettingTimestamp: Date.now(),
362
- initiatedByMe: true
370
+ else if ('limitSharing' in message) m = { protocolMessage: { type: 3, limitSharing: { sharingLimited: message.limitSharing === true, trigger: 1, limitSharingSettingTimestamp: Date.now(), initiatedByMe: true } } };
371
+ else if ('album' in message) {
372
+ const imageMessages = message.album.filter(item => 'image' in item);
373
+ const videoMessages = message.album.filter(item => 'video' in item);
374
+ m = { albumMessage: { expectedImageCount: imageMessages.length, expectedVideoCount: videoMessages.length } };
375
+ }
376
+ else if (MEDIA_KEYS.some(k => k in message)) m = await prepareWAMessageMedia(message, options);
377
+ }
378
+
379
+ // ===== SMART BUTTON HANDLING =====
380
+ if ('buttons' in message && Array.isArray(message.buttons) && message.buttons.length > 0) {
381
+ const hasNativeFlow = message.buttons.some(b => b.nativeFlowInfo || b.name || b.buttonParamsJson);
382
+
383
+ if (hasNativeFlow) {
384
+ // Convert to interactiveMessage
385
+ const interactive = {
386
+ body: { text: message.text || message.caption || message.contentText || '' },
387
+ footer: { text: message.footer || message.footerText || '' },
388
+ nativeFlowMessage: {
389
+ buttons: message.buttons.map(btn => {
390
+ if (btn.name && btn.buttonParamsJson) return btn;
391
+ if (btn.nativeFlowInfo) return { name: btn.nativeFlowInfo.name, buttonParamsJson: btn.nativeFlowInfo.paramsJson };
392
+ return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: btn.buttonText?.displayText || btn.displayText || '', id: btn.buttonId || btn.id || '' }) };
393
+ })
363
394
  }
395
+ };
396
+
397
+ if (message.title) interactive.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message.hasMediaAttachment || false };
398
+ if (Object.keys(m).length > 0) {
399
+ interactive.header = interactive.header || { title: message.title || '', hasMediaAttachment: true };
400
+ Object.assign(interactive.header, m);
364
401
  }
365
- };
366
- else m = await prepareWAMessageMedia(message, options);
402
+
403
+ m = { interactiveMessage: interactive };
404
+ } else {
405
+ // Old-style buttons
406
+ const buttonsMessage = { buttons: message.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE })) };
407
+ if ('text' in message) { buttonsMessage.contentText = message.text; buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.EMPTY; }
408
+ else { if ('caption' in message) buttonsMessage.contentText = message.caption; const type = Object.keys(m)[0]?.replace('Message', '').toUpperCase(); buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType[type] || proto.Message.ButtonsMessage.HeaderType.EMPTY; Object.assign(buttonsMessage, m); }
409
+ if (message.title) { buttonsMessage.text = message.title; buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.TEXT; }
410
+ if (message.footer) buttonsMessage.footerText = message.footer;
411
+ m = { buttonsMessage };
412
+ }
367
413
  }
368
-
369
- if ('viewOnce' in message && message.viewOnce) m = { viewOnceMessage: { message: m } };
370
- if ('mentions' in message && message.mentions?.length) {
371
- const key = m[Object.keys(m)[0]];
372
- if (key) key.contextInfo = { ...(key.contextInfo || {}), mentionedJid: message.mentions };
414
+
415
+ // ===== TEMPLATE BUTTONS =====
416
+ else if ('templateButtons' in message && !!message.templateButtons) {
417
+ const msg = { hydratedButtons: message.templateButtons };
418
+ if ('text' in message) msg.hydratedContentText = message.text;
419
+ else { if ('caption' in message) msg.hydratedContentText = message.caption; Object.assign(msg, m); }
420
+ if ('footer' in message && !!message.footer) msg.hydratedFooterText = message.footer;
421
+ m = { templateMessage: { fourRowTemplate: msg, hydratedTemplate: msg } };
373
422
  }
374
- if ('edit' in message) m = {
375
- protocolMessage: {
376
- key: message.edit,
377
- editedMessage: m,
378
- timestampMs: Date.now(),
379
- type: 1
423
+
424
+ // ===== LIST MESSAGE =====
425
+ else if ('sections' in message && !!message.sections) {
426
+ m = { listMessage: { sections: message.sections, buttonText: message.buttonText, title: message.title, footerText: message.footer, description: message.text, listType: proto.Message.ListMessage.ListType.SINGLE_SELECT } };
427
+ }
428
+
429
+ // ===== INTERACTIVE BUTTONS =====
430
+ else if ('interactiveButtons' in message && !!message.interactiveButtons) {
431
+ const interactiveMessage = { nativeFlowMessage: WAProto.Message.InteractiveMessage.NativeFlowMessage.fromObject({ buttons: message.interactiveButtons }) };
432
+ if ('text' in message) interactiveMessage.body = { text: message.text };
433
+ else if ('caption' in message) { interactiveMessage.body = { text: message.caption }; interactiveMessage.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message?.media ?? false }; Object.assign(interactiveMessage.header, m); }
434
+ if ('footer' in message && !!message.footer) interactiveMessage.footer = { text: message.footer };
435
+ if ('title' in message && !!message.title) { interactiveMessage.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message?.media ?? false }; Object.assign(interactiveMessage.header, m); }
436
+ m = { interactiveMessage };
437
+ }
438
+
439
+ // ===== SHOP MESSAGE (YOUR EXAMPLE) =====
440
+ else if ('shop' in message && !!message.shop) {
441
+ const interactiveMessage = {
442
+ shopStorefrontMessage: WAProto.Message.InteractiveMessage.ShopMessage.fromObject({
443
+ surface: message.shop.surface || 1,
444
+ id: message.shop.id || message.id
445
+ })
446
+ };
447
+
448
+ // Handle body text
449
+ if ('text' in message) interactiveMessage.body = { text: message.text };
450
+ else if ('caption' in message) interactiveMessage.body = { text: message.caption };
451
+
452
+ // Handle header with media
453
+ if (message.title || message.subtitle || Object.keys(m).length > 0) {
454
+ interactiveMessage.header = {
455
+ title: message.title || '',
456
+ subtitle: message.subtitle || '',
457
+ hasMediaAttachment: message.hasMediaAttachment ?? (Object.keys(m).length > 0)
458
+ };
459
+ if (Object.keys(m).length > 0) Object.assign(interactiveMessage.header, m);
380
460
  }
381
- };
382
- if ('contextInfo' in message && message.contextInfo) {
383
- const key = m[Object.keys(m)[0]];
384
- if (key) key.contextInfo = { ...key.contextInfo, ...message.contextInfo };
461
+
462
+ if ('footer' in message && !!message.footer) interactiveMessage.footer = { text: message.footer };
463
+
464
+ m = { interactiveMessage };
385
465
  }
386
466
 
467
+ // ===== AUTO-APPLY CONTEXT & WRAPPERS =====
468
+ const finalKey = Object.keys(m)[0];
469
+
470
+ // Auto-merge contextInfo and mentions
471
+ if ((message.contextInfo || message.mentions) && finalKey && m[finalKey]) {
472
+ m[finalKey].contextInfo = {
473
+ ...(m[finalKey].contextInfo || {}),
474
+ ...(message.contextInfo || {}),
475
+ mentionedJid: message.mentions || message.contextInfo?.mentionedJid || []
476
+ };
477
+ }
478
+
479
+ // ViewOnce wrapper
480
+ if (message.viewOnce === true) m = { viewOnceMessage: { message: m } };
481
+
482
+ // Edit wrapper
483
+ if (message.edit) m = { protocolMessage: { key: message.edit, editedMessage: m, timestampMs: Date.now(), type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT } };
484
+
387
485
  return WAProto.Message.create(m);
388
486
  };
389
487
 
390
- export const generateWAMessageFromContent = (jid, message, options) => {
488
+
489
+ export const generateWAMessageFromContent = (jid, message, options = {}) => {
391
490
  if (!options.timestamp) options.timestamp = new Date();
392
491
  const innerMessage = normalizeMessageContent(message);
393
492
  const key = getContentType(innerMessage);
394
493
  const timestamp = unixTimestampSeconds(options.timestamp);
395
494
  const { quoted, userJid } = options;
396
495
 
397
- if (quoted && !isJidNewsletter(jid)) {
496
+ if (quoted && key && !isJidNewsletter(jid)) {
398
497
  const participant = quoted.key.fromMe ? userJid : quoted.participant || quoted.key.participant || quoted.key.remoteJid;
399
498
  const quotedMsg = proto.Message.create({ [getContentType(normalizeMessageContent(quoted.message))]: normalizeMessageContent(quoted.message)[getContentType(normalizeMessageContent(quoted.message))] });
400
- const contextInfo = (innerMessage[key]?.contextInfo) || {};
499
+ if (!innerMessage[key]) innerMessage[key] = {};
500
+ const contextInfo = innerMessage[key].contextInfo || {};
401
501
  contextInfo.participant = jidNormalizedUser(participant);
402
502
  contextInfo.stanzaId = quoted.key.id;
403
503
  contextInfo.quotedMessage = quotedMsg;
@@ -405,12 +505,27 @@ export const generateWAMessageFromContent = (jid, message, options) => {
405
505
  innerMessage[key].contextInfo = contextInfo;
406
506
  }
407
507
 
408
- if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
508
+ if (options?.ephemeralExpiration && key && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
509
+ if (!innerMessage[key]) innerMessage[key] = {};
409
510
  innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL };
410
511
  }
411
512
 
513
+ const findMessageKey = (obj) => {
514
+ if (!obj || typeof obj !== 'object') return null;
515
+ if (obj.key?.id) return obj.key.id;
516
+ for (const value of Object.values(obj)) {
517
+ if (value && typeof value === 'object') {
518
+ const found = findMessageKey(value);
519
+ if (found) return found;
520
+ }
521
+ }
522
+ return null;
523
+ };
524
+
525
+ const messageId = findMessageKey(innerMessage) || options?.messageId || generateMessageIDV2();
526
+
412
527
  return WAProto.WebMessageInfo.fromObject({
413
- key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2() },
528
+ key: { remoteJid: jid, fromMe: true, id: messageId },
414
529
  message: WAProto.Message.create(message),
415
530
  messageTimestamp: timestamp,
416
531
  messageStubParameters: [],
@@ -419,34 +534,12 @@ export const generateWAMessageFromContent = (jid, message, options) => {
419
534
  });
420
535
  };
421
536
 
422
- export const generateWAMessage = async (jid, content, options) => {
537
+ export const generateWAMessage = async (jid, content, options = {}) => {
423
538
  options.logger = options?.logger?.child({ msgId: options.messageId });
424
539
  return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options);
425
540
  };
426
541
 
427
- export const getContentType = (content) => {
428
- if (!content) return;
429
- const keys = Object.keys(content);
430
- return keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
431
- };
432
-
433
- export const normalizeMessageContent = (content) => {
434
- if (!content) return;
435
- for (let i = 0; i < 5; i++) {
436
- const inner = content?.ephemeralMessage || content?.viewOnceMessage || content?.documentWithCaptionMessage || content?.viewOnceMessageV2 || content?.viewOnceMessageV2Extension || content?.editedMessage;
437
- if (!inner) break;
438
- content = inner.message;
439
- }
440
- return content;
441
- };
442
-
443
- export const extractMessageContent = (content) => {
444
- content = normalizeMessageContent(content);
445
- const extractTemplate = (msg) => msg.imageMessage ? { imageMessage: msg.imageMessage } : msg.documentMessage ? { documentMessage: msg.documentMessage } : msg.videoMessage ? { videoMessage: msg.videoMessage } : msg.locationMessage ? { locationMessage: msg.locationMessage } : { conversation: msg.contentText || msg.hydratedContentText || '' };
446
-
447
- return content?.buttonsMessage ? extractTemplate(content.buttonsMessage) : content?.templateMessage?.hydratedFourRowTemplate ? extractTemplate(content.templateMessage.hydratedFourRowTemplate) : content?.templateMessage?.hydratedTemplate ? extractTemplate(content.templateMessage.hydratedTemplate) : content?.templateMessage?.fourRowTemplate ? extractTemplate(content.templateMessage.fourRowTemplate) : content;
448
- };
449
-
542
+ // ===== UTILITIES =====
450
543
  export const getDevice = (id) => /^3A.{18}$/.test(id) ? 'ios' : /^3E.{20}$/.test(id) ? 'web' : /^(.{21}|.{32})$/.test(id) ? 'android' : /^(3F|.{18}$)/.test(id) ? 'desktop' : 'unknown';
451
544
 
452
545
  export const updateMessageWithReceipt = (msg, receipt) => {
@@ -506,9 +599,7 @@ const REUPLOAD_STATUS = [410, 404];
506
599
  export const downloadMediaMessage = async (message, type, options, ctx) => {
507
600
  const downloadMsg = async () => {
508
601
  let normalizedMessage = message;
509
- if (!message.message && message.key && message.participant) {
510
- normalizedMessage = { key: message.key, message: message, messageTimestamp: message.messageTimestamp };
511
- }
602
+ if (!message.message && message.key && message.participant) normalizedMessage = { key: message.key, message: message, messageTimestamp: message.messageTimestamp };
512
603
  if (!normalizedMessage.message && typeof message === 'object') {
513
604
  const possibleMessage = message.message || message.quoted?.message || message;
514
605
  normalizedMessage = { key: message.key || {}, message: possibleMessage, messageTimestamp: message.messageTimestamp };
@@ -524,9 +615,7 @@ export const downloadMediaMessage = async (message, type, options, ctx) => {
524
615
  const stream = await downloadContentFromMessage(download, mediaType, options);
525
616
  if (type === 'buffer') {
526
617
  const chunks = [];
527
- for await (const chunk of stream) {
528
- chunks.push(chunk);
529
- }
618
+ for await (const chunk of stream) chunks.push(chunk);
530
619
  return Buffer.concat(chunks);
531
620
  }
532
621
  return stream;
@@ -553,24 +642,18 @@ export async function prepareStickerPackMessage(stickerPack, options) {
553
642
  const { stream } = await getStream(s.data);
554
643
  let buffer = await toBuffer(stream);
555
644
  const isWebP = buffer.length >= 12 && buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46;
556
-
557
645
  if (!isWebP) {
558
646
  if ('sharp' in lib) buffer = await lib.sharp.default(buffer).webp().toBuffer();
559
647
  else if ('jimp' in lib) buffer = await lib.jimp.Jimp.read(buffer).then(img => img.getBuffer('image/webp'));
560
648
  }
561
-
562
649
  if (buffer.length > 1024 * 1024) {
563
650
  if ('sharp' in lib) buffer = await lib.sharp.default(buffer).webp({ quality: 50 }).toBuffer();
564
651
  if (buffer.length > 1024 * 1024) continue;
565
652
  }
566
-
567
653
  validStickers.push({
568
654
  fileName: `${sha256(buffer).toString('base64').replace(/\//g, '-')}.webp`,
569
- buffer,
570
- mimetype: 'image/webp',
571
- isAnimated: s.isAnimated || false,
572
- emojis: s.emojis || [],
573
- accessibilityLabel: s.accessibilityLabel
655
+ buffer, mimetype: 'image/webp', isAnimated: s.isAnimated || false,
656
+ emojis: s.emojis || [], accessibilityLabel: s.accessibilityLabel
574
657
  });
575
658
  } catch (e) { options.logger?.warn(`Sticker failed: ${e.message}`); }
576
659
  }
@@ -580,70 +663,55 @@ export async function prepareStickerPackMessage(stickerPack, options) {
580
663
  const { stream: covStream } = await getStream(stickerPack.cover);
581
664
  let coverBuffer = await toBuffer(covStream);
582
665
  const isWebPCover = coverBuffer.length >= 12 && coverBuffer[0] === 0x52 && coverBuffer[1] === 0x49 && coverBuffer[2] === 0x46 && coverBuffer[3] === 0x46;
583
- if (!isWebPCover) {
584
- if ('sharp' in lib) coverBuffer = await lib.sharp.default(coverBuffer).webp().toBuffer();
585
- else if ('jimp' in lib) coverBuffer = await lib.jimp.Jimp.read(coverBuffer).then(img => img.getBuffer('image/webp'));
586
- }
666
+ if (!isWebPCover) {
667
+ if ('sharp' in lib) coverBuffer = await lib.sharp.default(coverBuffer).webp().toBuffer();
668
+ else if ('jimp' in lib) coverBuffer = await lib.jimp.Jimp.read(coverBuffer).then(img => img.getBuffer('image/webp'));
669
+ }
587
670
 
588
- const processBatch = async (batch, batchIdx) => {
589
- const batchData = {};
590
- batch.forEach(s => { batchData[s.fileName] = [new Uint8Array(s.buffer), { level: 0 }]; });
591
- const trayFile = `${packId_}_batch${batchIdx}.webp`;
592
- batchData[trayFile] = [new Uint8Array(coverBuffer), { level: 0 }];
671
+ const processBatch = async (batch, batchIdx) => {
672
+ const batchData = {};
673
+ batch.forEach(s => { batchData[s.fileName] = [new Uint8Array(s.buffer), { level: 0 }]; });
674
+ const trayFile = `${packId_}_batch${batchIdx}.webp`;
675
+ batchData[trayFile] = [new Uint8Array(coverBuffer), { level: 0 }];
593
676
 
594
- const zipBuf = await new Promise((resolve, reject) => {
595
- zip(batchData, (err, data) => err ? reject(err) : resolve(Buffer.from(data)));
596
- });
597
-
598
- const upload = await encryptedStream(zipBuf, 'sticker-pack', { logger: options.logger, opts: options.options });
599
- const uploadRes = await options.upload(upload.encFilePath, {
600
- fileEncSha256B64: upload.fileEncSha256.toString('base64'),
601
- mediaType: 'sticker-pack',
602
- timeoutMs: options.mediaUploadTimeoutMs
603
- });
604
- await fs.unlink(upload.encFilePath);
605
-
606
- let thumbBuf;
607
- if ('sharp' in lib) thumbBuf = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer();
608
- else if ('jimp' in lib) thumbBuf = await lib.jimp.Jimp.read(coverBuffer).then(img => img.resize({ w: 252, h: 252 }).getBuffer('image/jpeg'));
609
-
610
- let thumbUploadRes;
611
- if (thumbBuf?.length) {
612
- const thumbUpload = await encryptedStream(thumbBuf, 'thumbnail-sticker-pack', { logger: options.logger, opts: options.options, mediaKey: upload.mediaKey });
613
- thumbUploadRes = await options.upload(thumbUpload.encFilePath, {
614
- fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'),
615
- mediaType: 'thumbnail-sticker-pack',
616
- timeoutMs: options.mediaUploadTimeoutMs
677
+ const zipBuf = await new Promise((resolve, reject) => { zip(batchData, (err, data) => err ? reject(err) : resolve(Buffer.from(data))); });
678
+ const upload = await encryptedStream(zipBuf, 'sticker-pack', { logger: options.logger, opts: options.options });
679
+ const uploadRes = await options.upload(upload.encFilePath, {
680
+ fileEncSha256B64: upload.fileEncSha256.toString('base64'), mediaType: 'sticker-pack', timeoutMs: options.mediaUploadTimeoutMs
617
681
  });
618
- await fs.unlink(thumbUpload.encFilePath);
619
- }
682
+ await fs.unlink(upload.encFilePath);
620
683
 
621
- return {
622
- name: `${name} (${batchIdx + 1})`,
623
- publisher, packDescription: description,
624
- stickerPackId: `${packId_}_${batchIdx}`,
625
- stickerPackOrigin: WAProto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED,
626
- stickerPackSize: zipBuf.length,
627
- stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
628
- fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
629
- directPath: uploadRes.directPath, fileLength: upload.fileLength, mediaKeyTimestamp: unixTimestampSeconds(),
630
- trayIconFileName: trayFile,
631
- ...(thumbUploadRes && {
632
- thumbnailDirectPath: thumbUploadRes.directPath,
633
- thumbnailHeight: 252, thumbnailWidth: 252,
634
- imageDataHash: thumbBuf ? sha256(thumbBuf).toString('base64') : undefined
635
- })
684
+ let thumbBuf;
685
+ if ('sharp' in lib) thumbBuf = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer();
686
+ else if ('jimp' in lib) thumbBuf = await lib.jimp.Jimp.read(coverBuffer).then(img => img.resize({ w: 252, h: 252 }).getBuffer('image/jpeg'));
687
+
688
+ let thumbUploadRes;
689
+ if (thumbBuf?.length) {
690
+ const thumbUpload = await encryptedStream(thumbBuf, 'thumbnail-sticker-pack', { logger: options.logger, opts: options.options, mediaKey: upload.mediaKey });
691
+ thumbUploadRes = await options.upload(thumbUpload.encFilePath, {
692
+ fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'), mediaType: 'thumbnail-sticker-pack', timeoutMs: options.mediaUploadTimeoutMs
693
+ });
694
+ await fs.unlink(thumbUpload.encFilePath);
695
+ }
696
+
697
+ return {
698
+ name: `${name} (${batchIdx + 1})`, publisher, packDescription: description, stickerPackId: `${packId_}_${batchIdx}`,
699
+ stickerPackOrigin: WAProto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED, stickerPackSize: zipBuf.length,
700
+ stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
701
+ fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
702
+ directPath: uploadRes.directPath, fileLength: upload.fileLength, mediaKeyTimestamp: unixTimestampSeconds(), trayIconFileName: trayFile,
703
+ ...(thumbUploadRes && { thumbnailDirectPath: thumbUploadRes.directPath, thumbnailHeight: 252, thumbnailWidth: 252, imageDataHash: thumbBuf ? sha256(thumbBuf).toString('base64') : undefined })
704
+ };
636
705
  };
637
- };
638
706
 
639
- if (validStickers.length > 60) {
640
- const batches = [];
641
- for (let i = 0; i < validStickers.length; i += 60) batches.push(validStickers.slice(i, i + 60));
642
- const batchResults = await Promise.all(batches.map((b, i) => processBatch(b, i)));
643
- return { stickerPackMessage: batchResults, isBatched: true, batchCount: batches.length };
644
- }
707
+ if (validStickers.length > 60) {
708
+ const batches = [];
709
+ for (let i = 0; i < validStickers.length; i += 60) batches.push(validStickers.slice(i, i + 60));
710
+ const batchResults = await Promise.all(batches.map((b, i) => processBatch(b, i)));
711
+ return { stickerPackMessage: batchResults, isBatched: true, batchCount: batches.length };
712
+ }
645
713
 
646
- return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false };
714
+ return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false };
647
715
  }
648
716
 
649
717
  export const assertMediaContent = (content) => {
@@ -651,5 +719,4 @@ export const assertMediaContent = (content) => {
651
719
  const mediaContent = content?.documentMessage || content?.imageMessage || content?.videoMessage || content?.audioMessage || content?.stickerMessage;
652
720
  if (!mediaContent) throw new Boom('given message is not a media message', { statusCode: 400, data: content });
653
721
  return mediaContent;
654
- };
655
-
722
+ };