@vanzxy/baileys 1.4.2 → 1.4.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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * lib/Utils/MessageBuilder.js — AIRich / Button / ButtonV2 / Carousel / Toolkit
3
3
  *
4
- * Part of @vanzxy/baileys 1.3.9. Third-party attribution for the base
4
+ * Part of @vanzxy/baileys 1.4.4. Third-party attribution for the base
5
5
  * implementation this file was adapted from is kept in /NOTICE.md
6
6
  * (not inline here) per the original author's license terms.
7
7
  *
@@ -9,11 +9,36 @@
9
9
  * @blurose/baileys 1.1.13 fork (base, feature-complete) with perf defaults
10
10
  * and message-authenticity fields backported from arslan-baileys 1.1.0 and
11
11
  * this project's own rich-message-utils.js. See changes tagged "Vanz@" below.
12
+ *
13
+ * Vanz@Merge 22-08-26 --- v4.6 -> v4.7. Button class hardened + extended:
14
+ * - Bug fix: addCall() wrote its second arg into buttonParamsJson.id, but
15
+ * the cta_call native-flow schema keys on `phone_number` (id is ignored
16
+ * by WhatsApp for this button type). Confirmed against 3 independent
17
+ * current Baileys-fork call sites before changing the wire shape.
18
+ * - Required-field validation added to the CTA helpers that silently
19
+ * produced a button WA would render but never route correctly
20
+ * (empty id/url/copy_code/phone_number).
21
+ * - New native-flow helpers for names WA recognises beyond the "mixed"
22
+ * set (cta_catalog, open_webview, call_permission_request,
23
+ * automated_greeting_message_view_catalog, payment_info,
24
+ * review_and_pay, wa_payment_transaction_details, mpm) — see the
25
+ * Button.#SPECIAL_FLOW map and each method's JSDoc for the
26
+ * business/official-client gating caveat.
27
+ * - send() now picks the correct <native_flow v=.. name=..> node per the
28
+ * first button's name instead of always emitting v=9 name=mixed, which
29
+ * is required for the special names above to have a chance of
30
+ * rendering at all (mirrors the same class of bug already fixed for
31
+ * lone single_select in 1.3.x).
32
+ * - JSDoc added across Toolkit/BaseBuilder/Button/ButtonV2/Carousel/AIRich
33
+ * for editor hover-docs; kept in sync with MessageBuilder.d.ts.
34
+ * All additions above are original implementations written against public
35
+ * Baileys-ecosystem documentation of the native-flow wire format, not
36
+ * copied from any third-party fork.
12
37
  */
13
38
 
14
39
  'use strict';
15
40
 
16
- const MESSAGE_BUILDER_VERSION = '4.6';
41
+ const MESSAGE_BUILDER_VERSION = '4.7';
17
42
 
18
43
  import { generateWAMessageFromContent, prepareWAMessageMedia } from './messages.js';
19
44
  import { botMetadataSignature, botMetadataCertificate } from './rich-message-utils.js';
@@ -235,13 +260,16 @@ async function waitAllPromises(input) {
235
260
  return deep(await input);
236
261
  }
237
262
 
263
+ /** Static grab-bag of media/text helpers shared by the builder classes above. */
238
264
  class Toolkit {
239
265
  constructor() {}
240
266
 
267
+ /** Parse `[label](url)` hyperlinks, `[]()` citations, and `[expr]<img-url>` latex tags out of `text`. */
241
268
  static extractIE(text, { extract = true, hyperlink = true, citation = true, latex = true } = {}) {
242
269
  return extractIE(text, { extract, hyperlink, citation, latex });
243
270
  }
244
271
 
272
+ /** Resize an image buffer to `x`×`y` via sharp (lazy-loaded; throws with an install hint if sharp isn't present). */
245
273
  static async resize(buffer, x, y, fit = 'cover') {
246
274
  const sharp = await getSharp();
247
275
  return await sharp(buffer)
@@ -254,10 +282,12 @@ class Toolkit {
254
282
  .toBuffer();
255
283
  }
256
284
 
285
+ /** Deeply await every Promise nested in `input` (objects/arrays), resolving it into plain values. */
257
286
  static async waitAllPromises(input) {
258
287
  return await waitAllPromises(input);
259
288
  }
260
289
 
290
+ /** Fetch `url` into a Buffer. @param {boolean} [silent] Return an empty Buffer instead of throwing on failure. */
261
291
  static async fetchBuffer(url, options = {}, { silent = true } = {}) {
262
292
  try {
263
293
  let response = await fetch(url, options);
@@ -269,6 +299,7 @@ class Toolkit {
269
299
  }
270
300
  }
271
301
 
302
+ /** Upload media to WhatsApp's media server and return its `url`/`directPath` descriptor. */
272
303
  static async toUrl(_client, path, mediaType = 'document') {
273
304
  if (!path) throw new Error('Url or buffer needed');
274
305
 
@@ -285,6 +316,7 @@ class Toolkit {
285
316
  return Object.values(media)[0]?.url;
286
317
  }
287
318
 
319
+ /** Normalize a url/buffer/array of either into the requested `result` shape ('url' | 'buffer' | 'base64'), optionally resizing and/or uploading to WA's media server first. */
288
320
  static async resolveMedia(_client, media, mediaType = 'image', { resolveUrl = false, resolveWAUrl = false, result = 'url', resize = false, width = 300, height = 300 } = {}) {
289
321
  const isUrl = (str) => /^https?:\/\/.+/i.test(str);
290
322
 
@@ -305,8 +337,6 @@ class Toolkit {
305
337
  );
306
338
  }
307
339
 
308
- const originalIsBuffer = Buffer.isBuffer(media);
309
-
310
340
  if (typeof media === 'string' && isUrl(media)) {
311
341
  if (isWAUrl(media)) {
312
342
  if (resolveWAUrl) {
@@ -347,13 +377,14 @@ class Toolkit {
347
377
  return media.toString('base64');
348
378
  }
349
379
 
350
- if (originalIsBuffer) {
351
- return Toolkit.toUrl(_client, media, mediaType);
352
- }
353
-
380
+ // Vanz@Fix 22-08-26 (v4.7) --- both branches of the old if/else here returned the exact
381
+ // same `Toolkit.toUrl(_client, media, mediaType)` call (dead branching left over from an
382
+ // earlier version that must have treated buffer vs non-buffer input differently). Collapsed
383
+ // to a single return; `originalIsBuffer` is now unused and removed below.
354
384
  return Toolkit.toUrl(_client, media, mediaType);
355
385
  }
356
386
 
387
+ /** Read an mp4 buffer's duration (seconds) straight from its moov atom, no ffprobe needed. */
357
388
  static getMp4Duration(buffer, { silent = true } = {}) {
358
389
  try {
359
390
  if (!Buffer.isBuffer(buffer) || buffer.length < 8) {
@@ -431,6 +462,7 @@ class Toolkit {
431
462
  }
432
463
  }
433
464
 
465
+ /** Extract a single frame from an mp4 buffer as a thumbnail (ffmpeg lazy-loaded; throws with an install hint if missing). */
434
466
  static getMp4Preview(videoBuffer, { time, result = 'buffer', resize = true, width = 300, height = 300, silent = true } = {}) {
435
467
  return new Promise((resolve, reject) => {
436
468
  const fail = (err) => {
@@ -491,6 +523,12 @@ class Toolkit {
491
523
  }
492
524
  }
493
525
 
526
+ /**
527
+ * Shared chaining base for Button/ButtonV2/Carousel/AIRich: title/subtitle/body/footer,
528
+ * contextInfo (quoted/mentions/etc.), and an escape-hatch payload merged verbatim
529
+ * into the generated message content.
530
+ * @abstract
531
+ */
494
532
  class BaseBuilder {
495
533
  constructor() {
496
534
  this._title = '';
@@ -501,6 +539,7 @@ class BaseBuilder {
501
539
  this._extraPayload = {};
502
540
  }
503
541
 
542
+ /** @param {string} title */
504
543
  setTitle(title) {
505
544
  if (typeof title !== 'string') {
506
545
  throw new TypeError('Title must be a string');
@@ -509,6 +548,7 @@ class BaseBuilder {
509
548
  return this;
510
549
  }
511
550
 
551
+ /** @param {string} subtitle */
512
552
  setSubtitle(subtitle) {
513
553
  if (typeof subtitle !== 'string') {
514
554
  throw new TypeError('Subtitle must be a string');
@@ -517,6 +557,7 @@ class BaseBuilder {
517
557
  return this;
518
558
  }
519
559
 
560
+ /** @param {string} body Main message text. */
520
561
  setBody(body) {
521
562
  if (typeof body !== 'string') {
522
563
  throw new TypeError('Body must be a string');
@@ -525,6 +566,7 @@ class BaseBuilder {
525
566
  return this;
526
567
  }
527
568
 
569
+ /** @param {string} footer */
528
570
  setFooter(footer) {
529
571
  if (typeof footer !== 'string') {
530
572
  throw new TypeError('Footer must be a string');
@@ -533,6 +575,7 @@ class BaseBuilder {
533
575
  return this;
534
576
  }
535
577
 
578
+ /** @param {Record<string, any>} obj Raw `contextInfo` (quotedMessage, mentionedJid, etc.), merged verbatim. */
536
579
  setContextInfo(obj) {
537
580
  if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
538
581
  throw new TypeError('ContextInfo must be a plain object');
@@ -542,6 +585,7 @@ class BaseBuilder {
542
585
  return this;
543
586
  }
544
587
 
588
+ /** Escape hatch: shallow-merge arbitrary keys into the generated message content, alongside whatever this builder produces. */
545
589
  addPayload(obj) {
546
590
  if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
547
591
  throw new TypeError('Payload must be a plain object');
@@ -553,9 +597,17 @@ class BaseBuilder {
553
597
  }
554
598
  }
555
599
 
600
+ /**
601
+ * Interactive (native-flow) message builder — header/body/footer + a mix of
602
+ * buttons (quick_reply, cta_url, cta_call, single_select, ...). Sends via
603
+ * `interactiveMessage` (or falls back to a legacy `listMessage` when the
604
+ * only button is a lone `single_select`, since WA doesn't render that
605
+ * combination as native-flow).
606
+ */
556
607
  class Button extends BaseBuilder {
557
608
  #client;
558
609
 
610
+ /** @param {import('../../WAProto/index.js').WASocket} client Active Baileys socket (must expose `relayMessage`). */
559
611
  constructor(client) {
560
612
  super();
561
613
  if (!client) {
@@ -570,24 +622,28 @@ class Button extends BaseBuilder {
570
622
  this._params = {};
571
623
  }
572
624
 
625
+ /** Attach a video as the interactive header. @param {string|Buffer} path Url or buffer. */
573
626
  setVideo(path, options = {}) {
574
627
  if (!path) throw new Error('Url or buffer needed');
575
628
  Buffer.isBuffer(path) ? (this._data = { video: path, ...options }) : (this._data = { video: { url: path }, ...options });
576
629
  return this;
577
630
  }
578
631
 
632
+ /** Attach an image as the interactive header. @param {string|Buffer} path Url or buffer. */
579
633
  setImage(path, options = {}) {
580
634
  if (!path) throw new Error('Url or buffer needed');
581
635
  Buffer.isBuffer(path) ? (this._data = { image: path, ...options }) : (this._data = { image: { url: path }, ...options });
582
636
  return this;
583
637
  }
584
638
 
639
+ /** Attach a document as the interactive header. @param {string|Buffer} path Url or buffer. */
585
640
  setDocument(path, options = {}) {
586
641
  if (!path) throw new Error('Url or buffer needed');
587
642
  Buffer.isBuffer(path) ? (this._data = { document: path, ...options }) : (this._data = { document: { url: path }, ...options });
588
643
  return this;
589
644
  }
590
645
 
646
+ /** Set a raw pre-built header media object (bypasses setVideo/setImage/setDocument shorthands). */
591
647
  setMedia(obj) {
592
648
  if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
593
649
  throw new TypeError('Media must be a plain object');
@@ -597,17 +653,30 @@ class Button extends BaseBuilder {
597
653
  return this;
598
654
  }
599
655
 
656
+ /** Remove every button added so far (keeps title/body/footer/media). */
600
657
  clearButtons() {
601
658
  this._buttons = [];
602
659
  return this;
603
660
  }
604
661
 
662
+ /** Overwrite the message-level `nativeFlowMessage.messageParamsJson` payload wholesale. */
605
663
  setParams(obj) {
606
664
  this._params = obj;
607
665
  return this;
608
666
  }
609
667
 
668
+ /**
669
+ * Low-level escape hatch: push a raw native-flow button by name. Prefer the
670
+ * dedicated `add*()` helpers below when one exists — they validate the
671
+ * required keys for that button type.
672
+ * @param {string} name Native-flow button name, e.g. 'quick_reply', 'cta_url'.
673
+ * @param {string|Record<string, any>} params Either a pre-stringified JSON payload or a plain object.
674
+ */
610
675
  addButton(name, params) {
676
+ if (typeof name !== 'string' || !name.trim()) {
677
+ throw new TypeError('addButton(name, params) requires a non-empty string name');
678
+ }
679
+
611
680
  this._buttons.push({
612
681
  name,
613
682
  buttonParamsJson: typeof params === 'string' ? params : JSON.stringify(params),
@@ -616,16 +685,21 @@ class Button extends BaseBuilder {
616
685
  return this;
617
686
  }
618
687
 
688
+ /** Append a row to the section currently open on the last `addSelection()` (call `makeSection()` first). */
619
689
  makeRow(header = '', title = '', description = '', id = '') {
620
690
  if (this._currentSelectionIndex === -1 || this._currentSectionIndex === -1) {
621
691
  throw new Error('You need to create a selection and a section first');
622
692
  }
693
+ if (!title || !id) {
694
+ throw new TypeError('makeRow() requires both a title and an id');
695
+ }
623
696
  const buttonParams = JSON.parse(this._buttons[this._currentSelectionIndex].buttonParamsJson);
624
697
  buttonParams.sections[this._currentSectionIndex].rows.push({ header, title, description, id });
625
698
  this._buttons[this._currentSelectionIndex].buttonParamsJson = JSON.stringify(buttonParams);
626
699
  return this;
627
700
  }
628
701
 
702
+ /** Open a new section on the `single_select` button added by the last `addSelection()` call. */
629
703
  makeSection(title = '', highlight_label = '') {
630
704
  if (this._currentSelectionIndex === -1) {
631
705
  throw new Error('You need to create a selection first');
@@ -637,14 +711,24 @@ class Button extends BaseBuilder {
637
711
  return this;
638
712
  }
639
713
 
714
+ /** Start a `single_select` (in-button picker list) button. Follow with `makeSection()` + `makeRow()`. */
640
715
  addSelection(title, options = {}) {
716
+ if (!title) throw new TypeError('addSelection(title) requires a non-empty title');
641
717
  this._buttons.push({ ...options, name: 'single_select', buttonParamsJson: JSON.stringify({ title, sections: [] }) });
642
718
  this._currentSelectionIndex = this._buttons.length - 1;
643
719
  this._currentSectionIndex = -1;
644
720
  return this;
645
721
  }
646
722
 
723
+ /**
724
+ * Add a `quick_reply` button — sends `id` back as the interactive-response id when tapped.
725
+ * @param {string} display_text Button label.
726
+ * @param {string} id Unique id returned on tap; required, WA silently drops replies without one.
727
+ */
647
728
  addReply(display_text = '', id = '', options = {}) {
729
+ if (!display_text || !id) {
730
+ throw new TypeError('addReply(display_text, id) requires both a label and a unique id');
731
+ }
648
732
  this._buttons.push({
649
733
  name: 'quick_reply',
650
734
  buttonParamsJson: JSON.stringify({
@@ -656,19 +740,37 @@ class Button extends BaseBuilder {
656
740
  return this;
657
741
  }
658
742
 
659
- addCall(display_text = '', id = '', options = {}) {
743
+ /**
744
+ * Add a `cta_call` (tap-to-dial) button.
745
+ * @param {string} display_text Button label.
746
+ * @param {string} phone_number Phone number to dial, e.g. '+15551234567'.
747
+ */
748
+ // Vanz@Fix 22-08-26 (v4.7) --- second arg used to be written into buttonParamsJson.id, but
749
+ // the cta_call schema keys on `phone_number` (confirmed against @chatunity/baileys,
750
+ // @neoxr/wb, and a WhiskeySockets/Baileys#2626 working example) — `id` is simply ignored
751
+ // by WhatsApp for this button, so every button built with the old addCall() silently
752
+ // rendered with no dial action. Kept the same 2nd-positional-arg call shape so existing
753
+ // call sites keep working; only the wire key changed.
754
+ addCall(display_text = '', phone_number = '', options = {}) {
755
+ if (!display_text || !phone_number) {
756
+ throw new TypeError('addCall(display_text, phone_number) requires both a label and a phone number');
757
+ }
660
758
  this._buttons.push({
661
759
  name: 'cta_call',
662
760
  buttonParamsJson: JSON.stringify({
663
761
  display_text,
664
- id,
762
+ phone_number,
665
763
  ...options,
666
764
  }),
667
765
  });
668
766
  return this;
669
767
  }
670
768
 
769
+ /** Add a `cta_reminder` button (schedules an in-chat reminder chip). */
671
770
  addReminder(display_text = '', id = '', options = {}) {
771
+ if (!display_text || !id) {
772
+ throw new TypeError('addReminder(display_text, id) requires both a label and a unique id');
773
+ }
672
774
  this._buttons.push({
673
775
  name: 'cta_reminder',
674
776
  buttonParamsJson: JSON.stringify({
@@ -680,7 +782,11 @@ class Button extends BaseBuilder {
680
782
  return this;
681
783
  }
682
784
 
785
+ /** Add a `cta_cancel_reminder` button, pairs with `addReminder()`. */
683
786
  addCancelReminder(display_text = '', id = '', options = {}) {
787
+ if (!display_text || !id) {
788
+ throw new TypeError('addCancelReminder(display_text, id) requires both a label and a unique id');
789
+ }
684
790
  this._buttons.push({
685
791
  name: 'cta_cancel_reminder',
686
792
  buttonParamsJson: JSON.stringify({
@@ -692,7 +798,11 @@ class Button extends BaseBuilder {
692
798
  return this;
693
799
  }
694
800
 
801
+ /** Add an `address_message` button (prompts the user's saved-address picker). */
695
802
  addAddress(display_text = '', id = '', options = {}) {
803
+ if (!display_text || !id) {
804
+ throw new TypeError('addAddress(display_text, id) requires both a label and a unique id');
805
+ }
696
806
  this._buttons.push({
697
807
  name: 'address_message',
698
808
  buttonParamsJson: JSON.stringify({
@@ -704,6 +814,7 @@ class Button extends BaseBuilder {
704
814
  return this;
705
815
  }
706
816
 
817
+ /** Add a `send_location` button (requests the user's live location). */
707
818
  addLocation(options = {}) {
708
819
  this._buttons.push({
709
820
  name: 'send_location',
@@ -712,13 +823,26 @@ class Button extends BaseBuilder {
712
823
  return this;
713
824
  }
714
825
 
826
+ /**
827
+ * Add a `cta_url` button.
828
+ * @param {string} display_text Button label.
829
+ * @param {string} url Url opened on tap.
830
+ * @param {boolean} webview_interaction Open inside WhatsApp's in-app webview instead of the system browser.
831
+ */
832
+ // Vanz@Add 22-08-26 (v4.7) --- `merchant_url` is present (and equal to `url`) on every
833
+ // cta_url button seen in captured client traffic, alongside `url`. Defaulted here rather
834
+ // than left for the caller to remember; still overridable via options if it should differ.
715
835
  addUrl(display_text = '', url = '', webview_interaction = false, options = {}) {
836
+ if (!display_text || !url) {
837
+ throw new TypeError('addUrl(display_text, url) requires both a label and a url');
838
+ }
716
839
  this._buttons.push({
717
840
  ...options,
718
841
  name: 'cta_url',
719
842
  buttonParamsJson: JSON.stringify({
720
843
  display_text,
721
844
  url,
845
+ merchant_url: url,
722
846
  webview_interaction,
723
847
  ...options,
724
848
  }),
@@ -726,7 +850,11 @@ class Button extends BaseBuilder {
726
850
  return this;
727
851
  }
728
852
 
853
+ /** Add a `cta_copy` button (copies `copy_code` to the user's clipboard on tap). */
729
854
  addCopy(display_text = '', copy_code = '', options = {}) {
855
+ if (!display_text || !copy_code) {
856
+ throw new TypeError('addCopy(display_text, copy_code) requires both a label and the text to copy');
857
+ }
730
858
  this._buttons.push({
731
859
  name: 'cta_copy',
732
860
  buttonParamsJson: JSON.stringify({
@@ -738,6 +866,138 @@ class Button extends BaseBuilder {
738
866
  return this;
739
867
  }
740
868
 
869
+ /**
870
+ * Add an `open_webview` button — opens a titled in-app webview (distinct from
871
+ * `cta_url`'s `webview_interaction` flag: this is its own native-flow name).
872
+ * @param {string} title Webview title shown in the header bar.
873
+ * @param {string} url Url loaded inside the webview.
874
+ */
875
+ addOpenWebview(title = '', url = '', options = {}) {
876
+ if (!title || !url) {
877
+ throw new TypeError('addOpenWebview(title, url) requires both a title and a url');
878
+ }
879
+ this._buttons.push({
880
+ name: 'open_webview',
881
+ buttonParamsJson: JSON.stringify({
882
+ title,
883
+ link: { url },
884
+ ...options,
885
+ }),
886
+ });
887
+ return this;
888
+ }
889
+
890
+ /**
891
+ * Add a `cta_catalog` button (opens the sender's WhatsApp Business catalog).
892
+ * Vanz@Add 22-08-26 (v4.7) --- WA only shows the catalog action for accounts
893
+ * that actually have a catalog attached; on regular accounts the button may
894
+ * render inert. See Button.#SPECIAL_FLOW for the native-flow node this name requires.
895
+ */
896
+ addCatalog(display_text = '', options = {}) {
897
+ this._buttons.push({
898
+ name: 'cta_catalog',
899
+ buttonParamsJson: JSON.stringify({
900
+ ...(display_text ? { display_text } : {}),
901
+ ...options,
902
+ }),
903
+ });
904
+ return this;
905
+ }
906
+
907
+ /**
908
+ * Add an `automated_greeting_message_view_catalog` button — the "View catalog"
909
+ * action WhatsApp Business shows on the automated greeting message.
910
+ * Vanz@Add 22-08-26 (v4.7). Business-account only; see Button.#SPECIAL_FLOW.
911
+ */
912
+ addViewCatalog(options = {}) {
913
+ this._buttons.push({
914
+ name: 'automated_greeting_message_view_catalog',
915
+ buttonParamsJson: JSON.stringify(options),
916
+ });
917
+ return this;
918
+ }
919
+
920
+ /**
921
+ * Add a `call_permission_request` button — asks the user to grant call
922
+ * permission before a voice/video call can be placed.
923
+ * Vanz@Add 22-08-26 (v4.7). See Button.#SPECIAL_FLOW.
924
+ */
925
+ addCallPermission(display_text = '', options = {}) {
926
+ this._buttons.push({
927
+ name: 'call_permission_request',
928
+ buttonParamsJson: JSON.stringify({
929
+ ...(display_text ? { display_text } : {}),
930
+ ...options,
931
+ }),
932
+ });
933
+ return this;
934
+ }
935
+
936
+ /**
937
+ * Add a `payment_info` button carrying a structured payment-settings payload
938
+ * (e.g. a PIX static-code block). Payload shape is dictated by WhatsApp's
939
+ * payment flows and is passed through as given — validate it yourself.
940
+ * Vanz@Add 22-08-26 (v4.7). Business/payment-enabled accounts only.
941
+ * @param {Record<string, any>} payload e.g. `{ payment_settings: [{ type: 'pix_static_code', pix_static_code: {...} }] }`.
942
+ */
943
+ addPaymentInfo(payload = {}) {
944
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
945
+ throw new TypeError('addPaymentInfo(payload) requires a plain object');
946
+ }
947
+ this._buttons.push({
948
+ name: 'payment_info',
949
+ buttonParamsJson: JSON.stringify(payload),
950
+ });
951
+ return this;
952
+ }
953
+
954
+ /**
955
+ * Add a `review_and_pay` button (order/payment summary flow).
956
+ * Vanz@Add 22-08-26 (v4.7). Server-validated by WhatsApp; malformed or
957
+ * unauthorized payloads are typically ignored rather than erroring locally.
958
+ * @param {Record<string, any>} payload Order/payment summary payload.
959
+ */
960
+ addReviewAndPay(payload = {}) {
961
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
962
+ throw new TypeError('addReviewAndPay(payload) requires a plain object');
963
+ }
964
+ this._buttons.push({
965
+ name: 'review_and_pay',
966
+ buttonParamsJson: JSON.stringify(payload),
967
+ });
968
+ return this;
969
+ }
970
+
971
+ /**
972
+ * Add a `wa_payment_transaction_details` button referencing a prior transaction.
973
+ * Vanz@Add 22-08-26 (v4.7). See Button.#SPECIAL_FLOW.
974
+ */
975
+ addTransactionDetails(payload = {}) {
976
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
977
+ throw new TypeError('addTransactionDetails(payload) requires a plain object');
978
+ }
979
+ this._buttons.push({
980
+ name: 'wa_payment_transaction_details',
981
+ buttonParamsJson: JSON.stringify(payload),
982
+ });
983
+ return this;
984
+ }
985
+
986
+ /**
987
+ * Add an `mpm` (multi-product message) button referencing a set of catalog items.
988
+ * Vanz@Add 22-08-26 (v4.7). Business-catalog accounts only; see Button.#SPECIAL_FLOW.
989
+ */
990
+ addMultiProduct(payload = {}) {
991
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
992
+ throw new TypeError('addMultiProduct(payload) requires a plain object');
993
+ }
994
+ this._buttons.push({
995
+ name: 'mpm',
996
+ buttonParamsJson: JSON.stringify(payload),
997
+ });
998
+ return this;
999
+ }
1000
+
741
1001
  // Vanz@Fix (bug 43) --- paramsList documented the schema for these 3 message-level native flow
742
1002
  // params (limited_time_offer / bottom_sheet / tap_target_configuration) but no helper ever wrote
743
1003
  // them into this._params — only manual setParams() could, with zero validation against the
@@ -756,6 +1016,7 @@ class Button extends BaseBuilder {
756
1016
  }
757
1017
  }
758
1018
 
1019
+ /** Set the message-level "limited time offer" strip (countdown banner above the buttons). */
759
1020
  setLimitedTimeOffer({ text = '', url = '', copy_code = '', expiration_time } = {}) {
760
1021
  const data = { text, url, copy_code, expiration_time };
761
1022
  Button.#validateAgainstSchema(Button.paramsList.limited_time_offer, data, 'limited_time_offer');
@@ -763,6 +1024,7 @@ class Button extends BaseBuilder {
763
1024
  return this;
764
1025
  }
765
1026
 
1027
+ /** Configure how many buttons show inline before the rest collapse into a bottom sheet. */
766
1028
  setBottomSheet({ in_thread_buttons_limit, divider_indices = [], list_title = '', button_title = '' } = {}) {
767
1029
  const data = { in_thread_buttons_limit, divider_indices, list_title, button_title };
768
1030
  Button.#validateAgainstSchema(Button.paramsList.bottom_sheet, data, 'bottom_sheet');
@@ -770,6 +1032,7 @@ class Button extends BaseBuilder {
770
1032
  return this;
771
1033
  }
772
1034
 
1035
+ /** Configure the tap-target callout shown pointing at a specific button by index. */
773
1036
  setTapTargetConfiguration({ title = '', description = '', canonical_url = '', domain = '', buttonIndex = 0 } = {}) {
774
1037
  const data = { title, description, canonical_url, domain, buttonIndex };
775
1038
  Button.#validateAgainstSchema(Button.paramsList.tap_target_configuration, data, 'tap_target_configuration');
@@ -799,6 +1062,25 @@ class Button extends BaseBuilder {
799
1062
  },
800
1063
  };
801
1064
 
1065
+ // Vanz@Add 22-08-26 (v4.7) --- native-flow names WA treats specially: the client
1066
+ // only recognises these when the *first* button's name matches AND the wrapping
1067
+ // <native_flow> biz-node carries the right v/name for that name. Everything not
1068
+ // listed here (quick_reply, cta_url, cta_call, cta_copy, single_select mixed with
1069
+ // others, etc.) uses the generic v=9 name=mixed node, which is what send() emitted
1070
+ // unconditionally before this change. Table cross-checked against the observed
1071
+ // wire behaviour documented by zqdevelopers/zq_baileys_helper and @chatunity/baileys.
1072
+ static #SPECIAL_FLOW = {
1073
+ review_and_pay: { v: '1', name: 'order_details' },
1074
+ payment_info: { v: '1', name: 'payment_info' },
1075
+ mpm: { v: '2', name: 'mpm' },
1076
+ cta_catalog: { v: '2', name: 'cta_catalog' },
1077
+ send_location: { v: '2', name: 'send_location' },
1078
+ call_permission_request: { v: '2', name: 'call_permission_request' },
1079
+ wa_payment_transaction_details: { v: '2', name: 'wa_payment_transaction_details' },
1080
+ automated_greeting_message_view_catalog: { v: '2', name: 'automated_greeting_message_view_catalog' },
1081
+ };
1082
+
1083
+ /** Render this builder's header/body/footer/media/buttons/params into an `interactiveMessage`-shaped card (without the outer `interactiveMessage` wrapper or contextInfo). */
802
1084
  async toCard() {
803
1085
  return {
804
1086
  body: {
@@ -825,7 +1107,50 @@ class Button extends BaseBuilder {
825
1107
  };
826
1108
  }
827
1109
 
1110
+ // Vanz@Fix (bug: single_select alone doesn't render) --- WhatsApp only renders a
1111
+ // `single_select` native_flow button when it's mixed with other native_flow buttons
1112
+ // (biz node <native_flow v='9' name='mixed'>). 'single_select' is NOT in the set of
1113
+ // button names WA treats as a standalone native_flow type (confirmed against
1114
+ // itsliaaa/baileys WABinary/generic-utils.js getBizBinaryNode() — it special-cases
1115
+ // message.listMessage separately, with its own biz node <list v='2' type='product_list'>,
1116
+ // and never routes 'single_select' through the single-type native_flow path).
1117
+ // A lone single_select must be sent as a legacy `listMessage` instead.
1118
+ #isLoneSingleSelect() {
1119
+ return this._buttons.length === 1 && this._buttons[0].name === 'single_select';
1120
+ }
1121
+
1122
+ #toListMessage() {
1123
+ const { title: buttonText, sections } = JSON.parse(this._buttons[0].buttonParamsJson);
1124
+ return {
1125
+ listMessage: {
1126
+ title: this._title || undefined,
1127
+ description: this._body || undefined,
1128
+ footerText: this._footer || undefined,
1129
+ buttonText: buttonText || undefined,
1130
+ listType: 1,
1131
+ sections: (sections || []).map((s) => ({
1132
+ title: s.title,
1133
+ rows: (s.rows || []).map((r) => ({
1134
+ title: r.title || r.header || '',
1135
+ description: r.description || '',
1136
+ rowId: r.id || '',
1137
+ })),
1138
+ })),
1139
+ contextInfo: this._contextInfo,
1140
+ },
1141
+ };
1142
+ }
1143
+
1144
+ /** @returns {Record<string, any>} The final content object, without generating/wrapping a WAMessage. Useful when composing this interactive card into something else (e.g. Carousel). */
828
1145
  async build(jid, { ...options } = {}) {
1146
+ if (this._buttons.length === 0) {
1147
+ throw new Error('Button requires at least one button (use addReply/addUrl/addCall/addSelection/addButton/...)');
1148
+ }
1149
+
1150
+ if (this.#isLoneSingleSelect()) {
1151
+ return generateWAMessageFromContent(jid, { ...this._extraPayload, ...this.#toListMessage() }, { ...options });
1152
+ }
1153
+
829
1154
  const message = await this.toCard();
830
1155
 
831
1156
  return generateWAMessageFromContent(
@@ -841,22 +1166,35 @@ class Button extends BaseBuilder {
841
1166
  );
842
1167
  }
843
1168
 
1169
+ // Vanz@Add 22-08-26 (v4.7) --- picks the native_flow node variant for the first
1170
+ // button's name, per Button.#SPECIAL_FLOW. Falls back to the generic mixed node
1171
+ // (previous unconditional behaviour) for anything not in that map.
1172
+ #buildNativeFlowNode() {
1173
+ const special = Button.#SPECIAL_FLOW[this._buttons[0]?.name];
1174
+ return special ? { tag: 'native_flow', attrs: special } : { tag: 'native_flow', attrs: { v: '9', name: 'mixed' } };
1175
+ }
1176
+
1177
+ /** Build and send this interactive message. @param {string} jid Destination chat/group jid. */
844
1178
  async send(jid, { ...options } = {}) {
845
1179
  const msg = await this.build(jid, options);
846
1180
 
1181
+ const bizContent = this.#isLoneSingleSelect()
1182
+ ? [{ tag: 'list', attrs: { v: '2', type: 'product_list' } }]
1183
+ : [
1184
+ {
1185
+ tag: 'interactive',
1186
+ attrs: { type: 'native_flow', v: '1' },
1187
+ content: [this.#buildNativeFlowNode()],
1188
+ },
1189
+ ];
1190
+
847
1191
  await this.#client.relayMessage(msg.key.remoteJid, msg.message, {
848
1192
  messageId: msg.key.id,
849
1193
  additionalNodes: [
850
1194
  {
851
1195
  tag: 'biz',
852
1196
  attrs: {},
853
- content: [
854
- {
855
- tag: 'interactive',
856
- attrs: { type: 'native_flow', v: '1' },
857
- content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }],
858
- },
859
- ],
1197
+ content: bizContent,
860
1198
  },
861
1199
  ],
862
1200
  ...options,
@@ -865,9 +1203,15 @@ class Button extends BaseBuilder {
865
1203
  }
866
1204
  }
867
1205
 
1206
+ /**
1207
+ * Legacy `buttonsMessage` builder (up to 3 simple quick-reply buttons under a
1208
+ * media/location header). Simpler and more universally supported than
1209
+ * `Button`'s native-flow messages, but capped to `type: 1` quick replies.
1210
+ */
868
1211
  class ButtonV2 extends BaseBuilder {
869
1212
  #client;
870
1213
 
1214
+ /** @param {import('../../WAProto/index.js').WASocket} client Active Baileys socket. */
871
1215
  constructor(client) {
872
1216
  super();
873
1217
  if (!client) {
@@ -880,7 +1224,9 @@ class ButtonV2 extends BaseBuilder {
880
1224
  this._buttons = [];
881
1225
  }
882
1226
 
1227
+ /** Add a simple quick-reply button. @param {string} displayText Label. @param {string} [buttonId] Defaults to a random uuid. */
883
1228
  addButton(displayText = '', buttonId = crypto.randomUUID()) {
1229
+ if (!displayText) throw new TypeError('addButton(displayText) requires a non-empty label');
884
1230
  this._buttons.push({
885
1231
  buttonId,
886
1232
  buttonText: { displayText },
@@ -889,6 +1235,7 @@ class ButtonV2 extends BaseBuilder {
889
1235
  return this;
890
1236
  }
891
1237
 
1238
+ /** Push a raw pre-built button object, bypassing the `addButton()` shorthand. */
892
1239
  addRawButton(obj) {
893
1240
  if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
894
1241
  throw new TypeError('Buttons must be a plain object');
@@ -898,12 +1245,14 @@ class ButtonV2 extends BaseBuilder {
898
1245
  return this;
899
1246
  }
900
1247
 
1248
+ /** Set the header thumbnail (used as a fallback location-header image when no `setMedia()` header is given). */
901
1249
  setThumbnail(path) {
902
1250
  if (!path) throw new Error('Url or buffer needed');
903
1251
  this._image = path;
904
1252
  return this;
905
1253
  }
906
1254
 
1255
+ /** Set a raw pre-built header media object for the buttons message. */
907
1256
  setMedia(obj) {
908
1257
  if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
909
1258
  throw new TypeError('Media must be a plain object');
@@ -913,8 +1262,15 @@ class ButtonV2 extends BaseBuilder {
913
1262
  return this;
914
1263
  }
915
1264
 
916
- async build(jid, { ...options } = {}) {
917
- let _thumbnail = this._image ? await Toolkit.resize(Buffer.isBuffer(this._image) ? this._image : await Toolkit.fetchBuffer(this._image, {}, { silent: true }), 300, 300) : null;
1265
+ // Vanz@Fix 22-08-26 (v4.7) --- _thumbnail was computed unconditionally (fetch + resize) even
1266
+ // when setMedia() is used, in which case the location-fallback header (the only place
1267
+ // _thumbnail is used) never runs at all — wasted network/CPU work on every build() call.
1268
+ // Now only computed when it'll actually be used. Also: `viewOnce` was hardcoded true with no
1269
+ // way to opt out (kept as the default — some clients need it to render legacy buttonsMessage
1270
+ // at all — but it's now a `{ viewOnce = true }` option instead of a hardcoded literal).
1271
+ /** @returns {Promise<Record<string, any>>} The generated WAMessage (without sending). @param {boolean} [viewOnce] Default true — some clients require this for legacy buttonsMessage to render; pass false to send it as a normal (non-disappearing) message. */
1272
+ async build(jid, { viewOnce = true, ...options } = {}) {
1273
+ const _thumbnail = !this._data && this._image ? await Toolkit.resize(Buffer.isBuffer(this._image) ? this._image : await Toolkit.fetchBuffer(this._image, {}, { silent: true }), 300, 300) : null;
918
1274
  const msg = generateWAMessageFromContent(
919
1275
  jid,
920
1276
  {
@@ -934,7 +1290,7 @@ class ButtonV2 extends BaseBuilder {
934
1290
  jpegThumbnail: _thumbnail,
935
1291
  },
936
1292
  }),
937
- viewOnce: true,
1293
+ viewOnce,
938
1294
  contextInfo: this._contextInfo,
939
1295
  buttons: [...this._buttons],
940
1296
  },
@@ -944,6 +1300,7 @@ class ButtonV2 extends BaseBuilder {
944
1300
  return msg;
945
1301
  }
946
1302
 
1303
+ /** Build and send this buttons message. @param {string} jid Destination chat/group jid. */
947
1304
  async send(jid, { ...options } = {}) {
948
1305
  if (this._buttons.length < 1) throw new Error('ButtonV2 requires at least one button');
949
1306
  const msg = await this.build(jid, options);
@@ -969,9 +1326,16 @@ class ButtonV2 extends BaseBuilder {
969
1326
  }
970
1327
  }
971
1328
 
1329
+ /** Carousel of interactive cards (each with its own header media + optional buttons), scrollable horizontally in-chat. */
972
1330
  class Carousel extends BaseBuilder {
973
1331
  #client;
974
1332
 
1333
+ // Vanz@Add 22-08-26 (v4.7) --- WhatsApp caps carousels at 10 cards; anything beyond
1334
+ // that is silently truncated client-side, so failing fast here is more useful than
1335
+ // shipping a carousel that quietly loses cards.
1336
+ static MAX_CARDS = 10;
1337
+
1338
+ /** @param {import('../../WAProto/index.js').WASocket} client Active Baileys socket. */
975
1339
  constructor(client) {
976
1340
  super();
977
1341
  if (!client) {
@@ -982,6 +1346,11 @@ class Carousel extends BaseBuilder {
982
1346
  this._cards = [];
983
1347
  }
984
1348
 
1349
+ /**
1350
+ * Add one card, or an array of cards, to the carousel.
1351
+ * @param {Record<string, any>|Record<string, any>[]} card A card (or array of cards) with `header.hasMediaAttachment: true`
1352
+ * — typically built via `new Button(client).setImage(...).addUrl(...).toCard()`.
1353
+ */
985
1354
  addCard(card) {
986
1355
  const cards = Array.isArray(card) ? card : [card];
987
1356
  const baseIndex = this._cards.length;
@@ -992,10 +1361,15 @@ class Carousel extends BaseBuilder {
992
1361
  }
993
1362
  }
994
1363
 
1364
+ if (this._cards.length + cards.length > Carousel.MAX_CARDS) {
1365
+ throw new Error(`Carousel supports at most ${Carousel.MAX_CARDS} cards (got ${this._cards.length + cards.length})`);
1366
+ }
1367
+
995
1368
  this._cards.push(...cards);
996
1369
  return this;
997
1370
  }
998
1371
 
1372
+ /** @returns {Record<string, any>} The generated WAMessage (without sending). */
999
1373
  build(jid, { ...options } = {}) {
1000
1374
  return generateWAMessageFromContent(
1001
1375
  jid,
@@ -1017,7 +1391,10 @@ class Carousel extends BaseBuilder {
1017
1391
  );
1018
1392
  }
1019
1393
 
1394
+ /** Build and send this carousel. @param {string} jid Destination chat/group jid. */
1020
1395
  async send(jid, { ...options } = {}) {
1396
+ if (this._cards.length === 0) throw new Error('Carousel requires at least one card (use addCard())');
1397
+
1021
1398
  const msg = this.build(jid, options);
1022
1399
 
1023
1400
  await this.#client.relayMessage(msg.key.remoteJid, msg.message, {
@@ -1041,6 +1418,150 @@ class Carousel extends BaseBuilder {
1041
1418
  }
1042
1419
  }
1043
1420
 
1421
+ /**
1422
+ * Vanz@Add (v4.8) --- Chainable poll builder, wrapping the socket's own well-tested
1423
+ * `sendMessage({ poll })` path (see messages.js) instead of hand-building
1424
+ * pollCreationMessageV3/V5 over relayMessage. Two things from the traffic you sent were
1425
+ * deliberately NOT implemented here because they can't be built with confidence:
1426
+ * 1. Per-option poll images (`values: [{ name, image }]`) — the proto this fork ships
1427
+ * only has a plain `optionName` string per option; an image-poll option isn't a named
1428
+ * field anywhere in it. The one place an image-poll concept even appears
1429
+ * (`pollCreationOptionImageMessage`) is typed as an opaque `FutureProofMessage` (a
1430
+ * forward-compat envelope with no documented inner layout) — there's no field list to
1431
+ * target, so adding "support" for it would just be silently dropping the image and
1432
+ * guessing at a shape. Flagging instead of faking it.
1433
+ * 2. Quiz-mode `correctAnswer.optionHash` built by hand — the one working example you
1434
+ * captured had a 65-character hex string where a sha256 digest should be 64, and this
1435
+ * builder's target `sendMessage({poll})` path (pollCreationMessageV5) already computes
1436
+ * quiz mode correctly from a plain `correctAnswer` string, so `setQuiz()` below defers to
1437
+ * that existing, already-tested logic rather than reimplementing the hash.
1438
+ */
1439
+ class Poll extends BaseBuilder {
1440
+ #client;
1441
+
1442
+ /** @param {import('../../WAProto/index.js').WASocket} client Active Baileys socket (must expose `sendMessage`). */
1443
+ constructor(client) {
1444
+ super();
1445
+ if (!client) throw new Error('Socket is required');
1446
+ this.#client = client;
1447
+
1448
+ this._name = '';
1449
+ this._values = [];
1450
+ this._selectableCount = 1;
1451
+ this._hideVoter = false;
1452
+ this._canAddOption = false;
1453
+ this._toAnnouncementGroup = false;
1454
+ this._correctAnswer;
1455
+ this._endDate;
1456
+ }
1457
+
1458
+ /** Set the poll question/title. */
1459
+ setName(name) {
1460
+ if (typeof name !== 'string' || !name) throw new TypeError('setName(name) requires a non-empty string');
1461
+ this._name = name;
1462
+ return this;
1463
+ }
1464
+
1465
+ /** Append one option. Chainable — call repeatedly, or use `addOptions()` for an array. */
1466
+ addOption(name) {
1467
+ if (typeof name !== 'string' || !name) throw new TypeError('addOption(name) requires a non-empty string');
1468
+ this._values.push(name);
1469
+ return this;
1470
+ }
1471
+
1472
+ /** Append several options at once. @param {string[]} names */
1473
+ addOptions(names) {
1474
+ if (!Array.isArray(names) || !names.length) throw new TypeError('addOptions(names) requires a non-empty array of strings');
1475
+ names.forEach((name) => this.addOption(name));
1476
+ return this;
1477
+ }
1478
+
1479
+ /** How many options a voter can pick (default 1). Use `setMultiSelect()` for unlimited. */
1480
+ setSelectable(count) {
1481
+ if (typeof count !== 'number' || count < 0) throw new TypeError('setSelectable(count) requires a non-negative number');
1482
+ this._selectableCount = count;
1483
+ return this;
1484
+ }
1485
+
1486
+ /** Shortcut for unlimited-choice polls (`selectableCount: 0`). Pass `false` to revert to single-select. */
1487
+ setMultiSelect(canSelectMultiple = true) {
1488
+ this._selectableCount = canSelectMultiple ? 0 : 1;
1489
+ return this;
1490
+ }
1491
+
1492
+ /** Hide voter names from other participants (where the client supports it). */
1493
+ setHideVoter(hide = true) {
1494
+ this._hideVoter = hide;
1495
+ return this;
1496
+ }
1497
+
1498
+ /** Allow voters to add their own options. */
1499
+ setCanAddOption(allow = true) {
1500
+ this._canAddOption = allow;
1501
+ return this;
1502
+ }
1503
+
1504
+ /** Mark this a community-announcement-group poll (pollCreationMessageV2 path). */
1505
+ setAnnouncementGroup(isAnnouncement = true) {
1506
+ this._toAnnouncementGroup = isAnnouncement;
1507
+ return this;
1508
+ }
1509
+
1510
+ /** Auto-close the poll at this date/time. */
1511
+ setEndDate(date) {
1512
+ this._endDate = date instanceof Date ? date : new Date(date);
1513
+ return this;
1514
+ }
1515
+
1516
+ /**
1517
+ * Turn this into a quiz: one option is marked correct. Delegates the actual hash/version
1518
+ * wiring to the socket's own `sendMessage({poll:{...correctAnswer}})` handling — see class
1519
+ * docblock for why this builder doesn't compute the hash itself.
1520
+ * @param {string} correctOptionName Must exactly match one of the strings passed to `addOption()`/`addOptions()`.
1521
+ */
1522
+ setQuiz(correctOptionName) {
1523
+ if (typeof correctOptionName !== 'string' || !correctOptionName) {
1524
+ throw new TypeError('setQuiz(correctOptionName) requires a non-empty string');
1525
+ }
1526
+ this._correctAnswer = correctOptionName;
1527
+ return this;
1528
+ }
1529
+
1530
+ /** @returns {{poll: Record<string, any>}} The `sendMessage()`-shaped poll payload, without sending it. */
1531
+ build() {
1532
+ if (!this._name) throw new Error('Poll requires a name (use setName())');
1533
+ if (this._values.length < 2) throw new Error('Poll requires at least 2 options (use addOption()/addOptions())');
1534
+ if (this._correctAnswer && !this._values.includes(this._correctAnswer)) {
1535
+ throw new Error('setQuiz(correctOptionName) must match one of the added options exactly');
1536
+ }
1537
+
1538
+ return {
1539
+ poll: {
1540
+ name: this._name,
1541
+ values: this._values,
1542
+ selectableCount: this._selectableCount,
1543
+ toAnnouncementGroup: this._toAnnouncementGroup,
1544
+ hideVoter: this._hideVoter,
1545
+ canAddOption: this._canAddOption,
1546
+ ...(this._endDate && { endDate: this._endDate }),
1547
+ ...(this._correctAnswer && { pollType: 1, correctAnswer: this._correctAnswer }),
1548
+ },
1549
+ };
1550
+ }
1551
+
1552
+ /** Build and send via the socket's `sendMessage()`. */
1553
+ async send(jid, options = {}) {
1554
+ return this.#client.sendMessage(jid, this.build(), options);
1555
+ }
1556
+ }
1557
+
1558
+ /**
1559
+ * "Rich AI-response" style message builder: text with hyperlink/citation/latex
1560
+ * inline entities, code blocks, tables, sources, image/video attachments,
1561
+ * inline product/post cards, tip banners and quick-reply suggestions —
1562
+ * everything ChatGPT/Gemini-in-WhatsApp-style bots typically render.
1563
+ * Also exported as `AIVanzxy` / `LeafRich` / `VanzxyAI` / `VanzxyRich` (identical class, alternate names).
1564
+ */
1044
1565
  class AIRich extends BaseBuilder {
1045
1566
  #client;
1046
1567
 
@@ -1062,6 +1583,7 @@ class AIRich extends BaseBuilder {
1062
1583
  this._inlineImages = [];
1063
1584
  }
1064
1585
 
1586
+ /** Push a raw pre-built submessage block (escape hatch for shapes not covered by the add*() helpers). */
1065
1587
  addSubmessage(submessage) {
1066
1588
  const items = Array.isArray(submessage) ? submessage : [submessage];
1067
1589
 
@@ -1076,6 +1598,7 @@ class AIRich extends BaseBuilder {
1076
1598
  return this;
1077
1599
  }
1078
1600
 
1601
+ /** Push a raw pre-built section wrapper around one or more submessages. */
1079
1602
  addSection(section) {
1080
1603
  const items = Array.isArray(section) ? section : [section];
1081
1604
 
@@ -1090,6 +1613,7 @@ class AIRich extends BaseBuilder {
1090
1613
  return this;
1091
1614
  }
1092
1615
 
1616
+ /** Add a text block. `[label](url)` becomes a hyperlink, `[](url)` a numbered citation, `[expr]<img-url>` a rendered latex expression — toggle each via the options. */
1093
1617
  addText(text, { hyperlink = true, citation = true, latex = true } = {}) {
1094
1618
  if (typeof text != 'string') {
1095
1619
  throw new TypeError('Text must be a string');
@@ -1119,6 +1643,7 @@ class AIRich extends BaseBuilder {
1119
1643
  return this;
1120
1644
  }
1121
1645
 
1646
+ /** Add a syntax-highlighted code block. @param {string} language e.g. 'javascript', 'python'. */
1122
1647
  addCode(language, code) {
1123
1648
  if (typeof language !== 'string' || typeof code !== 'string') {
1124
1649
  throw new TypeError('Language and code must be a string');
@@ -1145,6 +1670,7 @@ class AIRich extends BaseBuilder {
1145
1670
  return this;
1146
1671
  }
1147
1672
 
1673
+ /** Add a table. @param {string[][]} table Row-major grid, first row treated as the header. */
1148
1674
  addTable(table, { hyperlink = true, citation = true, latex = true } = {}) {
1149
1675
  if (!Array.isArray(table)) {
1150
1676
  throw new TypeError('Table must be an array');
@@ -1170,6 +1696,7 @@ class AIRich extends BaseBuilder {
1170
1696
  return this;
1171
1697
  }
1172
1698
 
1699
+ /** Add a "Sources" strip. @param {string[]|string[][]} sources Flat list of urls, or `[title, url]` pairs. */
1173
1700
  addSource(sources = []) {
1174
1701
  if (!(Array.isArray(sources) && (sources.every((item) => typeof item === 'string') || sources.every((item) => Array.isArray(item) && item.every((v) => typeof v === 'string'))))) {
1175
1702
  throw new TypeError('Sources must be a string array or an array of string arrays');
@@ -1202,6 +1729,7 @@ class AIRich extends BaseBuilder {
1202
1729
  return this;
1203
1730
  }
1204
1731
 
1732
+ /** Add a horizontally-scrollable reel of image/video items. */
1205
1733
  addReels(reelsItems = []) {
1206
1734
  if (
1207
1735
  !(
@@ -1271,6 +1799,7 @@ class AIRich extends BaseBuilder {
1271
1799
  return this;
1272
1800
  }
1273
1801
 
1802
+ /** Add a full-width image (or grid of images if `imageUrl` is an array). */
1274
1803
  addImage(imageUrl, { resolveUrl = false } = {}) {
1275
1804
  if (!(typeof imageUrl === 'string' || Buffer.isBuffer(imageUrl) || (Array.isArray(imageUrl) && imageUrl.every((v) => typeof v === 'string' || Buffer.isBuffer(v))))) {
1276
1805
  throw new TypeError('imageUrl must be string | buffer | array of string/buffer');
@@ -1329,6 +1858,7 @@ class AIRich extends BaseBuilder {
1329
1858
  // GRID_IMAGE/GenAIImaginePrimitive section schema instead of GenAIInlineImageUXPrimitive),
1330
1859
  // which broke client-side unifiedResponse rendering even though the submessage itself was fine.
1331
1860
  // Mirrors RichSubMessageType.INLINE_IMAGE handling in rich-message-utils.js's toUnified().
1861
+ /** Add an image inline with the surrounding text flow (falls back to a plain imageMessage on send() if the client can't render inline images — see skipImageFallback). */
1332
1862
  addInlineImage(imageUrl, { text = '', alignment = 'center', tapLinkUrl = '', resolveUrl = false } = {}) {
1333
1863
  if (!(typeof imageUrl === 'string' || Buffer.isBuffer(imageUrl) || (imageUrl && typeof imageUrl === 'object'))) {
1334
1864
  throw new TypeError('imageUrl must be string | buffer | { imagePreviewUrl, imageHighResUrl, sourceUrl }');
@@ -1387,6 +1917,7 @@ class AIRich extends BaseBuilder {
1387
1917
  // fetch-full-video + ffmpeg-frame-extraction + duration-parse round trip per video, which
1388
1918
  // was the main source of blurose's slower response time. Pass { autoFill: true } to opt
1389
1919
  // back into the complete/slow path (real thumbnail + duration + file_length).
1920
+ /** Add a video block. */
1390
1921
  addVideo(videoUrl, { autoFill = false } = {}) {
1391
1922
  const isObjectVideo = (v) => v && typeof v === 'object' && v.url;
1392
1923
 
@@ -1467,11 +1998,18 @@ class AIRich extends BaseBuilder {
1467
1998
  return this;
1468
1999
  }
1469
2000
 
2001
+ /** Add an inline product card (or array of cards). Each item needs at least a `title`. */
1470
2002
  addProduct(data = {}) {
1471
2003
  if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
1472
2004
  throw new TypeError('Product items must be an object or an array of objects');
1473
2005
  }
1474
2006
 
2007
+ const itemsToCheck = Array.isArray(data) ? data : [data];
2008
+ const missingTitleAt = itemsToCheck.findIndex((item) => !item.title);
2009
+ if (missingTitleAt !== -1) {
2010
+ throw new TypeError(`addProduct() item[${missingTitleAt}] is missing a required "title"`);
2011
+ }
2012
+
1475
2013
  this._submessages.push({
1476
2014
  messageType: 2,
1477
2015
  messageText: '[ Produk tidak dapat dimuat ]',
@@ -1501,6 +2039,7 @@ class AIRich extends BaseBuilder {
1501
2039
  return this;
1502
2040
  }
1503
2041
 
2042
+ /** Add an inline social-post style card (or array of cards). */
1504
2043
  addPost(data = {}) {
1505
2044
  if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
1506
2045
  throw new TypeError('Post items must be an object or an array of objects');
@@ -1540,7 +2079,12 @@ class AIRich extends BaseBuilder {
1540
2079
  return this;
1541
2080
  }
1542
2081
 
2082
+ /** Add a small "tip" callout banner. @param {string} text */
1543
2083
  addTip(text) {
2084
+ if (typeof text !== 'string' || !text) {
2085
+ throw new TypeError('addTip(text) requires a non-empty string');
2086
+ }
2087
+
1544
2088
  this._submessages.push({
1545
2089
  messageType: 2,
1546
2090
  messageText: text,
@@ -1556,6 +2100,328 @@ class AIRich extends BaseBuilder {
1556
2100
  return this;
1557
2101
  }
1558
2102
 
2103
+ // Vanz@Add 22-08-26 (v4.7) --- addHeading/addImageCard/addWidget/addFooterAction: 4 primitives
2104
+ // reverse-engineered from captured Meta-AI-in-WhatsApp traffic that this project's own crm/snip
2105
+ // tooling (see rich-message-utils.js) dumps for study. Not in any public Baileys schema, so
2106
+ // unknown enum values (kind/state on addWidget's ctas) are passed through as observed rather
2107
+ // than guessed at, and documented as experimental below.
2108
+
2109
+ /** Add a large heading-style text block (`FOATextPrimitive`) — visually distinct from `addText()`'s regular paragraph text. */
2110
+ addHeading(text) {
2111
+ if (typeof text !== 'string' || !text) {
2112
+ throw new TypeError('addHeading(text) requires a non-empty string');
2113
+ }
2114
+
2115
+ this._submessages.push({
2116
+ messageType: 2,
2117
+ messageText: text,
2118
+ });
2119
+
2120
+ this._sections.push(
2121
+ AIRich.newLayout('Single', {
2122
+ text,
2123
+ __typename: 'FOATextPrimitive',
2124
+ })
2125
+ );
2126
+
2127
+ return this;
2128
+ }
2129
+
2130
+ /**
2131
+ * Add a "ready" static image card (`GenAIImagePrimitive`: preview + full-res, no generating/status
2132
+ * state) — distinct from `addImage()`'s AI-generation-style `GenAIImaginePrimitive`.
2133
+ * @param {string|Buffer} previewUrl Preview/thumbnail image.
2134
+ * @param {string|Buffer} [fullUrl] Full-resolution image; defaults to `previewUrl`.
2135
+ */
2136
+ addImageCard(previewUrl, fullUrl = previewUrl, { resolveUrl = false } = {}) {
2137
+ if (!(typeof previewUrl === 'string' || Buffer.isBuffer(previewUrl))) {
2138
+ throw new TypeError('addImageCard(previewUrl) requires a string url or buffer');
2139
+ }
2140
+
2141
+ const preview = Toolkit.resolveMedia(this.#client, previewUrl, 'image', { resolveUrl });
2142
+ const full = fullUrl === previewUrl ? preview : Toolkit.resolveMedia(this.#client, fullUrl, 'image', { resolveUrl });
2143
+
2144
+ this._submessages.push({
2145
+ messageType: 1,
2146
+ gridImageMetadata: {
2147
+ gridImageUrl: { imagePreviewUrl: preview },
2148
+ imageUrls: [{ imagePreviewUrl: preview, imageHighResUrl: full, sourceUrl: full }],
2149
+ },
2150
+ });
2151
+
2152
+ this._sections.push(
2153
+ AIRich.newLayout('Single', {
2154
+ preview_image: { url: preview, mime_type: 'image/jpeg', __typename: 'GenAIMediaItem' },
2155
+ full_image: { url: full, mime_type: 'image/jpeg', __typename: 'GenAIMediaItem' },
2156
+ __typename: 'GenAIImagePrimitive',
2157
+ })
2158
+ );
2159
+
2160
+ return this;
2161
+ }
2162
+
2163
+ /**
2164
+ * Add a "3P extension" widget card (`GenAI3PExtWidgetPrimitive`) — a small panel with a title and
2165
+ * a row of tappable CTA chips. Per captured traffic these CTAs call back into a tool (`tool_call_id`)
2166
+ * rather than opening a url; `kind`/`state` semantics beyond the observed `'OTHER'`/`'PENDING'`
2167
+ * defaults aren't publicly documented, so treat this as experimental.
2168
+ *
2169
+ * Vanz@Add (v4.8) --- accepts an `{ layout }` override so consecutive `addWidget()` calls can
2170
+ * pick different renderings (e.g. one `HScroll` row, one `ActionRow` stack) instead of always
2171
+ * inferring HScroll-for-array/Single-for-object from the shape of `data`. Also accepts either
2172
+ * `ctas` (original key, matches the wire field) or `actions` (alias) on each item — whichever
2173
+ * is present is used; `ctas` wins if both are somehow given.
2174
+ * @param {Record<string, any>|Record<string, any>[]} data `{ title, ctas|actions: [{ label, tool_call_id?, kind?, state?, toast? }] }` (single or array).
2175
+ * @param {{layout?: 'Single'|'HScroll'|'ActionRow'|string}} [options] `layout` overrides the default single/array inference.
2176
+ */
2177
+ addWidget(data = {}, { layout } = {}) {
2178
+ const items = Array.isArray(data) ? data : [data];
2179
+
2180
+ items.forEach((item, i) => {
2181
+ if (!item?.title) {
2182
+ throw new TypeError(`addWidget() item[${i}] is missing a required "title"`);
2183
+ }
2184
+ const ctas = item.ctas ?? item.actions;
2185
+ if (!Array.isArray(ctas) || !ctas.length) {
2186
+ throw new TypeError(`addWidget() item[${i}] requires a non-empty "ctas" (or "actions") array`);
2187
+ }
2188
+ });
2189
+
2190
+ this._submessages.push({
2191
+ messageType: 2,
2192
+ messageText: items.map((item) => item.title).join(', '),
2193
+ });
2194
+
2195
+ const widgets = items.map((item) => {
2196
+ const ctas = item.ctas ?? item.actions;
2197
+ return {
2198
+ header: { title: item.title, __typename: 'GenAI3PExtWidgetStandardHeader' },
2199
+ body: {
2200
+ sections: item.sections ?? [],
2201
+ ctas: ctas.map((cta, idx) => ({
2202
+ label: cta.label ?? '',
2203
+ state: cta.state ?? 'PENDING',
2204
+ kind: cta.kind ?? 'OTHER',
2205
+ tool_call_id: cta.tool_call_id ?? String(idx).padStart(2, '0'),
2206
+ ...(cta.toast !== false && {
2207
+ toast: { label: typeof cta.toast === 'string' ? cta.toast : item.title, __typename: 'GenAI3PExtWidgetToast' },
2208
+ }),
2209
+ __typename: 'GenAI3PExtWidgetCTA',
2210
+ })),
2211
+ __typename: item.body_typename ?? 'GenAI3PExtCalendarEventList',
2212
+ },
2213
+ __typename: 'GenAI3PExtWidgetPrimitive',
2214
+ };
2215
+ });
2216
+
2217
+ const resolvedLayout = layout ?? (Array.isArray(data) ? 'HScroll' : 'Single');
2218
+ const asArray = resolvedLayout !== 'Single';
2219
+
2220
+ this._sections.push(AIRich.newLayout(resolvedLayout, asArray ? widgets : widgets[0]));
2221
+
2222
+ return this;
2223
+ }
2224
+
2225
+ /**
2226
+ * Add footer action link(s) (`GenAIFooterActionPrimitive`) — e.g. "Join our WhatsApp Group/Channel"
2227
+ * chips shown below the response, separate from `setFooter()`'s plain text footer.
2228
+ * @param {{text: string, url: string, type?: string}|{text: string, url: string, type?: string}[]} actions
2229
+ */
2230
+ addFooterAction(actions) {
2231
+ const items = Array.isArray(actions) ? actions : [actions];
2232
+
2233
+ items.forEach((item, i) => {
2234
+ if (!item?.text || !item?.url) {
2235
+ throw new TypeError(`addFooterAction() item[${i}] requires both "text" and "url"`);
2236
+ }
2237
+ });
2238
+
2239
+ const primitives = items.map((item) => ({
2240
+ cta_text: item.text,
2241
+ cta_type: item.type ?? 'OPEN_URL',
2242
+ cta_url: item.url,
2243
+ __typename: 'GenAIFooterActionPrimitive',
2244
+ }));
2245
+
2246
+ this._sections.push(AIRich.newLayout('HScroll', primitives));
2247
+
2248
+ return this;
2249
+ }
2250
+
2251
+ // Vanz@Add (v4.8) --- 8 primitives from the 20-item reference test script that had no
2252
+ // add*() helper yet (Divider/Spacer/Task/ProgressStatus/ThinkingStatus/QuotaUpsell/FOABloks
2253
+ // have no dedicated AIRichResponseSubMessageType — WA carries them purely in the
2254
+ // unifiedResponse view-model JSON, so their submessage falls back to plain AI_RICH_RESPONSE_TEXT
2255
+ // like addTip/addHeading already do. Latex is the one exception: it has a real proto type
2256
+ // (AI_RICH_RESPONSE_LATEX = 8, confirmed in WAProto) with its own latexMetadata, so that one
2257
+ // gets a proper submessage instead of the text fallback.
2258
+
2259
+ /** Add a plain horizontal divider line (`GenAIDividerPrimitive`, no content). */
2260
+ addDivider() {
2261
+ this._submessages.push({ messageType: 2, messageText: '---' });
2262
+ this._sections.push(AIRich.newLayout('Single', { __typename: 'GenAIDividerPrimitive' }));
2263
+ return this;
2264
+ }
2265
+
2266
+ /** Add blank vertical spacing (`GenAISpacerPrimitive`). @param {number} [spacing=1] Spacing unit, per observed traffic. */
2267
+ addSpacer(spacing = 1) {
2268
+ if (typeof spacing !== 'number' || spacing < 0) {
2269
+ throw new TypeError('addSpacer(spacing) requires a non-negative number');
2270
+ }
2271
+ this._submessages.push({ messageType: 2, messageText: `spasi ${spacing}` });
2272
+ this._sections.push(AIRich.newLayout('Single', { spacing, __typename: 'GenAISpacerPrimitive' }));
2273
+ return this;
2274
+ }
2275
+
2276
+ /**
2277
+ * Add a rendered LaTeX expression (`GenAILatexUXPrimitive`), with a real `AI_RICH_RESPONSE_LATEX`
2278
+ * submessage (unlike most primitives in this block, this one has a proper proto type).
2279
+ * @param {string} expression LaTeX source, e.g. `'$$E = mc^2$$'`.
2280
+ */
2281
+ addLatex(expression) {
2282
+ if (typeof expression !== 'string' || !expression) {
2283
+ throw new TypeError('addLatex(expression) requires a non-empty string');
2284
+ }
2285
+ this._submessages.push({
2286
+ messageType: 8,
2287
+ latexMetadata: { text: expression, expressions: [{ latexExpression: expression }] },
2288
+ });
2289
+ this._sections.push(AIRich.newLayout('Single', { latex_expression: expression, __typename: 'GenAILatexUXPrimitive' }));
2290
+ return this;
2291
+ }
2292
+
2293
+ /**
2294
+ * Add a task/checklist card (`GenAITaskPrimitive`).
2295
+ * @param {{task_id?: string, title: string, subtitle?: string, status?: string}} data
2296
+ */
2297
+ addTask(data = {}) {
2298
+ if (!data?.title) {
2299
+ throw new TypeError('addTask() requires a "title"');
2300
+ }
2301
+ this._submessages.push({ messageType: 2, messageText: `Tugas: ${data.title}` });
2302
+ this._sections.push(
2303
+ AIRich.newLayout('Single', {
2304
+ task_id: data.task_id ?? '',
2305
+ title: data.title,
2306
+ subtitle: data.subtitle ?? '',
2307
+ status: data.status ?? 'IN_PROGRESS',
2308
+ __typename: 'GenAITaskPrimitive',
2309
+ })
2310
+ );
2311
+ return this;
2312
+ }
2313
+
2314
+ /**
2315
+ * Add a "searching/working" progress banner (`GenAIBotProgressStatusPrimitive`) — a one-shot
2316
+ * status chip (unlike `addSuggest`, this isn't tappable). Distinct from `addThinkingStatus()`'s
2317
+ * icon/typename.
2318
+ * @param {string} title
2319
+ * @param {{icon?: string, is_in_progress?: boolean}} [options]
2320
+ */
2321
+ addProgressStatus(title, { icon = 'SEARCH', is_in_progress = true, target_secondary_screen_id, target_secondary_screen_tab_id } = {}) {
2322
+ if (typeof title !== 'string' || !title) {
2323
+ throw new TypeError('addProgressStatus(title) requires a non-empty string');
2324
+ }
2325
+ this._submessages.push({ messageType: 2, messageText: title });
2326
+ const primitive = {
2327
+ title,
2328
+ icon,
2329
+ is_in_progress,
2330
+ meta_search_apps: [],
2331
+ __typename: 'GenAIBotProgressStatusPrimitive',
2332
+ };
2333
+ // NOTE: these two fields must be OMITTED when unset, not sent as `null` —
2334
+ // an explicit null here was reproducibly crashing the WA client renderer
2335
+ // on group-open/media-download. Only include when the caller actually passes one.
2336
+ if (target_secondary_screen_id != null) primitive.target_secondary_screen_id = target_secondary_screen_id;
2337
+ if (target_secondary_screen_tab_id != null) primitive.target_secondary_screen_tab_id = target_secondary_screen_tab_id;
2338
+ this._sections.push(AIRich.newLayout('Single', primitive));
2339
+ return this;
2340
+ }
2341
+
2342
+ /** Add a "thinking" status banner (`GenAIBotThinkingStatusPrimitive`). See `addProgressStatus()`. */
2343
+ addThinkingStatus(title, { icon = 'THINKING', is_in_progress = true, target_secondary_screen_id, target_secondary_screen_tab_id, textFallback = true } = {}) {
2344
+ if (typeof title !== 'string' || !title) {
2345
+ throw new TypeError('addThinkingStatus(title) requires a non-empty string');
2346
+ }
2347
+ this._submessages.push({ messageType: 2, messageText: title });
2348
+ const primitive = {
2349
+ title,
2350
+ icon,
2351
+ is_in_progress,
2352
+ meta_search_apps: [],
2353
+ __typename: 'GenAIBotThinkingStatusPrimitive',
2354
+ };
2355
+ // Same crash-avoidance rule as addProgressStatus(): omit, never null.
2356
+ if (target_secondary_screen_id != null) primitive.target_secondary_screen_id = target_secondary_screen_id;
2357
+ if (target_secondary_screen_tab_id != null) primitive.target_secondary_screen_tab_id = target_secondary_screen_tab_id;
2358
+ this._sections.push(AIRich.newLayout('Single', primitive));
2359
+ // Safety net: stock WA client doesn't render this primitive's own view (it's meant
2360
+ // as a transient spinner in the official app), so the card shows blank when forwarded.
2361
+ // Append a plain text section so the title is still visible. Set { textFallback: false } to skip.
2362
+ if (textFallback) {
2363
+ this._sections.push(AIRich.newLayout('Single', { text: title, __typename: 'FOATextPrimitive' }));
2364
+ }
2365
+ return this;
2366
+ }
2367
+
2368
+ /**
2369
+ * Add a subscription-quota-limit upsell card (`GenAIMetaSubsQuotaUpsellPrimitive`).
2370
+ * @param {{title: string, body?: string, body_line1?: string, body_line2?: string, buttons?: {label: string, action?: string, deeplink?: string}[]}} data
2371
+ */
2372
+ addQuotaUpsell(data = {}) {
2373
+ if (!data?.title) {
2374
+ throw new TypeError('addQuotaUpsell() requires a "title"');
2375
+ }
2376
+ this._submessages.push({ messageType: 2, messageText: data.title });
2377
+ this._sections.push(
2378
+ AIRich.newLayout('Single', {
2379
+ title: data.title,
2380
+ body: data.body ?? '',
2381
+ body_line1: data.body_line1 ?? '',
2382
+ body_line2: data.body_line2 ?? '',
2383
+ buttons: (data.buttons ?? []).map((b) => ({
2384
+ label: b.label ?? '',
2385
+ action: b.action ?? 'OPEN_DEEPLINK',
2386
+ deeplink: b.deeplink ?? '',
2387
+ })),
2388
+ __typename: 'GenAIMetaSubsQuotaUpsellPrimitive',
2389
+ })
2390
+ );
2391
+ return this;
2392
+ }
2393
+
2394
+ /**
2395
+ * Add a raw Bloks payload (`FOABloksPrimitive`) — Meta's internal UI-description format.
2396
+ * Escape hatch: field meaning beyond what's passed through is undocumented, so this is the
2397
+ * most experimental primitive in this block; pass whatever your captured traffic shows.
2398
+ * @param {{type: string, data: string, uuid?: string, initial_response?: any, versioning_id?: string}} data
2399
+ */
2400
+ addBloks(data = {}) {
2401
+ if (!data?.type) {
2402
+ throw new TypeError('addBloks() requires a "type"');
2403
+ }
2404
+ this._submessages.push({ messageType: 2, messageText: 'Bloks' });
2405
+ const primitive = {
2406
+ type: data.type,
2407
+ data: data.data ?? '{}',
2408
+ uuid: data.uuid ?? '',
2409
+ versioning_id: data.versioning_id ?? '',
2410
+ __typename: 'FOABloksPrimitive',
2411
+ };
2412
+ // Omit initial_response entirely when unset — same null-field crash as addProgressStatus/addThinkingStatus.
2413
+ if (data.initial_response != null) primitive.initial_response = data.initial_response;
2414
+ this._sections.push(AIRich.newLayout('Single', primitive));
2415
+ // Safety net: FOABloksPrimitive needs a real, client-registered Bloks screen to render
2416
+ // anything — arbitrary/placeholder payloads show up blank. Append a plain text section
2417
+ // so the card isn't empty. Set data.textFallback = false to skip.
2418
+ if (data.textFallback !== false) {
2419
+ this._sections.push(AIRich.newLayout('Single', { text: `Bloks: ${data.type}`, __typename: 'FOATextPrimitive' }));
2420
+ }
2421
+ return this;
2422
+ }
2423
+
2424
+ /** Add tappable follow-up suggestion chips below the message. @param {string|string[]} suggestion */
1559
2425
  addSuggest(suggestion, { scroll = true, layout } = {}) {
1560
2426
  if (!(typeof suggestion === 'string' || (Array.isArray(suggestion) && suggestion.every((v) => typeof v === 'string')))) {
1561
2427
  throw new TypeError('Suggestion must be a string or array of strings');
@@ -1582,6 +2448,7 @@ class AIRich extends BaseBuilder {
1582
2448
  return this;
1583
2449
  }
1584
2450
 
2451
+ /** @returns {Promise<Record<string, any>>} The generated AI-rich message content (without wrapping/sending it). */
1585
2452
  async build({ forwarded = true, notification = false, includesUnifiedResponse = true, includesSubmessages = true, quoted, quotedParticipant, ...options } = {}) {
1586
2453
  const forward = forwarded
1587
2454
  ? {
@@ -1668,10 +2535,6 @@ class AIRich extends BaseBuilder {
1668
2535
  };
1669
2536
  }
1670
2537
 
1671
- // Vanz@Fix (bug 42 / inline image fallback) --- WA won't render AIRichResponseInlineImageMetadata
1672
- // for bot-sent messages (confirmed: even a valid WA-CDN url with mediaKey stays blank), so any
1673
- // image added via addInlineImage() is sent here as a normal imageMessage instead. Pass
1674
- // { skipImageFallback: true } to opt out and send only the (image-less-looking) rich card.
1675
2538
  // Vanz@Fix (bug 42 / inline image fallback) --- WA won't render AIRichResponseInlineImageMetadata
1676
2539
  // for bot-sent messages (confirmed: even a valid WA-CDN url with mediaKey stays blank), so any
1677
2540
  // image added via addInlineImage() is sent here as a normal imageMessage instead. Pass
@@ -1679,6 +2542,7 @@ class AIRich extends BaseBuilder {
1679
2542
  // Vanz@Fix: don't spread relayMessage-shaped `options` into sendMessage()'s options param —
1680
2543
  // the two calls expect different option shapes, so the fallback now only forwards `quoted`
1681
2544
  // (the one option that clearly applies to both) instead of blindly spreading everything.
2545
+ /** Build and send this AI-rich message. @param {string} jid Destination chat/group jid. @param {boolean} [skipImageFallback] Skip auto-resending inline images as a plain imageMessage. */
1682
2546
  async send(jid, { forwarded, notification, includesUnifiedResponse, includesSubmessages, skipImageFallback = false, quoted, ...options } = {}) {
1683
2547
  const msg = await this.build({ forwarded, notification, includesUnifiedResponse, includesSubmessages, quoted, ...options });
1684
2548
 
@@ -1696,6 +2560,7 @@ class AIRich extends BaseBuilder {
1696
2560
  return await this.#client.relayMessage(jid, msg, { ...options });
1697
2561
  }
1698
2562
 
2563
+ /** Tokenize `code` into `{ type, value }` spans for syntax highlighting. Covers JS/TS/Python/Java and more; unsupported languages fall back to a single plain-text token. */
1699
2564
  static tokenizer(code, lang = 'javascript') {
1700
2565
  const keywordsMap = {
1701
2566
  javascript: new Set([
@@ -2361,6 +3226,7 @@ class AIRich extends BaseBuilder {
2361
3226
  };
2362
3227
  }
2363
3228
 
3229
+ /** Convert a raw `string[][]` grid into the table metadata shape addTable()/addText() produce internally. */
2364
3230
  static toTableMetadata(arr, { hyperlink = true, citation = true, latex = true } = {}) {
2365
3231
  if (!Array.isArray(arr) || !arr.every((row) => Array.isArray(row) && row.every((cell) => typeof cell === 'string'))) {
2366
3232
  throw new TypeError('Table must be a nested array of strings');
@@ -2409,6 +3275,7 @@ class AIRich extends BaseBuilder {
2409
3275
  };
2410
3276
  }
2411
3277
 
3278
+ /** Build a raw submessage layout block by name — escape hatch for layouts not covered by the add*() helpers. */
2412
3279
  static newLayout(name, data, extra = {}) {
2413
3280
  return {
2414
3281
  ...extra,
@@ -2420,4 +3287,21 @@ class AIRich extends BaseBuilder {
2420
3287
  }
2421
3288
  }
2422
3289
 
2423
- export { MESSAGE_BUILDER_VERSION, Button, ButtonV2, Carousel, AIRich, Toolkit };
3290
+ // Vanz@Alias --- AIRich diekspos ulang pake nama sendiri. Implementasi & referensi
3291
+ // internal (AIRich.newLayout/tokenizer/toTableMetadata) TETAP pake nama class asli
3292
+ // biar nggak perlu rewrite ratusan pemanggilan; ini cuma nge-alias binding exportnya.
3293
+ // Attribution buat base implementation tetep di NOTICE.md, gak kehapus cuma gara-gara
3294
+ // alias nama di sini.
3295
+ export {
3296
+ MESSAGE_BUILDER_VERSION,
3297
+ Button,
3298
+ ButtonV2,
3299
+ Carousel,
3300
+ Poll,
3301
+ AIRich,
3302
+ AIRich as AIVanzxy,
3303
+ AIRich as LeafRich,
3304
+ AIRich as VanzxyAI,
3305
+ AIRich as VanzxyRich,
3306
+ Toolkit,
3307
+ };