@vanzxy/baileys 1.6.2 → 1.6.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.
Files changed (51) hide show
  1. package/NOTICE.md +50 -0
  2. package/lib/Utils/A2UI.js +217 -0
  3. package/lib/Utils/MessageBuilder.js +332 -46
  4. package/lib/Utils/MessageBuilder_d.ts +45 -0
  5. package/lib/Utils/PersistentStore.js +592 -0
  6. package/lib/Utils/PersistentStore_d.ts +60 -0
  7. package/lib/Utils/anti-delete.d.ts +68 -0
  8. package/lib/Utils/anti-delete.js +185 -0
  9. package/lib/Utils/auto-reply.d.ts +47 -0
  10. package/lib/Utils/auto-reply.js +155 -0
  11. package/lib/Utils/button-helper-utils.js +314 -0
  12. package/lib/Utils/button-sender.js +817 -0
  13. package/lib/Utils/chat-history-helpers.d.ts +21 -0
  14. package/lib/Utils/chat-history-helpers.js +71 -0
  15. package/lib/Utils/index.d.ts +11 -0
  16. package/lib/Utils/index.js +16 -0
  17. package/lib/Utils/media-messages.d.ts +18 -0
  18. package/lib/Utils/media-messages.js +71 -0
  19. package/lib/Utils/media-set.d.ts +13 -0
  20. package/lib/Utils/media-set.js +165 -0
  21. package/lib/Utils/message-kind.js +139 -0
  22. package/lib/Utils/message-search.d.ts +44 -0
  23. package/lib/Utils/message-search.js +174 -0
  24. package/lib/Utils/scheduling.d.ts +42 -0
  25. package/lib/Utils/scheduling.js +140 -0
  26. package/lib/Utils/status.d.ts +50 -0
  27. package/lib/Utils/status.js +108 -0
  28. package/lib/Utils/stickerpack.d.ts +51 -0
  29. package/lib/Utils/stickerpack.js +276 -0
  30. package/lib/Utils/templates.d.ts +76 -0
  31. package/lib/Utils/templates.js +151 -0
  32. package/lib/Utils/use-sqlite-auth-state.js +28 -1
  33. package/lib/Utils/vcard.d.ts +58 -0
  34. package/lib/Utils/vcard.js +94 -0
  35. package/lib/VoIP/audio-feeder.d.ts +15 -0
  36. package/lib/VoIP/audio-feeder.js +132 -0
  37. package/lib/VoIP/index.js +277 -0
  38. package/lib/VoIP/relay-transport.d.ts +43 -0
  39. package/lib/VoIP/relay-transport.js +559 -0
  40. package/lib/VoIP/signaling.js +624 -0
  41. package/lib/VoIP/types.d.ts +69 -0
  42. package/lib/VoIP/types.js +17 -0
  43. package/lib/VoIP/wasm-engine.d.ts +103 -0
  44. package/lib/VoIP/wasm-engine.js +1214 -0
  45. package/lib/VoIP/worker-bootstrap.js +1042 -0
  46. package/lib/WABinary/generic-utils.js +8 -0
  47. package/lib/assets/wasm/loader.js +5 -0
  48. package/lib/assets/wasm/whatsapp.wasm +0 -0
  49. package/lib/assets/wasm/worker-modules.js +273 -0
  50. package/lib/index.js +4 -0
  51. package/package.json +22 -1
@@ -44,7 +44,7 @@
44
44
 
45
45
  'use strict';
46
46
 
47
- const MESSAGE_BUILDER_VERSION = '4.9.1';
47
+ const MESSAGE_BUILDER_VERSION = '4.8';
48
48
  // Vanz@Fix 24-08-26: use the exported builder version everywhere; the old
49
49
  // verification helper referenced an undeclared `VERSION`, making every AIRich.build() fail.
50
50
 
@@ -52,6 +52,15 @@ const MESSAGE_BUILDER_VERSION = '4.9.1';
52
52
  import { generateWAMessageFromContent, prepareWAMessageMedia } from './messages.js';
53
53
  import { generateMessageIDV2 } from './generics.js';
54
54
  import { botMetadataSignature, botMetadataCertificate } from './rich-message-utils.js';
55
+ // Vanz@Fix 27-08-26: Button.send() was hand-rolling its own <biz> node with
56
+ // attrs: {} instead of reusing the canonical builder. Real client traffic
57
+ // (and the auto-attached biz node in Socket/messages-send.js) always carries
58
+ // actual_actors/host_storage/privacy_mode_ts on every <biz> node, including
59
+ // the one wrapping a lone single_select's <list> node. Missing them made the
60
+ // single_select wire payload diverge from what messages-send.js emits for
61
+ // every other message type -- import the shared helper instead of duplicating
62
+ // (and silently drifting from) its attrs.
63
+ import { getBizBinaryNode } from '../WABinary/index.js';
55
64
  import crypto from 'crypto';
56
65
  import { PassThrough, Readable } from 'stream';
57
66
  // Vanz@Fix 15-08-26 --- sharp/fluent-ffmpeg were statically imported in the blurose source.
@@ -628,6 +637,46 @@ class BaseBuilder {
628
637
  }
629
638
  }
630
639
 
640
+ /** Tiny fluent helper for building a single quickReply button row (type 1). Standalone — not tied to Button/ButtonV2. */
641
+ class RowBuilder {
642
+ constructor() {
643
+ this.buttons = [];
644
+ }
645
+
646
+ button(displayText, buttonId) {
647
+ this.buttons.push({ buttonId, buttonText: { displayText }, type: 1 });
648
+ return this;
649
+ }
650
+ }
651
+
652
+ /** Thin fluent wrapper around `Button` for building a single image+title+text+reply "card" in one chain. */
653
+ class CardBuilder {
654
+ /** @param {import('../../WAProto/index.js').WASocket} client Active Baileys socket. */
655
+ constructor(client) {
656
+ this._card = new Button(client);
657
+ }
658
+
659
+ image(url) {
660
+ this._card.setImage(url);
661
+ return this;
662
+ }
663
+
664
+ title(t) {
665
+ this._card.setTitle(t);
666
+ return this;
667
+ }
668
+
669
+ text(t) {
670
+ this._card.setBody(t);
671
+ return this;
672
+ }
673
+
674
+ button(displayText, id) {
675
+ this._card.addReply(displayText, id);
676
+ return this;
677
+ }
678
+ }
679
+
631
680
  /**
632
681
  * Interactive (native-flow) message builder — header/body/footer + a mix of
633
682
  * buttons (quick_reply, cta_url, cta_call, single_select, ...). Sends via
@@ -704,19 +753,44 @@ class Button extends BaseBuilder {
704
753
  // error-prone (dangling ids, ordering), so this accepts a plain nested tree instead —
705
754
  // { component, ...props, children: [...] } / { component, ...props, child: {...} } — and
706
755
  // flattens it into that array itself, auto-assigning ids.
707
- #flattenBloks(tree, out, counter = { n: 0 }, id = 'root') {
756
+ //
757
+ // Vanz@Fix 26-08-26 (v2) --- Modal is a different shape: `trigger`/`content` point to a
758
+ // SIBLING node by id (e.g. the Button that opens it), not a fresh child — captured traffic
759
+ // confirmed `{ trigger: "<id of an existing Button node>", content: "<id of its body>" }`.
760
+ // Nesting it as a plain `child`/`children` would duplicate the trigger node. So:
761
+ // - any prop whose value is `{ component: ..., ... }` is now auto-flattened as a nested
762
+ // node too (not just `child`/`children`) — covers Modal.content directly.
763
+ // - a node can carry a builder-only `ref: 'name'` tag; another prop can then point back
764
+ // at it with `{ $ref: 'name' }`, resolved to that node's real id after the whole tree
765
+ // is walked (order-independent). `ref` itself is stripped and never reaches the wire.
766
+ #flattenBloks(tree, out, ctx = { n: 0, refs: new Map(), pending: [] }, id = 'root') {
708
767
  if (!tree || typeof tree !== 'object') throw new TypeError('setBloksWidget: every node needs a "component" type');
709
- const { component, children, child, ...props } = tree;
768
+ const { component, children, child, ref, ...rest } = tree;
710
769
  if (typeof component !== 'string' || !component) throw new TypeError('setBloksWidget: every node needs a "component" type');
711
770
 
712
- const node = { id, component, ...props };
771
+ const isTreeNode = (v) => v && typeof v === 'object' && !Array.isArray(v) && typeof v.component === 'string';
772
+ const isRefMarker = (v) => v && typeof v === 'object' && !Array.isArray(v) && typeof v.$ref === 'string' && Object.keys(v).length === 1;
773
+
774
+ const node = { id, component };
775
+ for (const [k, v] of Object.entries(rest)) {
776
+ if (isRefMarker(v)) {
777
+ node[k] = null; // resolved once the full tree (and its `ref` tags) has been walked
778
+ ctx.pending.push({ node, key: k, refName: v.$ref });
779
+ } else if (isTreeNode(v)) {
780
+ node[k] = this.#flattenBloks(v, out, ctx, `n${ctx.n++}`);
781
+ } else {
782
+ node[k] = v;
783
+ }
784
+ }
713
785
 
714
786
  if (Array.isArray(children)) {
715
- node.children = children.map((c) => this.#flattenBloks(c, out, counter, `n${counter.n++}`));
787
+ node.children = children.map((c) => this.#flattenBloks(c, out, ctx, `n${ctx.n++}`));
716
788
  } else if (child) {
717
- node.child = this.#flattenBloks(child, out, counter, `n${counter.n++}`);
789
+ node.child = this.#flattenBloks(child, out, ctx, `n${ctx.n++}`);
718
790
  }
719
791
 
792
+ if (ref) ctx.refs.set(ref, id);
793
+
720
794
  out.push(node);
721
795
  return id;
722
796
  }
@@ -725,12 +799,23 @@ class Button extends BaseBuilder {
725
799
  * Set a Bloks/A2UI native widget (`bloksWidget`, `type: "im_a2ui"`) — a real interactive
726
800
  * screen (images, video, checkboxes, text fields, buttons that fire an `action`), not a
727
801
  * static card. Pass a nested tree; ids are assigned automatically.
802
+ *
803
+ * For components that reference a SIBLING node instead of nesting one — currently just
804
+ * `Modal.trigger` — tag the source node with `ref: 'someName'` and point at it with
805
+ * `{ $ref: 'someName' }`. Everything else (including `Modal.content`) can just be nested
806
+ * directly, no special key needed.
728
807
  * @param {Record<string, any>} tree Root node, e.g. `{ component: 'Column', children: [...] }`.
729
808
  * @param {{uuid?: string, catalogId?: string, surfaceId?: string, version?: string}} [options]
730
809
  */
731
810
  setBloksWidget(tree, { uuid = crypto.randomUUID(), catalogId = 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json', surfaceId, version = 'v0.9' } = {}) {
732
811
  const components = [];
733
- this.#flattenBloks(tree, components);
812
+ const ctx = { n: 0, refs: new Map(), pending: [] };
813
+ this.#flattenBloks(tree, components, ctx);
814
+ for (const { node, key, refName } of ctx.pending) {
815
+ const resolved = ctx.refs.get(refName);
816
+ if (!resolved) throw new Error(`setBloksWidget: ref "${refName}" (used on "${key}") was never declared with ref: "${refName}" on any node`);
817
+ node[key] = resolved;
818
+ }
734
819
 
735
820
  this._bloksWidget = {
736
821
  uuid,
@@ -1165,13 +1250,23 @@ class Button extends BaseBuilder {
1165
1250
  return this;
1166
1251
  }
1167
1252
 
1168
- /** WhatsApp Flows `flow_action` shortcut. Requires a real registered Flow. */
1253
+ /** WhatsApp Flows shortcut. Requires a real registered Flow. */
1169
1254
  addFlow(flow = {}, display_text = '') {
1170
1255
  if (typeof flow !== 'object' || flow === null || Array.isArray(flow) || !flow.id) throw new TypeError('addFlow(flow) requires a plain object with flow.id');
1171
1256
  this._buttons.push({
1172
- name: 'flow_action',
1257
+ // Vanz@Fix 27-08-26: the button name was 'flow_action', which is actually
1258
+ // the *field name* inside buttonParamsJson (flow_action: 'navigate' | 'data_exchange'),
1259
+ // not a native_flow name WhatsApp recognises. The real native_flow name for
1260
+ // launching a registered WhatsApp Flow is 'flow' -- client silently ignored
1261
+ // the button because <native_flow name='flow_action'> isn't a thing it renders.
1262
+ name: 'flow',
1173
1263
  buttonParamsJson: JSON.stringify({
1174
1264
  flow_message_version: flow.version || '3',
1265
+ // flow_token: unique per-send session token WhatsApp Flows requires to
1266
+ // correlate a flow_action data-exchange callback with this specific
1267
+ // message. Was missing entirely -- without it the client can accept the
1268
+ // message but has nothing to key the flow session on.
1269
+ flow_token: flow.token || generateMessageIDV2(),
1175
1270
  flow_id: flow.id,
1176
1271
  flow_cta: display_text || flow.cta || 'Continue',
1177
1272
  flow_action: flow.action || 'navigate',
@@ -1369,50 +1464,29 @@ class Button extends BaseBuilder {
1369
1464
  );
1370
1465
  }
1371
1466
 
1372
- // Vanz@Add 22-08-26 (v4.7) --- picks the native_flow node variant for the first
1373
- // button's name, per Button.#SPECIAL_FLOW. Falls back to the generic mixed node
1374
- // (previous unconditional behaviour) for anything not in that map.
1375
- #buildNativeFlowNode() {
1376
- const special = Button.#SPECIAL_FLOW[this._buttons[0]?.name];
1377
- return special ? { tag: 'native_flow', attrs: special } : { tag: 'native_flow', attrs: { v: '9', name: 'mixed' } };
1378
- }
1379
-
1380
1467
  /** Build and send this interactive message. @param {string} jid Destination chat/group jid. */
1381
1468
  async send(jid, { ...options } = {}) {
1382
1469
  const msg = await this.build(jid, options);
1383
1470
 
1384
- const bizContent = this.#isLoneSingleSelect()
1385
- ? [{ tag: 'list', attrs: { v: '2', type: 'product_list' } }]
1386
- : [
1387
- ...(this._buttons.length > 0
1388
- ? [
1389
- {
1390
- tag: 'interactive',
1391
- attrs: { type: 'native_flow', v: '1' },
1392
- content: [this.#buildNativeFlowNode()],
1393
- },
1394
- ]
1395
- : []),
1396
- ...(this._bloksWidget
1397
- ? [
1398
- {
1399
- tag: 'quality_control',
1400
- attrs: { decision_id: crypto.randomUUID().replace(/-/g, ''), source_type: 'third_party' },
1401
- content: [{ tag: 'decision_source', attrs: { value: 'df' } }],
1402
- },
1403
- ]
1404
- : []),
1405
- ];
1471
+ // Vanz@Fix 27-08-26: delegate the <biz> node to the shared getBizBinaryNode()
1472
+ // helper instead of hand-rolling one here. It already:
1473
+ // - stamps actual_actors/host_storage/privacy_mode_ts (previously missing,
1474
+ // the likely cause of single_select's wire being flagged for validation)
1475
+ // - picks the correct wrapper per button name (FLOWS_MAP dedicated node,
1476
+ // ORDER_RESPONSE_ALIAS native_flow_name, mixed native_flow, or the
1477
+ // <list v='2' type='product_list'> node for a lone single_select via
1478
+ // message.listMessage) using the exact same table messages-send.js uses
1479
+ // for every non-Button send path, so Button.send() can't silently drift
1480
+ // out of sync with it.
1481
+ // Button.#SPECIAL_FLOW is kept only as documentation for addFlow()/etc.
1482
+ // JSDoc now (see below); getBizBinaryNode()'s own ORDER_RESPONSE_ALIAS /
1483
+ // FLOWS_MAP tables are what actually pick the wire node -- keep those two
1484
+ // tables in sync if a new special-cased button name is ever added.
1485
+ const bizNode = getBizBinaryNode(msg.message);
1406
1486
 
1407
1487
  await this.#client.relayMessage(msg.key.remoteJid, msg.message, {
1408
1488
  messageId: msg.key.id,
1409
- additionalNodes: [
1410
- {
1411
- tag: 'biz',
1412
- attrs: {},
1413
- content: bizContent,
1414
- },
1415
- ],
1489
+ additionalNodes: [bizNode],
1416
1490
  ...options,
1417
1491
  });
1418
1492
  return msg;
@@ -1478,6 +1552,24 @@ class ButtonV2 extends BaseBuilder {
1478
1552
  return this;
1479
1553
  }
1480
1554
 
1555
+ /** Alias for addButton() — shorthand parity with RowBuilder#button(). */
1556
+ button(displayText, buttonId) {
1557
+ return this.addButton(displayText, buttonId);
1558
+ }
1559
+
1560
+ /**
1561
+ * Vanz@Add 29-08-26 --- Fluent row helper ported from the RowBuilder class (already
1562
+ * present but previously unwired into ButtonV2). Lets callers group buttons via a
1563
+ * callback instead of chaining addButton() calls one at a time.
1564
+ * @param {(row: RowBuilder) => void} cb
1565
+ */
1566
+ row(cb) {
1567
+ const r = new RowBuilder();
1568
+ cb(r);
1569
+ r.buttons.forEach((b) => this._buttons.push(b));
1570
+ return this;
1571
+ }
1572
+
1481
1573
  // Vanz@Fix 22-08-26 (v4.7) --- _thumbnail was computed unconditionally (fetch + resize) even
1482
1574
  // when setMedia() is used, in which case the location-fallback header (the only place
1483
1575
  // _thumbnail is used) never runs at all — wasted network/CPU work on every build() call.
@@ -1542,6 +1634,193 @@ class ButtonV2 extends BaseBuilder {
1542
1634
  }
1543
1635
  }
1544
1636
 
1637
+ /**
1638
+ * Legacy `templateMessage` / `hydratedFourRowTemplate` builder — WA's Generation-1
1639
+ * button protocol (predates the nativeFlow format that Button/ButtonV2 use).
1640
+ * Capped at 3 buttons (quickReply/url/call only), no interactive list/flow support.
1641
+ * Ported from MessageBuilderV4.7.
1642
+ */
1643
+ class ButtonV3 extends BaseBuilder {
1644
+ #client;
1645
+
1646
+ /** @param {import('../../WAProto/index.js').WASocket} client Active Baileys socket. */
1647
+ constructor(client) {
1648
+ super();
1649
+ if (!client) {
1650
+ throw new Error('Socket is required');
1651
+ }
1652
+
1653
+ this.#client = client;
1654
+ this._data;
1655
+ this._mediaHeaderType = null;
1656
+ this._buttons = [];
1657
+ }
1658
+
1659
+ /** Load an existing templateMessage (e.g. from a fetched/quoted message) for editing. */
1660
+ loadFrom(msg) {
1661
+ if (!msg) throw new Error('templateMessage needed');
1662
+ if (!msg.templateMessage) throw new Error('templateMessage not found');
1663
+
1664
+ const { templateMessage, ...extraPayload } = msg;
1665
+ const hft = templateMessage.hydratedFourRowTemplate || {};
1666
+
1667
+ this._title = hft.hydratedTitleText || '';
1668
+ this._body = hft.hydratedContentText || '';
1669
+ this._footer = hft.hydratedFooterText || '';
1670
+ this._contextInfo = templateMessage.contextInfo || {};
1671
+ this._extraPayload = extraPayload;
1672
+
1673
+ this._buttons = Array.isArray(hft.hydratedButtons)
1674
+ ? hft.hydratedButtons.map((button) => ({ ...button }))
1675
+ : [];
1676
+
1677
+ if (hft.imageMessage) {
1678
+ this._data = { imageMessage: hft.imageMessage };
1679
+ this._mediaHeaderType = 'imageMessage';
1680
+ } else if (hft.videoMessage) {
1681
+ this._data = { videoMessage: hft.videoMessage };
1682
+ this._mediaHeaderType = 'videoMessage';
1683
+ } else if (hft.documentMessage) {
1684
+ this._data = { documentMessage: hft.documentMessage };
1685
+ this._mediaHeaderType = 'documentMessage';
1686
+ } else if (hft.locationMessage) {
1687
+ this._data = { locationMessage: hft.locationMessage };
1688
+ this._mediaHeaderType = 'locationMessage';
1689
+ } else {
1690
+ this._data = undefined;
1691
+ this._mediaHeaderType = null;
1692
+ }
1693
+
1694
+ return this;
1695
+ }
1696
+
1697
+ setImage(path, options = {}) {
1698
+ if (!path) throw new Error('Url or buffer needed');
1699
+ this._data = Buffer.isBuffer(path)
1700
+ ? { image: path, ...options }
1701
+ : { image: { url: path }, ...options };
1702
+ this._mediaHeaderType = 'imageMessage';
1703
+ return this;
1704
+ }
1705
+
1706
+ setVideo(path, options = {}) {
1707
+ if (!path) throw new Error('Url or buffer needed');
1708
+ this._data = Buffer.isBuffer(path)
1709
+ ? { video: path, ...options }
1710
+ : { video: { url: path }, ...options };
1711
+ this._mediaHeaderType = 'videoMessage';
1712
+ return this;
1713
+ }
1714
+
1715
+ setDocument(path, options = {}) {
1716
+ if (!path) throw new Error('Url or buffer needed');
1717
+ this._data = Buffer.isBuffer(path)
1718
+ ? { document: path, ...options }
1719
+ : { document: { url: path }, ...options };
1720
+ this._mediaHeaderType = 'documentMessage';
1721
+ return this;
1722
+ }
1723
+
1724
+ setMedia(obj) {
1725
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
1726
+ throw new TypeError('Media must be a plain object');
1727
+ }
1728
+ this._data = obj;
1729
+ this._mediaHeaderType = null; // caller is expected to pass an already-resolved shape
1730
+ return this;
1731
+ }
1732
+
1733
+ clearButtons() {
1734
+ this._buttons = [];
1735
+ return this;
1736
+ }
1737
+
1738
+ addButton(hydratedButton) {
1739
+ if (this._buttons.length >= 3) {
1740
+ throw new Error('ButtonV3 (TemplateMessage) supports a maximum of 3 buttons');
1741
+ }
1742
+ this._buttons.push({ index: this._buttons.length + 1, ...hydratedButton });
1743
+ return this;
1744
+ }
1745
+
1746
+ addReply(display_text = '', id = '') {
1747
+ return this.addButton({
1748
+ quickReplyButton: { displayText: display_text, id },
1749
+ });
1750
+ }
1751
+
1752
+ addUrl(display_text = '', url = '', options = {}) {
1753
+ return this.addButton({
1754
+ urlButton: { displayText: display_text, url, ...options },
1755
+ });
1756
+ }
1757
+
1758
+ addCall(display_text = '', phone_number = '') {
1759
+ return this.addButton({
1760
+ callButton: { displayText: display_text, phoneNumber: phone_number },
1761
+ });
1762
+ }
1763
+
1764
+ async toTemplate() {
1765
+ let mediaFields = {};
1766
+
1767
+ if (this._data) {
1768
+ const alreadyResolved =
1769
+ this._data.imageMessage || this._data.videoMessage ||
1770
+ this._data.documentMessage || this._data.locationMessage;
1771
+
1772
+ mediaFields = alreadyResolved
1773
+ ? this._data
1774
+ : await prepareWAMessageMedia(this._data, {
1775
+ upload: this.#client.waUploadToServer,
1776
+ }).catch((e) => {
1777
+ if (String(e).includes('Invalid media type')) return this._data;
1778
+ throw e;
1779
+ });
1780
+ } else if (this._title) {
1781
+ mediaFields = { hydratedTitleText: this._title };
1782
+ }
1783
+
1784
+ return {
1785
+ hydratedContentText: this._body,
1786
+ hydratedFooterText: this._footer,
1787
+ hydratedButtons: this._buttons,
1788
+ ...mediaFields,
1789
+ };
1790
+ }
1791
+
1792
+ async build(jid, { messageId, ...options } = {}) {
1793
+ const hydratedFourRowTemplate = await this.toTemplate();
1794
+
1795
+ return generateWAMessageFromContent(
1796
+ jid,
1797
+ {
1798
+ ...this._extraPayload,
1799
+ templateMessage: {
1800
+ hydratedFourRowTemplate,
1801
+ contextInfo: this._contextInfo,
1802
+ },
1803
+ },
1804
+ { messageId: messageId || generateMessageIDV2(), ...options },
1805
+ );
1806
+ }
1807
+
1808
+ async send(jid, { messageId, additionalNodes = [], ...options } = {}) {
1809
+ if (this._buttons.length < 1)
1810
+ throw new Error('ButtonV3 requires at least one button');
1811
+ const msg = await this.build(jid, { messageId, ...options });
1812
+
1813
+ // TemplateMessage predates the nativeFlow protocol and does not need the
1814
+ // "biz"/"native_flow" additionalNodes hack that Button/ButtonV2 use.
1815
+ await this.#client.relayMessage(msg.key.remoteJid, msg.message, {
1816
+ messageId: msg.key.id,
1817
+ additionalNodes,
1818
+ ...options,
1819
+ });
1820
+ return msg;
1821
+ }
1822
+ }
1823
+
1545
1824
  /** Carousel of interactive cards (each with its own header media + optional buttons), scrollable horizontally in-chat. */
1546
1825
  class Carousel extends BaseBuilder {
1547
1826
  #client;
@@ -4018,6 +4297,9 @@ class AIRich extends BaseBuilder {
4018
4297
  }
4019
4298
  }
4020
4299
 
4300
+ /** Thin no-op subclass of `AIRich` — kept for drop-in compatibility with code ported from ourin-baileys that references `ORich` by name. */
4301
+ class ORich extends AIRich {}
4302
+
4021
4303
  // Vanz@Alias --- AIRich diekspos ulang pake nama sendiri. Implementasi & referensi
4022
4304
  // internal (AIRich.newLayout/tokenizer/toTableMetadata) TETAP pake nama class asli
4023
4305
  // biar nggak perlu rewrite ratusan pemanggilan; ini cuma nge-alias binding exportnya.
@@ -4027,9 +4309,13 @@ export {
4027
4309
  MESSAGE_BUILDER_VERSION,
4028
4310
  Button,
4029
4311
  ButtonV2,
4312
+ ButtonV3,
4313
+ RowBuilder,
4314
+ CardBuilder,
4030
4315
  Carousel,
4031
4316
  Poll,
4032
4317
  AIRich,
4318
+ ORich,
4033
4319
  AIRich as AIVanzxy,
4034
4320
  AIRich as LeafRich,
4035
4321
  AIRich as VanzxyAI,
@@ -104,9 +104,27 @@ export class Button extends BaseBuilder {
104
104
  static menu?: (client: any, options?: Record<string, any>) => Button;
105
105
  }
106
106
 
107
+ export class RowBuilder {
108
+ constructor();
109
+ buttons: Record<string, any>[];
110
+ button(displayText: string, buttonId?: string): this;
111
+ }
112
+
113
+ export class CardBuilder {
114
+ constructor(client: any);
115
+ title(text: string): this;
116
+ text(text: string): this;
117
+ image(path: string | Buffer, options?: Record<string, any>): this;
118
+ button(displayText: string, buttonId?: string): this;
119
+ }
120
+
107
121
  export class ButtonV2 extends BaseBuilder {
108
122
  constructor(client: any);
109
123
  addButton(displayText: string, buttonId?: string): this;
124
+ /** Alias for addButton(). */
125
+ button(displayText: string, buttonId?: string): this;
126
+ /** Fluent row helper — group buttons via a callback instead of chaining addButton(). */
127
+ row(cb: (row: RowBuilder) => void): this;
110
128
  addRawButton(obj: Record<string, any>): this;
111
129
  setThumbnail(path: string | Buffer): this;
112
130
  setMedia(obj: Record<string, any>): this;
@@ -115,6 +133,30 @@ export class ButtonV2 extends BaseBuilder {
115
133
  send(jid: string, options?: Record<string, any> & { viewOnce?: boolean }): Promise<any>;
116
134
  }
117
135
 
136
+ /**
137
+ * Legacy `templateMessage` / `hydratedFourRowTemplate` builder — WA's Generation-1
138
+ * button protocol (predates the nativeFlow format Button/ButtonV2 use). Capped at
139
+ * 3 buttons (quickReply/url/call only), no interactive list/flow support.
140
+ */
141
+ export class ButtonV3 extends BaseBuilder {
142
+ constructor(client: any);
143
+ /** Load an existing templateMessage (e.g. from a fetched/quoted message) for editing. */
144
+ loadFrom(msg: Record<string, any>): this;
145
+ setImage(path: string | Buffer, options?: Record<string, any>): this;
146
+ setVideo(path: string | Buffer, options?: Record<string, any>): this;
147
+ setDocument(path: string | Buffer, options?: Record<string, any>): this;
148
+ setMedia(obj: Record<string, any>): this;
149
+ clearButtons(): this;
150
+ /** Max 3 buttons — throws past the limit. */
151
+ addButton(hydratedButton: Record<string, any>): this;
152
+ addReply(display_text?: string, id?: string): this;
153
+ addUrl(display_text?: string, url?: string, options?: Record<string, any>): this;
154
+ addCall(display_text?: string, phone_number?: string): this;
155
+ toTemplate(): Promise<Record<string, any>>;
156
+ build(jid: string, options?: Record<string, any>): Promise<Record<string, any>>;
157
+ send(jid: string, options?: Record<string, any>): Promise<any>;
158
+ }
159
+
118
160
  export class Carousel extends BaseBuilder {
119
161
  constructor(client: any);
120
162
  /** WhatsApp caps carousels at this many cards (10); addCard() throws past it. */
@@ -224,6 +266,9 @@ export class AIRich extends BaseBuilder {
224
266
  // MessageBuilder.js). All four are structurally identical to AIRich.
225
267
  export { AIRich as AIVanzxy, AIRich as LeafRich, AIRich as VanzxyAI, AIRich as VanzxyRich };
226
268
 
269
+ /** `class ORich extends AIRich {}` — plain re-export alias, no additional members. */
270
+ export class ORich extends AIRich {}
271
+
227
272
  export class Toolkit {
228
273
  static extractIE(text: string, options?: AddTextOptions): Record<string, any>;
229
274
  static getMp4Duration(buffer: Buffer, options?: { silent?: boolean }): Promise<number>;