@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.
- package/cjs/lib/Socket/index.js +11 -0
- package/cjs/lib/Utils/MessageBuilder.js +472 -23
- package/cjs/lib/Utils/chat-utils.js +4 -1
- package/cjs/lib/Utils/crypto.js +7 -1
- package/cjs/lib/Utils/lt-hash.js +3 -1
- package/cjs/lib/Utils/messages.js +5 -0
- package/lib/Socket/index.js +17 -0
- package/lib/Utils/MessageBuilder.d.ts +202 -0
- package/lib/Utils/MessageBuilder.js +909 -25
- package/lib/Utils/messages.js +7 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
917
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|