@nuiisweety/baileys 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,18 @@
1
1
  import { Boom } from '@hapi/boom';
2
2
  import { randomBytes } from 'crypto';
3
+ import { zip } from 'fflate';
3
4
  import { promises as fs } from 'fs';
4
5
  import {} from 'stream';
5
6
  import { proto } from '../../WAProto/index.js';
6
- import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
7
- import { WAMessageStatus, WAProto } from '../Types/index.js';
8
- import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
7
+ import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, DONATE_URL, LIBRARY_NAME, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
8
+ import { AssociationType, ButtonHeaderType, ButtonType, CarouselCardType, ListType, ProtocolType, WAMessageStatus, WAProto } from '../Types/index.js';
9
+ import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidNormalizedUser } from '../WABinary/index.js';
9
10
  import { sha256 } from './crypto.js';
10
11
  import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
11
- import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData } from './messages-media.js';
12
+ import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getImageProcessingLibrary, getRawMediaUploadData, getStream, toBuffer } from './messages-media.js';
13
+ import { prepareRichResponseMessage } from './rich-message-utils.js';
12
14
  import { shouldIncludeReportingToken } from './reporting-utils.js';
15
+ const CONCURRENCY_LIMIT = 10;
13
16
  const MIMETYPE_MAP = {
14
17
  image: 'image/jpeg',
15
18
  video: 'video/mp4',
@@ -261,19 +264,322 @@ export const generateForwardMessageContent = (message, forceForward) => {
261
264
  return content;
262
265
  };
263
266
  export const hasNonNullishProperty = (message, key) => {
264
- return (typeof message === 'object' &&
265
- message !== null &&
267
+ return message != null &&
268
+ typeof message === 'object' &&
266
269
  key in message &&
267
- message[key] !== null &&
268
- message[key] !== undefined);
270
+ message[key] != null;
271
+ };
272
+ export const hasOptionalProperty = (obj, key) => {
273
+ return obj != null &&
274
+ typeof obj === 'object' &&
275
+ key in obj &&
276
+ obj[key] != null;
277
+ };
278
+ // Validate album message media to avoid bug
279
+ export const hasValidAlbumMedia = (message) => {
280
+ return message.imageMessage ||
281
+ message.videoMessage;
282
+ };
283
+ export const hasValidInteractiveHeader = (message) => {
284
+ return message.imageMessage ||
285
+ message.videoMessage ||
286
+ message.documentMessage ||
287
+ message.productMessage ||
288
+ message.locationMessage;
289
+ };
290
+ // Validate carousel cards header to avoid bug
291
+ export const hasValidCarouselHeader = (message) => {
292
+ return message.imageMessage ||
293
+ message.videoMessage ||
294
+ message.productMessage;
295
+ };
296
+ // Extract product message into a standalone function so it can also be reused as the header for interactive messages
297
+ const prepareProductMessage = async (message, options) => {
298
+ if (!message.businessOwnerJid) {
299
+ throw new Boom('"businessOwnerJid" is missing from the content', { statusCode: 400 });
300
+ }
301
+ const { imageMessage } = await prepareWAMessageMedia({ image: message.image || message.product.productImage }, options);
302
+ const content = {
303
+ ...message,
304
+ product: {
305
+ currencyCode: 'IDR',
306
+ priceAmount1000: 1000,
307
+ title: LIBRARY_NAME,
308
+ ...message.product,
309
+ productImage: imageMessage
310
+ }
311
+ };
312
+ delete content.image;
313
+ return content;
314
+ };
315
+ const prepareStickerPackMessage = async (message, options) => {
316
+ const { cover, stickers = [], name = '📦 Sticker Pack', publisher = 'GitHub: nuiisweety', description = '🏷️ nuiisweety/baileys' } = message;
317
+ if (stickers.length > 60) {
318
+ throw new Boom('Sticker pack exceeds the maximum limit of 60 stickers', { statusCode: 400 });
319
+ }
320
+ if (stickers.length === 0) {
321
+ throw new Boom('Sticker pack must contain at least one sticker', { statusCode: 400 });
322
+ }
323
+ if (!cover) {
324
+ throw new Boom('Sticker pack must contain a cover', { statusCode: 400 });
325
+ }
326
+ const logger = options.logger;
327
+ let cacheableKey = false;
328
+ if (Array.isArray(stickers) && stickers.length && options.mediaCache) {
329
+ const urls = [];
330
+ for (let i = 0; i < stickers.length; i++) {
331
+ const data = stickers[i].data;
332
+ if (typeof data === 'object' && data?.url) {
333
+ urls.push(data.url);
334
+ }
335
+ }
336
+ if (urls.length > 0) {
337
+ cacheableKey = 'sticker:' + urls.join('@');
338
+ }
339
+ }
340
+ if (cacheableKey) {
341
+ const mediaBuff = await options.mediaCache.get(cacheableKey);
342
+ if (mediaBuff) {
343
+ logger?.debug({ cacheableKey }, 'got media cache hit');
344
+ return proto.Message.StickerPackMessage.decode(mediaBuff);
345
+ }
346
+ }
347
+ const lib = await getImageProcessingLibrary();
348
+ const hasSharp = 'sharp' in lib && !!lib.sharp?.default;
349
+ const hasImage = 'image' in lib && !!lib.image?.Transformer;
350
+ const hasJimp = 'jimp' in lib && !!lib.jimp?.Jimp;
351
+ if (!hasSharp && !hasImage) {
352
+ throw new Boom('No image processing library (sharp or @napi-rs/image) available for converting sticker to WebP.');
353
+ }
354
+ const stickerPackIdValue = generateMessageIDV2();
355
+ const stickerData = {};
356
+ const stickerMetadata = new Array(stickers.length);
357
+ for (let i = 0; i < stickers.length; i += CONCURRENCY_LIMIT) {
358
+ const promises = [];
359
+ const chunkEnd = Math.min(i + CONCURRENCY_LIMIT, stickers.length);
360
+ for (let j = i; j < chunkEnd; j++) {
361
+ promises.push((async (index) => {
362
+ const sticker = stickers[index];
363
+ const { stream } = await getStream(sticker.data);
364
+ const buffer = await toBuffer(stream);
365
+ let webpBuffer;
366
+ let isAnimated = false;
367
+ if (isWebPBuffer(buffer)) {
368
+ webpBuffer = buffer;
369
+ isAnimated = isAnimatedWebP(buffer);
370
+ }
371
+ else if (hasSharp) {
372
+ webpBuffer = await lib.sharp.default(buffer)
373
+ .resize(512, 512, { fit: 'inside' })
374
+ .webp({ quality: 80 })
375
+ .toBuffer();
376
+ }
377
+ else {
378
+ webpBuffer = await new lib.image.Transformer(buffer)
379
+ .resize(512, 512)
380
+ .webp(80);
381
+ }
382
+ if (webpBuffer.length > 1024 * 1024) {
383
+ throw new Boom(`Sticker at index ${index} exceeds the 1MB size limit`, { statusCode: 400 });
384
+ }
385
+ const hash = sha256(webpBuffer).toString('base64').replace(/\//g, '-');
386
+ const fileName = `${hash}.webp`;
387
+ stickerData[fileName] = [new Uint8Array(webpBuffer), { level: 0 }];
388
+ stickerMetadata[index] = {
389
+ fileName,
390
+ mimetype: 'image/webp',
391
+ isAnimated,
392
+ emojis: sticker.emojis || ['✨'],
393
+ accessibilityLabel: sticker.accessibilityLabel || '‎'
394
+ };
395
+ })(j));
396
+ }
397
+ await Promise.all(promises);
398
+ }
399
+ const trayIconFileName = `${stickerPackIdValue}.webp`;
400
+ const { stream: coverStream } = await getStream(cover);
401
+ const coverBuffer = await toBuffer(coverStream);
402
+ let coverWebpBuffer;
403
+ if (isWebPBuffer(coverBuffer)) {
404
+ coverWebpBuffer = coverBuffer;
405
+ }
406
+ else if (hasSharp) {
407
+ coverWebpBuffer = await lib.sharp.default(coverBuffer)
408
+ .resize(512, 512, { fit: 'inside' })
409
+ .webp({ quality: 80 })
410
+ .toBuffer();
411
+ }
412
+ else {
413
+ coverWebpBuffer = await new lib.image.Transformer(coverBuffer)
414
+ .resize(512, 512)
415
+ .webp(80);
416
+ }
417
+ stickerData[trayIconFileName] = [new Uint8Array(coverWebpBuffer), { level: 0 }];
418
+ const zipBuffer = await new Promise((resolve, reject) => {
419
+ zip(stickerData, (error, data) => error ? reject(error) : resolve(Buffer.from(data)));
420
+ });
421
+ const stickerPackUpload = await encryptedStream(zipBuffer, 'sticker-pack', {
422
+ logger,
423
+ opts: options.options
424
+ });
425
+ let stickerPackUploadResult;
426
+ try {
427
+ stickerPackUploadResult = await options.upload(stickerPackUpload.encFilePath, {
428
+ fileEncSha256B64: stickerPackUpload.fileEncSha256.toString('base64'),
429
+ mediaType: 'sticker-pack',
430
+ timeoutMs: options.mediaUploadTimeoutMs
431
+ });
432
+ }
433
+ finally {
434
+ fs.unlink(stickerPackUpload.encFilePath).catch(() => logger?.warn('failed to remove tmp file'));
435
+ }
436
+ const obj = {
437
+ name,
438
+ publisher,
439
+ stickerPackId: stickerPackIdValue,
440
+ packDescription: description,
441
+ stickerPackOrigin: proto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED,
442
+ stickerPackSize: zipBuffer.length,
443
+ stickers: stickerMetadata,
444
+ fileSha256: stickerPackUpload.fileSha256,
445
+ fileEncSha256: stickerPackUpload.fileEncSha256,
446
+ mediaKey: stickerPackUpload.mediaKey,
447
+ directPath: stickerPackUploadResult.directPath,
448
+ fileLength: stickerPackUpload.fileLength,
449
+ mediaKeyTimestamp: unixTimestampSeconds(),
450
+ trayIconFileName
451
+ };
452
+ try {
453
+ let thumbnailBuffer;
454
+ if (hasSharp) {
455
+ thumbnailBuffer = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer();
456
+ }
457
+ else if (hasImage) {
458
+ thumbnailBuffer = await new lib.image.Transformer(coverBuffer).resize(252, 252).jpeg();
459
+ }
460
+ else if (hasJimp) {
461
+ const jimpImage = await lib.jimp.Jimp.read(coverBuffer);
462
+ thumbnailBuffer = await jimpImage.resize({ w: 252, h: 252 }).getBuffer('image/jpeg');
463
+ }
464
+ else {
465
+ throw new Error('No image processing library available for thumbnail generation');
466
+ }
467
+ if (!thumbnailBuffer || thumbnailBuffer.length === 0) {
468
+ throw new Error('Failed to generate thumbnail buffer');
469
+ }
470
+ const thumbUpload = await encryptedStream(thumbnailBuffer, 'thumbnail-sticker-pack', {
471
+ logger,
472
+ opts: options.options,
473
+ mediaKey: stickerPackUpload.mediaKey
474
+ });
475
+ let thumbUploadResult;
476
+ try {
477
+ thumbUploadResult = await options.upload(thumbUpload.encFilePath, {
478
+ fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'),
479
+ mediaType: 'thumbnail-sticker-pack',
480
+ timeoutMs: options.mediaUploadTimeoutMs
481
+ });
482
+ }
483
+ finally {
484
+ fs.unlink(thumbUpload.encFilePath).catch(() => logger?.warn('failed to remove tmp file'));
485
+ }
486
+ Object.assign(obj, {
487
+ thumbnailDirectPath: thumbUploadResult.directPath,
488
+ thumbnailSha256: thumbUpload.fileSha256,
489
+ thumbnailEncSha256: thumbUpload.fileEncSha256,
490
+ thumbnailHeight: 252,
491
+ thumbnailWidth: 252,
492
+ imageDataHash: sha256(thumbnailBuffer).toString('base64')
493
+ });
494
+ }
495
+ catch (error) {
496
+ logger?.warn(`Thumbnail generation failed: ${error}`);
497
+ }
498
+ if (cacheableKey) {
499
+ logger?.debug({ cacheableKey }, 'set cache (background)');
500
+ options.mediaCache.set(cacheableKey, WAProto.Message.StickerPackMessage.encode(obj).finish());
501
+ }
502
+ return WAProto.Message.StickerPackMessage.fromObject(obj);
503
+ };
504
+ const prepareNativeFlowButtons = (message) => {
505
+ const buttons = message.nativeFlow;
506
+ const isButtonsFieldArray = Array.isArray(buttons);
507
+ const correctedField = isButtonsFieldArray ? buttons : buttons.buttons;
508
+ const messageParamsJson = {};
509
+ if (hasOptionalProperty(message, 'offerText') && !!message.offerText) {
510
+ Object.assign(messageParamsJson, {
511
+ limited_time_offer: {
512
+ text: message.offerText || LIBRARY_NAME,
513
+ url: message.offerUrl || DONATE_URL,
514
+ copy_code: message.offerCode,
515
+ expiration_time: message.offerExpiration
516
+ }
517
+ });
518
+ }
519
+ if (hasOptionalProperty(message, 'optionText') && !!message.optionText) {
520
+ Object.assign(messageParamsJson, {
521
+ bottom_sheet: {
522
+ in_thread_buttons_limit: 1,
523
+ divider_indices: Array.from({ length: correctedField.length }, (_, index) => index),
524
+ list_title: message.optionTitle || '📄 Select Options',
525
+ button_title: message.optionText
526
+ }
527
+ });
528
+ }
529
+ return {
530
+ buttons: correctedField.map(button => {
531
+ const buttonText = button.text || button.buttonText;
532
+ const buttonIcon = button.icon?.toUpperCase();
533
+ if (hasOptionalProperty(button, 'id') && !!button.id) {
534
+ return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: buttonText || '👉🏻 Click', id: button.id, icon: buttonIcon }) };
535
+ }
536
+ else if (hasOptionalProperty(button, 'copy') && !!button.copy) {
537
+ return { name: 'cta_copy', buttonParamsJson: JSON.stringify({ display_text: buttonText || '📋 Copy', copy_code: button.copy, icon: buttonIcon }) };
538
+ }
539
+ else if (hasOptionalProperty(button, 'url') && !!button.url) {
540
+ return { name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: buttonText || '🌐 Visit', url: button.url, merchant_url: button.url, webview_interaction: button.useWebview, icon: buttonIcon }) };
541
+ }
542
+ else if (hasOptionalProperty(button, 'call') && !!button.call) {
543
+ return { name: 'cta_call', buttonParamsJson: JSON.stringify({ display_text: buttonText || '📞 Call', phone_number: button.call, icon: buttonIcon }) };
544
+ }
545
+ else if (hasOptionalProperty(button, 'sections') && !!button.sections) {
546
+ return { name: 'single_select', buttonParamsJson: JSON.stringify({ title: buttonText || '📋 Select', sections: button.sections, icon: buttonIcon }) };
547
+ }
548
+ return button;
549
+ }),
550
+ messageParamsJson: JSON.stringify(messageParamsJson),
551
+ messageVersion: 3
552
+ };
269
553
  };
270
- function hasOptionalProperty(obj, key) {
271
- return typeof obj === 'object' && obj !== null && key in obj && obj[key] !== null;
272
- }
273
554
  export const generateWAMessageContent = async (message, options) => {
274
555
  var _a, _b;
275
556
  let m = {};
276
- if (hasNonNullishProperty(message, 'text')) {
557
+ if (hasNonNullishProperty(message, 'raw')) {
558
+ delete message.raw;
559
+ return message;
560
+ }
561
+ else if (hasNonNullishProperty(message, 'code') ||
562
+ hasNonNullishProperty(message, 'links') ||
563
+ hasNonNullishProperty(message, 'table') ||
564
+ hasNonNullishProperty(message, 'richResponse') ||
565
+ hasNonNullishProperty(message, 'latex') ||
566
+ hasNonNullishProperty(message, 'gridImage') ||
567
+ hasNonNullishProperty(message, 'inlineImage') ||
568
+ hasNonNullishProperty(message, 'dynamic') ||
569
+ hasNonNullishProperty(message, 'map') ||
570
+ hasNonNullishProperty(message, 'contentItems') ||
571
+ // tipe baru (nama prefixed 'rich' agar tidak konflik dengan media biasa)
572
+ hasNonNullishProperty(message, 'richImage') ||
573
+ hasNonNullishProperty(message, 'richVideo') ||
574
+ hasNonNullishProperty(message, 'reels') ||
575
+ hasNonNullishProperty(message, 'source') ||
576
+ hasNonNullishProperty(message, 'richProduct') ||
577
+ hasNonNullishProperty(message, 'richPost') ||
578
+ hasNonNullishProperty(message, 'tip') ||
579
+ hasNonNullishProperty(message, 'suggest')) {
580
+ m = prepareRichResponseMessage(message);
581
+ }
582
+ else if (hasNonNullishProperty(message, 'text')) {
277
583
  const extContent = { text: message.text };
278
584
  let urlInfo = message.linkPreview;
279
585
  if (typeof urlInfo === 'undefined') {
@@ -362,6 +668,9 @@ export const generateWAMessageContent = async (message, options) => {
362
668
  }
363
669
  }
364
670
  }
671
+ else if (hasNonNullishProperty(message, 'stickers')) {
672
+ m.stickerPackMessage = await prepareStickerPackMessage(message, options);
673
+ }
365
674
  else if (hasNonNullishProperty(message, 'pin')) {
366
675
  m.pinInChatMessage = {};
367
676
  m.messageContextInfo = {};
@@ -370,6 +679,25 @@ export const generateWAMessageContent = async (message, options) => {
370
679
  m.pinInChatMessage.senderTimestampMs = Date.now();
371
680
  m.messageContextInfo.messageAddOnDurationInSecs = message.type === 1 ? message.time || 86400 : 0;
372
681
  }
682
+ else if (hasNonNullishProperty(message, 'keep')) {
683
+ m.keepInChatMessage = {};
684
+ m.keepInChatMessage.key = message.keep;
685
+ m.keepInChatMessage.keepType = message.type;
686
+ m.keepInChatMessage.timestampMs = Date.now();
687
+ }
688
+ else if (hasNonNullishProperty(message, 'flowReply')) {
689
+ m.interactiveResponseMessage = {
690
+ body: {
691
+ format: message.flowReply.format || proto.Message.InteractiveResponseMessage.Body.Format.DEFAULT,
692
+ text: message.flowReply.text
693
+ },
694
+ nativeFlowResponseMessage: {
695
+ name: message.flowReply.name,
696
+ paramsJson: message.flowReply.paramsJson || '{}',
697
+ version: message.flowReply.version || 1
698
+ }
699
+ };
700
+ }
373
701
  else if (hasNonNullishProperty(message, 'buttonReply')) {
374
702
  switch (message.type) {
375
703
  case 'template':
@@ -393,17 +721,32 @@ export const generateWAMessageContent = async (message, options) => {
393
721
  m.ptvMessage = videoMessage;
394
722
  }
395
723
  else if (hasNonNullishProperty(message, 'product')) {
396
- const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options);
397
- m.productMessage = WAProto.Message.ProductMessage.create({
724
+ if (!message.businessOwnerJid) {
725
+ throw new Boom('"businessOwnerJid" is missing from the content', { statusCode: 400 });
726
+ }
727
+ const { imageMessage } = await prepareWAMessageMedia({ image: message.image || message.product.productImage }, options);
728
+ const content = {
398
729
  ...message,
399
730
  product: {
731
+ currencyCode: 'IDR',
732
+ priceAmount1000: 1000,
733
+ title: LIBRARY_NAME,
400
734
  ...message.product,
401
735
  productImage: imageMessage
402
736
  }
403
- });
737
+ };
738
+ delete content.image;
739
+ m.productMessage = WAProto.Message.ProductMessage.create(content);
404
740
  }
405
741
  else if (hasNonNullishProperty(message, 'listReply')) {
406
- m.listResponseMessage = { ...message.listReply };
742
+ m.listResponseMessage = {
743
+ description: message.listReply.description,
744
+ listType: proto.Message.ListResponseMessage.ListType.SINGLE_SELECT,
745
+ singleSelectReply: {
746
+ selectedRowId: message.listReply.id
747
+ },
748
+ title: message.listReply.title
749
+ };
407
750
  }
408
751
  else if (hasNonNullishProperty(message, 'event')) {
409
752
  m.eventMessage = {};
@@ -436,21 +779,35 @@ export const generateWAMessageContent = async (message, options) => {
436
779
  statusCode: 400
437
780
  });
438
781
  }
439
- m.messageContextInfo = {
440
- // encKey
441
- messageSecret: message.poll.messageSecret || randomBytes(32)
442
- };
443
782
  const pollCreationMessage = {
444
783
  name: message.poll.name,
445
784
  selectableOptionsCount: message.poll.selectableCount,
446
- options: message.poll.values.map(optionName => ({ optionName }))
785
+ options: message.poll.values.map(optionName => ({ optionName })),
786
+ endTime: message.poll.endDate ? message.poll.endDate.getTime() : undefined,
787
+ hideParticipantName: message.poll.hideVoter ?? false,
788
+ allowAddOption: message.poll.canAddOption ?? false
447
789
  };
448
790
  if (message.poll.toAnnouncementGroup) {
449
791
  // poll v2 is for community announcement groups (single select and multiple)
450
792
  m.pollCreationMessageV2 = pollCreationMessage;
451
793
  }
452
794
  else {
453
- if (message.poll.selectableCount === 1) {
795
+ // Add quiz message support
796
+ if (message.poll.pollType === 1) {
797
+ if (!message.poll.correctAnswer) {
798
+ throw new Boom('No "correctAnswer" provided for quiz', { statusCode: 400 });
799
+ }
800
+ m.pollCreationMessageV5 = {
801
+ // quiz for newsletter only
802
+ ...pollCreationMessage,
803
+ correctAnswer: {
804
+ optionName: message.poll.correctAnswer.toString()
805
+ },
806
+ pollType: 1,
807
+ selectableOptionsCount: 1
808
+ };
809
+ }
810
+ else if (message.poll.selectableCount === 1) {
454
811
  //poll v3 is for single select polls
455
812
  m.pollCreationMessageV3 = pollCreationMessage;
456
813
  }
@@ -459,11 +816,88 @@ export const generateWAMessageContent = async (message, options) => {
459
816
  m.pollCreationMessage = pollCreationMessage;
460
817
  }
461
818
  }
819
+ m.messageContextInfo = {
820
+ // encKey
821
+ messageSecret: message.poll.messageSecret || randomBytes(32)
822
+ };
823
+ }
824
+ else if (hasNonNullishProperty(message, 'pollResult')) {
825
+ const pollResultSnapshotMessage = {
826
+ name: message.pollResult.name,
827
+ pollVotes: message.pollResult.votes.map(vote => ({
828
+ optionName: vote.name,
829
+ optionVoteCount: parseInt(vote.voteCount)
830
+ }))
831
+ };
832
+ if (message.pollResult.pollType === 1) {
833
+ pollResultSnapshotMessage.pollType = proto.Message.PollType.QUIZ;
834
+ m.pollResultSnapshotMessageV3 = pollResultSnapshotMessage;
835
+ }
836
+ else {
837
+ pollResultSnapshotMessage.pollType = proto.Message.PollType.POLL;
838
+ m.pollResultSnapshotMessage = pollResultSnapshotMessage;
839
+ }
840
+ }
841
+ else if (hasNonNullishProperty(message, 'pollUpdate')) {
842
+ if (!message.pollUpdate.key) {
843
+ throw new Boom('Message key is required', { statusCode: 400 });
844
+ }
845
+ if (!message.pollUpdate.vote) {
846
+ throw new Boom('Encrypted vote payload is required', { statusCode: 400 });
847
+ }
848
+ m.pollUpdateMessage = {
849
+ metadata: message.pollUpdate.metadata,
850
+ pollCreationMessageKey: message.pollUpdate.key,
851
+ senderTimestampMs: Date.now(),
852
+ vote: message.pollUpdate.vote
853
+ };
854
+ }
855
+ else if (hasNonNullishProperty(message, 'paymentInviteServiceType')) {
856
+ m.paymentInviteMessage = {
857
+ expiryTimestamp: Date.now(),
858
+ serviceType: message.paymentInviteServiceType
859
+ };
860
+ }
861
+ else if (hasNonNullishProperty(message, 'orderText')) {
862
+ if (!Buffer.isBuffer(message.thumbnail)) {
863
+ throw new Boom('Must provide thumbnail buffer in order message', { statusCode: 400 });
864
+ }
865
+ m.orderMessage = {
866
+ itemCount: 1,
867
+ messageVersion: 1,
868
+ orderTitle: LIBRARY_NAME,
869
+ status: proto.Message.OrderMessage.OrderStatus.INQUIRY,
870
+ surface: proto.Message.OrderMessage.OrderSurface.CATALOG,
871
+ token: generateMessageIDV2(),
872
+ totalAmount1000: 1000,
873
+ totalCurrencyCode: 'IDR',
874
+ ...message,
875
+ message: message.orderText
876
+ };
877
+ delete m.orderMessage.orderText;
462
878
  }
463
879
  else if (hasNonNullishProperty(message, 'album')) {
880
+ if (!Array.isArray(message.album)) {
881
+ throw new Boom('Invalid album type. Expected an array.', { statusCode: 400 });
882
+ }
883
+ let videoCount = 0;
884
+ for (let i = 0; i < message.album.length; i++) {
885
+ if (message.album[i].video)
886
+ videoCount++;
887
+ }
888
+ ;
889
+ let imageCount = 0;
890
+ for (let i = 0; i < message.album.length; i++) {
891
+ if (message.album[i].image)
892
+ imageCount++;
893
+ }
894
+ ;
895
+ if ((videoCount + imageCount) < 2) {
896
+ throw new Boom('Minimum provide 2 media to upload album message', { statusCode: 400 });
897
+ }
464
898
  m.albumMessage = {
465
- expectedImageCount: message.album.expectedImageCount,
466
- expectedVideoCount: message.album.expectedVideoCount
899
+ expectedImageCount: imageCount,
900
+ expectedVideoCount: videoCount
467
901
  };
468
902
  }
469
903
  else if (hasNonNullishProperty(message, 'sharePhoneNumber')) {
@@ -485,287 +919,336 @@ export const generateWAMessageContent = async (message, options) => {
485
919
  }
486
920
  };
487
921
  }
488
- else if ('keep' in message) {
489
- m.keepInChatMessage = {};
490
- m.keepInChatMessage.key = message.keep;
491
- m.keepInChatMessage.keepType = message.type;
492
- m.keepInChatMessage.timestampMs = Date.now();
493
- }
494
- else if ('call' in message) {
495
- m = {
496
- scheduledCallCreationMessage: {
497
- scheduledTimestampMs: message.call?.time ?? Date.now(),
498
- callType: message.call?.type ?? 1,
499
- title: message.call?.title
922
+ else if (hasNonNullishProperty(message, 'buttonsMessage')) {
923
+ // Direct buttonsMessage passthrough — supports headerType 6 (locationMessage), etc.
924
+ const btnMsg = { ...message.buttonsMessage };
925
+ if (btnMsg.locationMessage?.jpegThumbnail && !Buffer.isBuffer(btnMsg.locationMessage.jpegThumbnail)) {
926
+ const lib = await getImageProcessingLibrary();
927
+ const hasSharp = 'sharp' in lib && !!lib.sharp?.default;
928
+ if (hasSharp) {
929
+ const rawBuf = typeof btnMsg.locationMessage.jpegThumbnail === 'string'
930
+ ? (await fs.readFile(btnMsg.locationMessage.jpegThumbnail))
931
+ : btnMsg.locationMessage.jpegThumbnail;
932
+ btnMsg.locationMessage.jpegThumbnail = await lib.sharp.default(rawBuf)
933
+ .resize(300, 300, { fit: 'inside', withoutEnlargement: true })
934
+ .jpeg({ quality: 80 })
935
+ .toBuffer();
500
936
  }
501
- };
502
- }
503
- else if ('paymentInvite' in message) {
504
- m.paymentInviteMessage = {
505
- serviceType: message.paymentInvite?.type,
506
- expiryTimestamp: message.paymentInvite?.expiry
507
- };
508
- }
509
- else if ('order' in message) {
510
- m.orderMessage = WAProto.Message.OrderMessage.fromObject({
511
- orderId: message.order.id,
512
- thumbnail: message.order.thumbnail,
513
- itemCount: message.order.itemCount,
514
- status: message.order.status,
515
- surface: message.order.surface,
516
- orderTitle: message.order.title,
517
- message: message.order.text,
518
- sellerJid: message.order.seller,
519
- token: message.order.token,
520
- totalAmount1000: message.order.amount,
521
- totalCurrencyCode: message.order.currency
522
- });
523
- }
524
- else if ('inviteAdmin' in message) {
525
- m.newsletterAdminInviteMessage = {};
526
- m.newsletterAdminInviteMessage.inviteExpiration = message.inviteAdmin?.inviteExpiration;
527
- m.newsletterAdminInviteMessage.caption = message.inviteAdmin?.text;
528
- m.newsletterAdminInviteMessage.newsletterJid = message.inviteAdmin?.jid;
529
- m.newsletterAdminInviteMessage.newsletterName = message.inviteAdmin?.subject;
530
- m.newsletterAdminInviteMessage.jpegThumbnail = message.inviteAdmin?.thumbnail;
531
- }
532
- else if ('requestPayment' in message) {
533
- const _rp = message.requestPayment;
534
- const sticker = _rp?.sticker
535
- ? await prepareWAMessageMedia({ sticker: _rp.sticker }, options)
536
- : null;
537
- let notes = {};
538
- if (_rp?.sticker) {
539
- notes = {
540
- stickerMessage: {
541
- ...sticker?.stickerMessage,
542
- contextInfo: _rp?.contextInfo
543
- }
544
- };
545
- }
546
- else if (_rp?.note) {
547
- notes = {
548
- extendedTextMessage: {
549
- text: _rp.note,
550
- contextInfo: _rp?.contextInfo
551
- }
552
- };
553
937
  }
554
- else {
555
- throw new Boom('Invalid media type', { statusCode: 400 });
556
- }
557
- m.requestPaymentMessage = WAProto.Message.RequestPaymentMessage.fromObject({
558
- expiryTimestamp: _rp?.expiry,
559
- amount1000: _rp?.amount,
560
- currencyCodeIso4217: _rp?.currency,
561
- requestFrom: _rp?.from,
562
- noteMessage: { ...notes },
563
- background: _rp?.background ?? null
564
- });
938
+ m = { buttonsMessage: btnMsg };
565
939
  }
566
940
  else {
567
941
  m = await prepareWAMessageMedia(message, options);
568
942
  }
569
- if ('buttons' in message && !!message.buttons) {
570
- const nativeButtons = message.buttons.map(b => ({
571
- name: 'quick_reply',
572
- buttonParamsJson: JSON.stringify({
573
- display_text: b.buttonText?.displayText || b.displayText || '',
574
- id: b.buttonId || b.id || ''
575
- })
576
- }));
577
- const interactiveMessage = {
578
- nativeFlowMessage: WAProto.Message.InteractiveMessage.NativeFlowMessage.fromObject({
579
- buttons: nativeButtons
943
+ if (hasNonNullishProperty(message, 'buttons')) {
944
+ const buttonsMessage = {
945
+ buttons: message.buttons.map(button => {
946
+ const buttonText = button.text || button.buttonText;
947
+ if (hasOptionalProperty(button, 'sections')) {
948
+ return {
949
+ nativeFlowInfo: {
950
+ name: 'single_select',
951
+ paramsJson: JSON.stringify({
952
+ title: buttonText,
953
+ sections: button.sections
954
+ })
955
+ },
956
+ type: ButtonType.NATIVE_FLOW
957
+ };
958
+ }
959
+ else if (hasOptionalProperty(button, 'name')) {
960
+ return {
961
+ nativeFlowInfo: {
962
+ name: button.name,
963
+ paramsJson: button.paramsJson
964
+ },
965
+ type: ButtonType.NATIVE_FLOW
966
+ };
967
+ }
968
+ return {
969
+ buttonId: button.id || button.buttonId,
970
+ buttonText: typeof buttonText === 'string' ? { displayText: buttonText } : buttonText,
971
+ type: button.type || ButtonType.RESPONSE
972
+ };
580
973
  })
581
974
  };
582
- if ('text' in message) {
583
- interactiveMessage.body = { text: message.text };
584
- }
585
- else if ('caption' in message) {
586
- interactiveMessage.body = { text: message.caption };
587
- interactiveMessage.header = { hasMediaAttachment: true };
588
- Object.assign(interactiveMessage.header, m);
589
- }
590
- if ('title' in message && !!message.title) {
591
- interactiveMessage.header = interactiveMessage.header || {};
592
- interactiveMessage.header.title = message.title;
593
- interactiveMessage.header.hasMediaAttachment = false;
594
- }
595
- if ('footer' in message && !!message.footer) {
596
- interactiveMessage.footer = { text: message.footer };
975
+ if (hasOptionalProperty(message, 'text')) {
976
+ buttonsMessage.contentText = message.text;
977
+ buttonsMessage.headerType = ButtonHeaderType.EMPTY;
597
978
  }
598
- if ('contextInfo' in message && !!message.contextInfo) {
599
- interactiveMessage.contextInfo = message.contextInfo;
600
- }
601
- if ('mentions' in message && !!message.mentions) {
602
- interactiveMessage.contextInfo = { mentionedJid: message.mentions };
603
- }
604
- m = { interactiveMessage };
605
- }
606
- else if ('templateButtons' in message && !!message.templateButtons) {
607
- const nativeButtons = message.templateButtons.map(b => {
608
- if (b.quickReplyButton) {
609
- return {
610
- name: 'quick_reply',
611
- buttonParamsJson: JSON.stringify({
612
- display_text: b.quickReplyButton.displayText || '',
613
- id: b.quickReplyButton.id || ''
614
- })
615
- };
616
- }
617
- if (b.urlButton) {
618
- return {
619
- name: 'cta_url',
620
- buttonParamsJson: JSON.stringify({
621
- display_text: b.urlButton.displayText || '',
622
- url: b.urlButton.url || '',
623
- merchant_url: b.urlButton.url || ''
624
- })
625
- };
626
- }
627
- if (b.callButton) {
628
- return {
629
- name: 'cta_call',
630
- buttonParamsJson: JSON.stringify({
631
- display_text: b.callButton.displayText || '',
632
- phone_number: b.callButton.phoneNumber || ''
633
- })
634
- };
979
+ else {
980
+ if (hasOptionalProperty(message, 'caption')) {
981
+ buttonsMessage.contentText = message.caption;
635
982
  }
636
- return {
637
- name: 'quick_reply',
638
- buttonParamsJson: JSON.stringify({ display_text: '', id: '' })
639
- };
640
- });
641
- const interactiveMessage = {
642
- nativeFlowMessage: WAProto.Message.InteractiveMessage.NativeFlowMessage.fromObject({
643
- buttons: nativeButtons
983
+ const type = Object.keys(m)[0].replace('Message', '').toUpperCase();
984
+ buttonsMessage.headerType = ButtonHeaderType[type];
985
+ Object.assign(buttonsMessage, m);
986
+ }
987
+ if (hasOptionalProperty(message, 'footer')) {
988
+ buttonsMessage.footerText = message.footer;
989
+ }
990
+ m = { buttonsMessage };
991
+ }
992
+ else if (hasNonNullishProperty(message, 'sections')) {
993
+ const listMessage = {
994
+ sections: message.sections,
995
+ buttonText: message.buttonText,
996
+ title: message.title,
997
+ footerText: message.footer,
998
+ description: message.text,
999
+ listType: ListType.SINGLE_SELECT
1000
+ };
1001
+ m = { listMessage };
1002
+ }
1003
+ else if (hasNonNullishProperty(message, 'templateButtons')) {
1004
+ const hydratedTemplate = {
1005
+ hydratedButtons: message.templateButtons.map((button, i) => {
1006
+ const buttonText = button.text || button.buttonText;
1007
+ if (hasOptionalProperty(button, 'id')) {
1008
+ return {
1009
+ index: i,
1010
+ quickReplyButton: {
1011
+ displayText: buttonText || '👉🏻 Click',
1012
+ id: button.id
1013
+ }
1014
+ };
1015
+ }
1016
+ else if (hasOptionalProperty(button, 'url')) {
1017
+ return {
1018
+ index: i,
1019
+ urlButton: {
1020
+ displayText: buttonText || '🌐 Visit',
1021
+ url: button.url
1022
+ }
1023
+ };
1024
+ }
1025
+ else if (hasOptionalProperty(button, 'call')) {
1026
+ return {
1027
+ index: i,
1028
+ callButton: {
1029
+ displayText: buttonText || '📞 Call',
1030
+ phoneNumber: button.call
1031
+ }
1032
+ };
1033
+ }
1034
+ button.index = button.index || i;
1035
+ return button;
644
1036
  })
645
1037
  };
646
- if ('text' in message) {
647
- interactiveMessage.body = { text: message.text };
648
- }
649
- else if ('caption' in message) {
650
- interactiveMessage.body = { text: message.caption };
651
- interactiveMessage.header = { hasMediaAttachment: true };
652
- Object.assign(interactiveMessage.header, m);
1038
+ if (hasOptionalProperty(message, 'text')) {
1039
+ hydratedTemplate.hydratedContentText = message.text;
653
1040
  }
654
- if ('footer' in message && !!message.footer) {
655
- interactiveMessage.footer = { text: message.footer };
656
- }
657
- if ('contextInfo' in message && !!message.contextInfo) {
658
- interactiveMessage.contextInfo = message.contextInfo;
1041
+ else {
1042
+ if (hasOptionalProperty(message, 'caption')) {
1043
+ hydratedTemplate.hydratedTitleText = message.title;
1044
+ hydratedTemplate.hydratedContentText = message.caption;
1045
+ }
1046
+ ;
1047
+ Object.assign(hydratedTemplate, m);
659
1048
  }
660
- if ('mentions' in message && !!message.mentions) {
661
- interactiveMessage.contextInfo = { mentionedJid: message.mentions };
1049
+ if (hasOptionalProperty(message, 'footer')) {
1050
+ hydratedTemplate.hydratedFooterText = message.footer;
662
1051
  }
663
- m = { interactiveMessage };
664
- }
665
- if ('sections' in message && !!message.sections) {
666
- const interactiveMessage = {
667
- nativeFlowMessage: WAProto.Message.InteractiveMessage.NativeFlowMessage.fromObject({
668
- buttons: [{
669
- name: 'single_select',
670
- buttonParamsJson: JSON.stringify({
671
- title: message.buttonText || 'Lihat Pilihan',
672
- sections: message.sections.map(s => ({
673
- title: s.title || '',
674
- highlight_label: s.highlight_label || '',
675
- rows: (s.rows || []).map(r => ({
676
- header: r.title || '',
677
- title: r.description || '',
678
- id: r.rowId || r.id || ''
679
- }))
680
- }))
681
- })
682
- }]
683
- })
1052
+ hydratedTemplate.templateId = message.id || 'template-' + Date.now();
1053
+ m = {
1054
+ templateMessage: {
1055
+ hydratedFourRowTemplate: hydratedTemplate,
1056
+ hydratedTemplate: hydratedTemplate
1057
+ }
684
1058
  };
685
- if (message.text) interactiveMessage.body = { text: message.text };
686
- if (message.footer) interactiveMessage.footer = { text: message.footer };
687
- if (message.title) interactiveMessage.header = { title: message.title, hasMediaAttachment: false };
688
- if (message.contextInfo) interactiveMessage.contextInfo = message.contextInfo;
689
- if (message.mentions) interactiveMessage.contextInfo = { mentionedJid: message.mentions };
690
- m = { interactiveMessage };
691
1059
  }
692
- if ('interactiveButtons' in message && !!message.interactiveButtons) {
1060
+ else if (hasNonNullishProperty(message, 'nativeFlow')) {
693
1061
  const interactiveMessage = {
694
- nativeFlowMessage: WAProto.Message.InteractiveMessage.NativeFlowMessage.fromObject({
695
- buttons: message.interactiveButtons
696
- })
1062
+ nativeFlowMessage: prepareNativeFlowButtons(message)
697
1063
  };
698
- if ('text' in message) {
699
- interactiveMessage.body = { text: message.text };
1064
+ if (hasOptionalProperty(message, 'bizJid')) {
1065
+ interactiveMessage.collectionMessage = {
1066
+ bizJid: message.bizJid,
1067
+ id: message.id,
1068
+ messageVersion: 1
1069
+ };
700
1070
  }
701
- else if ('caption' in message) {
702
- interactiveMessage.body = { text: message.caption };
703
- interactiveMessage.header = {
704
- title: message.title,
705
- subtitle: message.subtitle,
706
- hasMediaAttachment: message?.media ?? false
1071
+ else if (hasOptionalProperty(message, 'shopSurface')) {
1072
+ interactiveMessage.shopStorefrontMessage = {
1073
+ surface: message.shopSurface,
1074
+ id: message.id,
1075
+ messageVersion: 1
707
1076
  };
708
- Object.assign(interactiveMessage.header, m);
709
1077
  }
710
- if ('footer' in message && !!message.footer) {
711
- interactiveMessage.footer = { text: message.footer };
1078
+ if (hasOptionalProperty(message, 'text')) {
1079
+ interactiveMessage.body = { text: message.text };
712
1080
  }
713
- if ('title' in message && !!message.title) {
714
- interactiveMessage.header = {
715
- title: message.title,
716
- subtitle: message.subtitle,
717
- hasMediaAttachment: message?.media ?? false
718
- };
1081
+ else {
1082
+ if (hasOptionalProperty(message, 'caption')) {
1083
+ const isValidHeader = hasValidInteractiveHeader(m);
1084
+ if (!isValidHeader) {
1085
+ throw new Boom('Invalid media type for interactive message header', { statusCode: 400 });
1086
+ }
1087
+ interactiveMessage.header = {
1088
+ title: message.title || '',
1089
+ subtitle: message.subtitle || '',
1090
+ hasMediaAttachment: isValidHeader
1091
+ };
1092
+ interactiveMessage.body = { text: message.caption };
1093
+ }
1094
+ if (hasOptionalProperty(message, 'thumbnail') && !!message.thumbnail) {
1095
+ interactiveMessage.jpegThumbnail = message.thumbnail;
1096
+ }
719
1097
  Object.assign(interactiveMessage.header, m);
720
1098
  }
721
- if ('contextInfo' in message && !!message.contextInfo) {
722
- interactiveMessage.contextInfo = message.contextInfo;
1099
+ if (hasOptionalProperty(message, 'audioFooter')) {
1100
+ const { audioMessage } = await prepareWAMessageMedia({
1101
+ audio: message.audioFooter
1102
+ }, options);
1103
+ interactiveMessage.footer = {
1104
+ audioMessage,
1105
+ hasMediaAttachment: true
1106
+ };
723
1107
  }
724
- if ('mentions' in message && !!message.mentions) {
725
- interactiveMessage.contextInfo = { mentionedJid: message.mentions };
1108
+ else if (hasOptionalProperty(message, 'footer')) {
1109
+ interactiveMessage.footer = { text: message.footer };
726
1110
  }
727
1111
  m = { interactiveMessage };
728
1112
  }
729
- if ('shop' in message && !!message.shop) {
1113
+ else if (hasNonNullishProperty(message, 'cards')) {
730
1114
  const interactiveMessage = {
731
- shopStorefrontMessage: WAProto.Message.InteractiveMessage.ShopMessage.fromObject({
732
- surface: message.shop,
733
- id: message.id
734
- })
1115
+ carouselMessage: {
1116
+ cards: await Promise.all(message.cards.map(async (card) => {
1117
+ let carouselHeader = {};
1118
+ if (hasNonNullishProperty(card, 'product')) {
1119
+ carouselHeader.productMessage = await prepareProductMessage(card, options);
1120
+ }
1121
+ else {
1122
+ carouselHeader = await prepareWAMessageMedia(card, options).catch(() => ({}));
1123
+ }
1124
+ const isValidHeader = hasValidCarouselHeader(carouselHeader);
1125
+ if (!isValidHeader) {
1126
+ throw new Boom('Invalid media type for carousel card', { statusCode: 400 });
1127
+ }
1128
+ const carouselCard = {
1129
+ nativeFlowMessage: prepareNativeFlowButtons(card.nativeFlow ? card : [])
1130
+ };
1131
+ if (hasOptionalProperty(card, 'text')) {
1132
+ carouselCard.body = { text: card.text };
1133
+ }
1134
+ else {
1135
+ if (hasOptionalProperty(card, 'caption')) {
1136
+ carouselCard.header = {
1137
+ title: card.title || '',
1138
+ subtitle: card.subtitle || '',
1139
+ hasMediaAttachment: isValidHeader
1140
+ };
1141
+ carouselCard.body = { text: card.caption };
1142
+ }
1143
+ if (hasOptionalProperty(card, 'thumbnail') && !!card.thumbnail) {
1144
+ carouselCard.jpegThumbnail = card.thumbnail;
1145
+ }
1146
+ Object.assign(carouselCard.header, carouselHeader);
1147
+ }
1148
+ if (hasOptionalProperty(card, 'audioFooter')) {
1149
+ const { audioMessage } = await prepareWAMessageMedia({
1150
+ audio: card.audioFooter
1151
+ }, options);
1152
+ carouselCard.footer = {
1153
+ audioMessage,
1154
+ hasMediaAttachment: true
1155
+ };
1156
+ }
1157
+ else if (hasOptionalProperty(card, 'footer')) {
1158
+ carouselCard.footer = { text: card.footer };
1159
+ }
1160
+ return carouselCard;
1161
+ })),
1162
+ carouselCardType: CarouselCardType.UNKNOWN,
1163
+ messageVersion: 1
1164
+ }
735
1165
  };
736
- if ('text' in message) {
1166
+ if (hasOptionalProperty(message, 'text')) {
737
1167
  interactiveMessage.body = { text: message.text };
738
1168
  }
739
- else if ('caption' in message) {
740
- interactiveMessage.body = { text: message.caption };
741
- interactiveMessage.header = {
742
- title: message.title,
743
- subtitle: message.subtitle,
744
- hasMediaAttachment: message?.media ?? false
745
- };
746
- Object.assign(interactiveMessage.header, m);
747
- }
748
- if ('footer' in message && !!message.footer) {
1169
+ if (hasOptionalProperty(message, 'footer')) {
749
1170
  interactiveMessage.footer = { text: message.footer };
750
1171
  }
751
- if ('title' in message && !!message.title) {
752
- interactiveMessage.header = {
753
- title: message.title,
754
- subtitle: message.subtitle,
755
- hasMediaAttachment: message?.media ?? false
756
- };
757
- Object.assign(interactiveMessage.header, m);
1172
+ m = { interactiveMessage };
1173
+ }
1174
+ else if (hasNonNullishProperty(message, 'requestPaymentFrom')) {
1175
+ const requestPaymentMessage = {
1176
+ amount: {
1177
+ currencyCode: 'IDR',
1178
+ offset: 1000,
1179
+ value: 1000
1180
+ },
1181
+ amount1000: 1000,
1182
+ currencyCodeIso4217: 'IDR',
1183
+ expiryTimestamp: Date.now(),
1184
+ noteMessage: m,
1185
+ requestFrom: message.requestPaymentFrom,
1186
+ ...message
1187
+ };
1188
+ delete requestPaymentMessage.requestPaymentFrom;
1189
+ if (hasNonNullishProperty(m, 'extendedTextMessage') || hasNonNullishProperty(m, 'stickerMessage')) {
1190
+ Object.assign(requestPaymentMessage.noteMessage, m);
758
1191
  }
759
- if ('contextInfo' in message && !!message.contextInfo) {
760
- interactiveMessage.contextInfo = message.contextInfo;
1192
+ else {
1193
+ throw new Boom('Invalid message type for request payment note message', { statusCode: 400 });
761
1194
  }
762
- if ('mentions' in message && !!message.mentions) {
763
- interactiveMessage.contextInfo = { mentionedJid: message.mentions };
1195
+ m = { requestPaymentMessage };
1196
+ }
1197
+ else if (hasNonNullishProperty(message, 'invoiceNote')) {
1198
+ const attachment = m.imageMessage || m.documentMessage;
1199
+ const type = Object.keys(m)[0].replace('Message', '').toUpperCase();
1200
+ const invoiceMessage = {
1201
+ attachmentType: proto.Message.InvoiceMessage.AttachmentType[type === 'DOCUMENT' ? 'PDF' : 'IMAGE'],
1202
+ note: message.invoiceNote
1203
+ };
1204
+ if (attachment) {
1205
+ const { directPath, fileEncSha256, fileSha256, jpegThumbnail = undefined, mediaKey, mediaKeyTimestamp, mimetype } = attachment;
1206
+ Object.assign(invoiceMessage, {
1207
+ attachmentDirectPath: directPath,
1208
+ attachmentFileEncSha256: fileEncSha256,
1209
+ attachmentFileSha256: fileSha256,
1210
+ attachmentJpegThumbnail: jpegThumbnail,
1211
+ attachmentMediaKey: mediaKey,
1212
+ attachmentMediaKeyTimestamp: mediaKeyTimestamp,
1213
+ attachmentMimetype: mimetype,
1214
+ token: generateMessageIDV2()
1215
+ });
764
1216
  }
765
- m = { interactiveMessage };
1217
+ else {
1218
+ throw new Boom('Invalid media type for invoice message', { statusCode: 400 });
1219
+ }
1220
+ m = { invoiceMessage };
766
1221
  }
767
- if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
768
- m = { viewOnceMessage: { message: m } };
1222
+ if (hasOptionalProperty(message, 'externalAdReply') && !!message.externalAdReply) {
1223
+ const messageType = Object.keys(m)[0];
1224
+ const key = m[messageType];
1225
+ const content = message.externalAdReply;
1226
+ if ('thumbnail' in content && !Buffer.isBuffer(content.thumbnail)) {
1227
+ throw new Boom('Thumbnail must in buffer type', { statusCode: 400 });
1228
+ }
1229
+ if (!content.url || typeof content.url !== 'string') {
1230
+ content.url = DONATE_URL;
1231
+ }
1232
+ const externalAdReply = {
1233
+ ...content,
1234
+ body: content.body,
1235
+ mediaType: content.mediaType || 1,
1236
+ mediaUrl: content.url,
1237
+ renderLargerThumbnail: content.largeThumbnail,
1238
+ sourceUrl: content.url,
1239
+ thumbnail: content.thumbnail,
1240
+ thumbnailUrl: content.url + '?update=' + Date.now(),
1241
+ title: content.title || LIBRARY_NAME
1242
+ };
1243
+ delete externalAdReply.subTitle;
1244
+ delete externalAdReply.largeThumbnail;
1245
+ delete externalAdReply.url;
1246
+ if ('contextInfo' in key && !!key.contextInfo) {
1247
+ key.contextInfo.externalAdReply = { ...key.contextInfo.externalAdReply, ...externalAdReply };
1248
+ }
1249
+ else if (key) {
1250
+ key.contextInfo = { externalAdReply };
1251
+ }
769
1252
  }
770
1253
  if ((hasOptionalProperty(message, 'mentions') && message.mentions?.length) ||
771
1254
  (hasOptionalProperty(message, 'mentionAll') && message.mentionAll)) {
@@ -787,16 +1270,6 @@ export const generateWAMessageContent = async (message, options) => {
787
1270
  };
788
1271
  }
789
1272
  }
790
- if (hasOptionalProperty(message, 'edit')) {
791
- m = {
792
- protocolMessage: {
793
- key: message.edit,
794
- editedMessage: m,
795
- timestampMs: Date.now(),
796
- type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT
797
- }
798
- };
799
- }
800
1273
  if (hasOptionalProperty(message, 'contextInfo') && !!message.contextInfo) {
801
1274
  const messageType = Object.keys(m)[0];
802
1275
  const key = m[messageType];
@@ -807,12 +1280,71 @@ export const generateWAMessageContent = async (message, options) => {
807
1280
  key.contextInfo = message.contextInfo;
808
1281
  }
809
1282
  }
810
- if (hasOptionalProperty(message, 'albumParentKey') && !!message.albumParentKey) {
811
- m.messageContextInfo = {
812
- ...m.messageContextInfo,
813
- messageAssociation: {
814
- associationType: WAProto.MessageAssociation.AssociationType.MEDIA_ALBUM,
815
- parentMessageKey: message.albumParentKey
1283
+ if (hasOptionalProperty(message, 'groupStatus') && !!message.groupStatus) {
1284
+ const messageType = Object.keys(m)[0];
1285
+ const key = m[messageType];
1286
+ if ('contextInfo' in key && !!key.contextInfo) {
1287
+ key.contextInfo.isGroupStatus = message.groupStatus;
1288
+ }
1289
+ else if (key) {
1290
+ key.contextInfo = {
1291
+ isGroupStatus: message.groupStatus
1292
+ };
1293
+ }
1294
+ m = { groupStatusMessageV2: { message: m } };
1295
+ delete message.groupStatus;
1296
+ }
1297
+ if (hasOptionalProperty(message, 'spoiler') && !!message.spoiler) {
1298
+ const messageType = Object.keys(m)[0];
1299
+ const key = m[messageType];
1300
+ if ('contextInfo' in key && !!key.contextInfo) {
1301
+ key.contextInfo.isSpoiler = message.spoiler;
1302
+ }
1303
+ else if (key) {
1304
+ key.contextInfo = {
1305
+ isSpoiler: message.spoiler
1306
+ };
1307
+ }
1308
+ m = { spoilerMessage: { message: m } };
1309
+ delete message.spoiler;
1310
+ }
1311
+ else if (hasOptionalProperty(message, 'interactiveAsTemplate') && !!message.interactiveAsTemplate) {
1312
+ if (!m.interactiveMessage) {
1313
+ throw new Boom('Invalid message type for template', { statusCode: 400 });
1314
+ }
1315
+ m = {
1316
+ templateMessage: {
1317
+ interactiveMessageTemplate: m.interactiveMessage,
1318
+ templateId: message.id || 'template-' + Date.now()
1319
+ }
1320
+ };
1321
+ delete message.interactiveAsTemplate;
1322
+ }
1323
+ if (hasOptionalProperty(message, 'ephemeral') && !!message.ephemeral) {
1324
+ m = { ephemeralMessage: { message: m } };
1325
+ delete message.ephemeral;
1326
+ }
1327
+ if (hasOptionalProperty(message, 'isLottie') && !!message.isLottie) {
1328
+ m = { lottieStickerMessage: { message: m } };
1329
+ }
1330
+ else if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
1331
+ m = { viewOnceMessage: { message: m } };
1332
+ }
1333
+ else if (hasOptionalProperty(message, 'viewOnceV2') && !!message.viewOnceV2) {
1334
+ m = { viewOnceMessageV2: { message: m } };
1335
+ delete message.viewOnceV2;
1336
+ }
1337
+ else if (hasOptionalProperty(message, 'viewOnceV2Extension') && !!message.viewOnceV2Extension) {
1338
+ m = { viewOnceMessageV2Extension: { message: m } };
1339
+ delete message.viewOnceV2Extension;
1340
+ }
1341
+ if (hasOptionalProperty(message, 'edit')) {
1342
+ m = {
1343
+ protocolMessage: {
1344
+ key: message.edit,
1345
+ editedMessage: m,
1346
+ timestampMs: Date.now(),
1347
+ type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT
816
1348
  }
817
1349
  };
818
1350
  }
@@ -831,10 +1363,12 @@ export const generateWAMessageFromContent = (jid, message, options) => {
831
1363
  options.timestamp = new Date();
832
1364
  }
833
1365
  const innerMessage = normalizeMessageContent(message);
1366
+ const messageContextInfo = message.messageContextInfo;
834
1367
  const key = getContentType(innerMessage);
835
1368
  const timestamp = unixTimestampSeconds(options.timestamp);
1369
+ const isNewsletter = isJidNewsletter(jid);
836
1370
  const { quoted, userJid } = options;
837
- if (quoted && !isJidNewsletter(jid)) {
1371
+ if (quoted) {
838
1372
  const participant = quoted.key.fromMe
839
1373
  ? userJid // TODO: Add support for LIDs
840
1374
  : quoted.participant || quoted.key.participant || quoted.key.remoteJid;
@@ -852,7 +1386,7 @@ export const generateWAMessageFromContent = (jid, message, options) => {
852
1386
  contextInfo.quotedMessage = quotedMsg;
853
1387
  // if a participant is quoted, then it must be a group
854
1388
  // hence, remoteJid of group must also be entered
855
- if (jid !== quoted.key.remoteJid) {
1389
+ if (!isNewsletter && jid !== quoted.key.remoteJid) {
856
1390
  contextInfo.remoteJid = quoted.key.remoteJid;
857
1391
  }
858
1392
  if (contextInfo && innerMessage[key]) {
@@ -868,7 +1402,7 @@ export const generateWAMessageFromContent = (jid, message, options) => {
868
1402
  // already not converted to disappearing message
869
1403
  key !== 'ephemeralMessage' &&
870
1404
  // newsletters don't support ephemeral messages
871
- !isJidNewsletter(jid)) {
1405
+ !isNewsletter) {
872
1406
  /* @ts-ignore */
873
1407
  innerMessage[key].contextInfo = {
874
1408
  ...(innerMessage[key].contextInfo || {}),
@@ -876,6 +1410,13 @@ export const generateWAMessageFromContent = (jid, message, options) => {
876
1410
  //ephemeralSettingTimestamp: options.ephemeralOptions.eph_setting_ts?.toString()
877
1411
  };
878
1412
  }
1413
+ if (messageContextInfo?.messageSecret && (isPnUser(jid) || isLidUser(jid))) {
1414
+ messageContextInfo.deviceListMetadata = {
1415
+ recipientKeyHash: randomBytes(10),
1416
+ recipientTimestamp: unixTimestampSeconds()
1417
+ };
1418
+ messageContextInfo.deviceListMetadataVersion = 2;
1419
+ }
879
1420
  message = WAProto.Message.create(message);
880
1421
  const messageJSON = {
881
1422
  key: {
@@ -895,7 +1436,10 @@ export const generateWAMessage = async (jid, content, options) => {
895
1436
  // ensure msg ID is with every log
896
1437
  options.logger = options?.logger?.child({ msgId: options.messageId });
897
1438
  // Pass jid in the options to generateWAMessageContent
898
- return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options);
1439
+ if (jid) {
1440
+ options.jid = jid;
1441
+ }
1442
+ return generateWAMessageFromContent(jid, await generateWAMessageContent(content, options), options);
899
1443
  };
900
1444
  /** Get the key to access the true type of content */
901
1445
  export const getContentType = (content) => {
@@ -905,12 +1449,6 @@ export const getContentType = (content) => {
905
1449
  return key;
906
1450
  }
907
1451
  };
908
- /**
909
- * Normalizes ephemeral, view once messages to regular message content
910
- * Eg. image messages in ephemeral messages, in view once messages etc.
911
- * @param content
912
- * @returns
913
- */
914
1452
  export const normalizeMessageContent = (content) => {
915
1453
  if (!content) {
916
1454
  return undefined;
@@ -925,29 +1463,33 @@ export const normalizeMessageContent = (content) => {
925
1463
  }
926
1464
  return content;
927
1465
  function getFutureProofMessage(message) {
928
- return (message?.ephemeralMessage ||
929
- message?.viewOnceMessage ||
1466
+ return (message?.associatedChildMessage ||
1467
+ message?.botForwardedMessage ||
1468
+ message?.botInvokeMessage ||
1469
+ message?.botTaskMessage ||
930
1470
  message?.documentWithCaptionMessage ||
931
- message?.viewOnceMessageV2 ||
932
- message?.viewOnceMessageV2Extension ||
933
1471
  message?.editedMessage ||
934
- message?.groupMentionedMessage ||
935
- message?.botInvokeMessage ||
936
- message?.lottieStickerMessage ||
1472
+ message?.ephemeralMessage ||
937
1473
  message?.eventCoverImage ||
938
- message?.statusMentionMessage ||
939
- message?.pollCreationOptionImageMessage ||
940
- message?.associatedChildMessage ||
1474
+ message?.groupMentionedMessage ||
941
1475
  message?.groupStatusMentionMessage ||
942
- message?.pollCreationMessageV4 ||
943
- message?.pollCreationMessageV5 ||
944
- message?.statusAddYours ||
945
1476
  message?.groupStatusMessage ||
1477
+ message?.groupStatusMessageV2 ||
946
1478
  message?.limitSharingMessage ||
947
- message?.botTaskMessage ||
1479
+ message?.lottieStickerMessage ||
1480
+ message?.newsletterAdminProfileMessage ||
1481
+ message?.newsletterAdminProfileMessageV2 ||
1482
+ message?.newsletterAdminProfileStatusMessage ||
1483
+ message?.pollCreationMessageV4 ||
1484
+ message?.pollCreationOptionImageMessage ||
948
1485
  message?.questionMessage ||
949
- message?.groupStatusMessageV2 ||
950
- message?.botForwardedMessage);
1486
+ message?.questionReplyMessage ||
1487
+ message?.spoilerMessage ||
1488
+ message?.statusAddYours ||
1489
+ message?.statusMentionMessage ||
1490
+ message?.viewOnceMessage ||
1491
+ message?.viewOnceMessageV2 ||
1492
+ message?.viewOnceMessageV2Extension);
951
1493
  }
952
1494
  };
953
1495
  /**
@@ -1180,4 +1722,74 @@ export const assertMediaContent = (content) => {
1180
1722
  }
1181
1723
  return mediaContent;
1182
1724
  };
1183
- //# sourceMappingURL=messages.js.map
1725
+ /**
1726
+ * Checks if a WebP buffer is animated by looking for VP8X chunk with animation flag
1727
+ * or ANIM/ANMF chunks
1728
+ */
1729
+ const isAnimatedWebP = (buffer) => {
1730
+ // WebP must start with RIFF....WEBP
1731
+ if (buffer.length < 12 ||
1732
+ buffer[0] !== 0x52 ||
1733
+ buffer[1] !== 0x49 ||
1734
+ buffer[2] !== 0x46 ||
1735
+ buffer[3] !== 0x46 ||
1736
+ buffer[8] !== 0x57 ||
1737
+ buffer[9] !== 0x45 ||
1738
+ buffer[10] !== 0x42 ||
1739
+ buffer[11] !== 0x50) {
1740
+ return false;
1741
+ }
1742
+ ;
1743
+ // Parse chunks starting after RIFF header (12 bytes)
1744
+ let offset = 12;
1745
+ while (offset < buffer.length - 8) {
1746
+ const chunkFourCC = buffer.toString('ascii', offset, offset + 4);
1747
+ const chunkSize = buffer.readUInt32LE(offset + 4);
1748
+ if (chunkFourCC === 'VP8X') {
1749
+ // VP8X extended header, check animation flag (bit 1 at offset+8)
1750
+ const flagsOffset = offset + 8;
1751
+ if (flagsOffset < buffer.length) {
1752
+ const flags = buffer[flagsOffset];
1753
+ if (flags & 0x02) {
1754
+ return true;
1755
+ }
1756
+ ;
1757
+ }
1758
+ ;
1759
+ }
1760
+ else if (chunkFourCC === 'ANIM' || chunkFourCC === 'ANMF') {
1761
+ // ANIM or ANMF chunks indicate animation
1762
+ return true;
1763
+ }
1764
+ ;
1765
+ // Move to next chunk (chunk size + 8 bytes header, padded to even)
1766
+ offset += 8 + chunkSize + (chunkSize % 2);
1767
+ }
1768
+ ;
1769
+ return false;
1770
+ };
1771
+ /**
1772
+ * Checks if a buffer is a WebP file
1773
+ */
1774
+ const isWebPBuffer = (buffer) => {
1775
+ return (buffer.length >= 12 &&
1776
+ buffer[0] === 0x52 &&
1777
+ buffer[1] === 0x49 &&
1778
+ buffer[2] === 0x46 &&
1779
+ buffer[3] === 0x46 &&
1780
+ buffer[8] === 0x57 &&
1781
+ buffer[9] === 0x45 &&
1782
+ buffer[10] === 0x42 &&
1783
+ buffer[11] === 0x50);
1784
+ };
1785
+ export const shouldIncludeBizBinaryNode = (message) => {
1786
+ const hasValidInteractive = message.interactiveMessage &&
1787
+ !message.interactiveMessage.carouselMessage &&
1788
+ !message.interactiveMessage.collectionMessage &&
1789
+ !message.interactiveMessage.shopStorefrontMessage;
1790
+ return (message.buttonsMessage ||
1791
+ message.interactiveMessage ||
1792
+ message.listMessage ||
1793
+ hasValidInteractive);
1794
+ };
1795
+ //# sourceMappingURL=messages.js.map