@vanzxy/baileys 1.5.5 → 1.5.7

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.
@@ -44,7 +44,10 @@
44
44
 
45
45
  'use strict';
46
46
 
47
- const MESSAGE_BUILDER_VERSION = '4.9';
47
+ const MESSAGE_BUILDER_VERSION = '4.9.1';
48
+ // Vanz@Fix 24-08-26: use the exported builder version everywhere; the old
49
+ // verification helper referenced an undeclared `VERSION`, making every AIRich.build() fail.
50
+
48
51
 
49
52
  import { generateWAMessageFromContent, prepareWAMessageMedia } from './messages.js';
50
53
  import { generateMessageIDV2 } from './generics.js';
@@ -532,10 +535,6 @@ class Toolkit {
532
535
  }
533
536
  });
534
537
  }
535
-
536
- static stringifyEscaped(obj) {
537
- return JSON.stringify(obj).replace(/[\u007f-\uffff]/g, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
538
- }
539
538
  }
540
539
 
541
540
  /**
@@ -1013,6 +1012,120 @@ class Button extends BaseBuilder {
1013
1012
  return this;
1014
1013
  }
1015
1014
 
1015
+
1016
+ /** Native-flow `payment_key_info` shortcut. Payload is passed through unchanged. */
1017
+ addPaymentKeyInfo(payload = {}) {
1018
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) throw new TypeError('addPaymentKeyInfo(payload) requires a plain object');
1019
+ this._buttons.push({ name: 'payment_key_info', buttonParamsJson: JSON.stringify(payload) });
1020
+ return this;
1021
+ }
1022
+
1023
+ /** Native-flow `booking_confirmation` shortcut. Payload is passed through unchanged. */
1024
+ addBookingConfirmation(payload = {}) {
1025
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) throw new TypeError('addBookingConfirmation(payload) requires a plain object');
1026
+ this._buttons.push({ name: 'booking_confirmation', buttonParamsJson: JSON.stringify(payload) });
1027
+ return this;
1028
+ }
1029
+
1030
+ /** Native-flow `card_message` shortcut, matching this fork's prepareNativeFlowButtons(). */
1031
+ addCardMessage(payload = {}) {
1032
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) throw new TypeError('addCardMessage(payload) requires a plain object');
1033
+ this._buttons.push({ name: 'card_message', buttonParamsJson: JSON.stringify(payload) });
1034
+ return this;
1035
+ }
1036
+
1037
+ /** Native-flow `order_details` shortcut. */
1038
+ addOrderDetails(payload = {}) {
1039
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) throw new TypeError('addOrderDetails(payload) requires a plain object');
1040
+ this._buttons.push({ name: 'order_details', buttonParamsJson: JSON.stringify(payload) });
1041
+ return this;
1042
+ }
1043
+
1044
+ /** Native-flow `order_status` shortcut. */
1045
+ addOrderStatus(payload = {}) {
1046
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) throw new TypeError('addOrderStatus(payload) requires a plain object');
1047
+ this._buttons.push({ name: 'order_status', buttonParamsJson: JSON.stringify(payload) });
1048
+ return this;
1049
+ }
1050
+
1051
+ /** Native-flow `payment_status` shortcut. */
1052
+ addPaymentStatus(payload = {}) {
1053
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) throw new TypeError('addPaymentStatus(payload) requires a plain object');
1054
+ this._buttons.push({ name: 'payment_status', buttonParamsJson: JSON.stringify(payload) });
1055
+ return this;
1056
+ }
1057
+
1058
+ /** Native-flow `payment_method` shortcut. */
1059
+ addPaymentMethod(payload = {}) {
1060
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) throw new TypeError('addPaymentMethod(payload) requires a plain object');
1061
+ this._buttons.push({ name: 'payment_method', buttonParamsJson: JSON.stringify(payload) });
1062
+ return this;
1063
+ }
1064
+
1065
+ /** Native-flow `track_order` shortcut. */
1066
+ addTrackOrder(id, display_text = '🚚 Track order') {
1067
+ if (!id) throw new TypeError('addTrackOrder(id) requires a non-empty id');
1068
+ this._buttons.push({ name: 'track_order', buttonParamsJson: JSON.stringify({ id, display_text }) });
1069
+ return this;
1070
+ }
1071
+
1072
+ /** Native-flow `reorder` shortcut. */
1073
+ addReorder(id, display_text = '🔁 Reorder') {
1074
+ if (!id) throw new TypeError('addReorder(id) requires a non-empty id');
1075
+ this._buttons.push({ name: 'reorder', buttonParamsJson: JSON.stringify({ id, display_text }) });
1076
+ return this;
1077
+ }
1078
+
1079
+ /** Native-flow `cancel_order` shortcut. */
1080
+ addCancelOrder(id, display_text = '❌ Cancel order') {
1081
+ if (!id) throw new TypeError('addCancelOrder(id) requires a non-empty id');
1082
+ this._buttons.push({ name: 'cancel_order', buttonParamsJson: JSON.stringify({ id, display_text }) });
1083
+ return this;
1084
+ }
1085
+
1086
+ /** Native-flow `clear_chat` shortcut. */
1087
+ addClearChat() {
1088
+ this._buttons.push({ name: 'clear_chat', buttonParamsJson: '{}' });
1089
+ return this;
1090
+ }
1091
+
1092
+ /** Native-flow `navigateToScreen` shortcut. */
1093
+ addNavigateToScreen(screen, data = {}) {
1094
+ if (!screen) throw new TypeError('addNavigateToScreen(screen) requires a non-empty screen');
1095
+ this._buttons.push({ name: 'navigateToScreen', buttonParamsJson: JSON.stringify({ screen_name: screen, data }) });
1096
+ return this;
1097
+ }
1098
+
1099
+ /** WhatsApp Flows `flow_action` shortcut. Requires a real registered Flow. */
1100
+ addFlow(flow = {}, display_text = '') {
1101
+ if (typeof flow !== 'object' || flow === null || Array.isArray(flow) || !flow.id) throw new TypeError('addFlow(flow) requires a plain object with flow.id');
1102
+ this._buttons.push({
1103
+ name: 'flow_action',
1104
+ buttonParamsJson: JSON.stringify({
1105
+ flow_message_version: flow.version || '3',
1106
+ flow_id: flow.id,
1107
+ flow_cta: display_text || flow.cta || 'Continue',
1108
+ flow_action: flow.action || 'navigate',
1109
+ flow_action_payload: flow.actionPayload || { screen: flow.screen || 'WELCOME', data: flow.data || {} },
1110
+ }),
1111
+ });
1112
+ return this;
1113
+ }
1114
+
1115
+ /** Native-flow `voice_call` shortcut. */
1116
+ addVoiceCall(id, display_text = '📞 Voice call') {
1117
+ if (!id) throw new TypeError('addVoiceCall(id) requires a non-empty id');
1118
+ this._buttons.push({ name: 'voice_call', buttonParamsJson: JSON.stringify({ display_text, id }) });
1119
+ return this;
1120
+ }
1121
+
1122
+ /** Native-flow `video_call_button` shortcut. */
1123
+ addVideoCall(id, display_text = '🎥 Video call') {
1124
+ if (!id) throw new TypeError('addVideoCall(id) requires a non-empty id');
1125
+ this._buttons.push({ name: 'video_call_button', buttonParamsJson: JSON.stringify({ display_text, id }) });
1126
+ return this;
1127
+ }
1128
+
1016
1129
  // Vanz@Fix (bug 43) --- paramsList documented the schema for these 3 message-level native flow
1017
1130
  // params (limited_time_offer / bottom_sheet / tap_target_configuration) but no helper ever wrote
1018
1131
  // them into this._params — only manual setParams() could, with zero validation against the
@@ -1092,6 +1205,8 @@ class Button extends BaseBuilder {
1092
1205
  send_location: { v: '2', name: 'send_location' },
1093
1206
  call_permission_request: { v: '2', name: 'call_permission_request' },
1094
1207
  wa_payment_transaction_details: { v: '2', name: 'wa_payment_transaction_details' },
1208
+ payment_key_info: { v: '1', name: 'payment_key_info' },
1209
+ booking_confirmation: { v: '1', name: 'booking_confirmation' },
1095
1210
  automated_greeting_message_view_catalog: { v: '2', name: 'automated_greeting_message_view_catalog' },
1096
1211
  };
1097
1212
 
@@ -1577,48 +1692,10 @@ class Poll extends BaseBuilder {
1577
1692
  * everything ChatGPT/Gemini-in-WhatsApp-style bots typically render.
1578
1693
  * Also exported as `AIVanzxy` / `LeafRich` / `VanzxyAI` / `VanzxyRich` (identical class, alternate names).
1579
1694
  */
1580
-
1581
- class AIRichError extends Error {
1582
- constructor(message, code, meta = {}) {
1583
- super(message);
1584
- this.name = 'AIRichError';
1585
- this.code = code;
1586
- Object.assign(this, meta);
1587
- }
1588
- }
1589
-
1590
- class ItemNotFoundError extends AIRichError {
1591
- constructor(id, knownIds = []) {
1592
- super(`Item with id "${id}" not found`, 'ITEM_NOT_FOUND', { id, knownIds });
1593
- this.name = 'ItemNotFoundError';
1594
- }
1595
- }
1596
-
1597
- class DuplicateIdError extends AIRichError {
1598
- constructor(id) {
1599
- super(`Id "${id}" is already in use`, 'DUPLICATE_ID', { id });
1600
- this.name = 'DuplicateIdError';
1601
- }
1602
- }
1603
-
1604
- class InvalidTargetError extends AIRichError {
1605
- constructor(message, meta = {}) {
1606
- super(message, 'INVALID_TARGET', meta);
1607
- this.name = 'InvalidTargetError';
1608
- }
1609
- }
1610
-
1611
- class ContentValidationError extends AIRichError {
1612
- constructor(message, meta = {}) {
1613
- super(message, 'CONTENT_VALIDATION', meta);
1614
- this.name = 'ContentValidationError';
1615
- }
1616
- }
1617
-
1618
1695
  class AIRich extends BaseBuilder {
1619
1696
  #client;
1620
1697
 
1621
- constructor(client, { dynamic = true, unsupportedTypeAlert = true } = {}) {
1698
+ constructor(client) {
1622
1699
  if (!client) {
1623
1700
  throw new Error('Socket is required');
1624
1701
  }
@@ -1626,125 +1703,152 @@ class AIRich extends BaseBuilder {
1626
1703
  super();
1627
1704
  this.#client = client;
1628
1705
  this._contextInfo = {};
1629
- this._nodes = [];
1630
- this._idIndex = new Map();
1631
- this._unsupportedTypeAlert = !!unsupportedTypeAlert;
1632
- this._dynamic = !!dynamic;
1633
- this._responseId = crypto.randomUUID();
1634
- this._botResponseId = crypto.randomUUID();
1706
+ this._submessages = [];
1707
+ this._sections = [];
1708
+ this._richResponseSources = [];
1709
+ // Vanz@Fix (bug 42 / inline image fallback): WA rejects rendering AIRichResponseInlineImageMetadata
1710
+ // for third-party bots regardless of URL (confirmed empirically — Meta/WA CDN url with valid
1711
+ // mediaKey still doesn't render, so it's a trust-chain gate, not a domain/encoding issue).
1712
+ // Track every addInlineImage() call here so send() can fall back to a normal imageMessage.
1713
+ this._inlineImages = [];
1714
+
1715
+ // Vanz@Add 24-08-26 --- ported from temen's MessageBuilderV4.7 (setResponseId/setBotResponseId
1716
+ // below), rewritten to fit this fork's conventions. build() used to always mint a fresh
1717
+ // crypto.randomUUID() for both unifiedResponse.response_id and botMetadata.botResponseId on
1718
+ // every call, with no way to reuse one — so a `sendEdit()`-style flow (rebuild the same
1719
+ // message with updated content, same response_id, so WA patches it in place instead of
1720
+ // showing a new message) was never actually possible despite being in the gist example.
1721
+ // null here means "not pinned yet" — build() falls back to a fresh randomUUID() same as before
1722
+ // when neither setResponseId() nor setBotResponseId() has been called.
1723
+ this._responseId = null;
1724
+ this._botResponseId = null;
1725
+
1726
+ // Vanz@Add --- set by send()/sendEdit() after every relay so a follow-up sendEdit(), called
1727
+ // with no args, knows which jid/message id to patch in place (matches temen's v4.7 API).
1635
1728
  this._lastMessageKey = null;
1636
- }
1637
-
1638
- loadFrom(msg) {
1639
- if (!msg) throw new Error('AI Rich message needed');
1640
-
1641
- const message = msg.message ?? msg;
1642
-
1643
- let richResponseMessage = message?.botForwardedMessage?.message?.richResponseMessage;
1644
-
1645
- if (!richResponseMessage) {
1646
- richResponseMessage = message?.botForwardedMessage?.richResponseMessage;
1647
- }
1648
-
1649
- if (!richResponseMessage) {
1650
- richResponseMessage = message?.richResponseMessage;
1651
- }
1652
1729
 
1653
- if (!richResponseMessage) {
1654
- throw new Error('richResponseMessage not found');
1655
- }
1656
-
1657
- const messageContextInfo = message?.messageContextInfo ?? {};
1658
- const botMetadata = messageContextInfo?.botMetadata ?? {};
1659
-
1660
- this._title = botMetadata?.messageDisclaimerText ?? '';
1661
-
1662
- this._contextInfo = structuredClone(richResponseMessage?.contextInfo ?? {});
1663
-
1664
- const loadedSubmessages = Array.isArray(richResponseMessage?.submessages) ? structuredClone(richResponseMessage.submessages) : [];
1665
-
1666
- let loadedSections = [];
1667
-
1668
- const unifiedData = richResponseMessage?.unifiedResponse?.data;
1669
-
1670
- if (unifiedData) {
1671
- try {
1672
- const decoded = Buffer.from(unifiedData, 'base64').toString('utf8');
1673
- const unifiedResponse = JSON.parse(decoded);
1674
-
1675
- if (Array.isArray(unifiedResponse?.sections)) {
1676
- loadedSections = structuredClone(unifiedResponse.sections);
1730
+ // Vanz@Add (v4.9.1) --- { id, insertAt } support for every add*()/set*() call, without
1731
+ // touching each method's own body/signature. Every add*() call ends up pushing 0-N items
1732
+ // onto _submessages and 0-N onto _sections (some push to both, some to just one — e.g.
1733
+ // addSuggest only touches _submessages, addSection only touches _sections). A Proxy wraps
1734
+ // every add*/set* call: it snapshots array lengths before calling the real method, lets the
1735
+ // method push onto the tail as it always has, then — if the caller passed { insertAt }
1736
+ // peels those freshly-pushed items back off the tail and re-splices them right after the
1737
+ // last item that belongs to the block named by insertAt. Blocks are tracked by *object
1738
+ // reference*, not saved numeric index, so earlier insertions shifting the array around never
1739
+ // invalidates a later insertAt lookup (indexOf on the reference always finds the live position).
1740
+ this._blocks = new Map(); // id -> { subItems: object[], secItems: object[] }
1741
+ return new Proxy(this, {
1742
+ get(target, prop, receiver) {
1743
+ const orig = Reflect.get(target, prop, receiver);
1744
+ if (typeof orig !== 'function') return orig;
1745
+
1746
+ // Vanz@Fix 23-08-26 (part 2) --- the add*/set* filter below only wrapped methods whose
1747
+ // name starts with "add"/"set". Everything else (send(), build(), ...) fell through to
1748
+ // `return orig` unwrapped, so calling e.g. `richInstance.send(...)` still invoked the
1749
+ // real method with `this` = the Proxy (`receiver`), hitting the exact same
1750
+ // "Cannot read private member #client..." brand-check error the add*/set* fix was for —
1751
+ // just one level up, in send()/build() themselves. Every function property now gets
1752
+ // bound to `target` (the real instance) at minimum; add*/set* additionally get the
1753
+ // insertAt/id bookkeeping below.
1754
+ if (!/^(add|set)/.test(String(prop))) {
1755
+ return (...args) => {
1756
+ const result = orig.apply(target, args);
1757
+ return result === target ? receiver : result;
1758
+ };
1677
1759
  }
1678
- } catch {}
1679
- }
1680
-
1681
- this._nodes = [];
1682
- this._idIndex = new Map();
1683
-
1684
- const maxLength = Math.max(loadedSections.length, loadedSubmessages.length);
1685
-
1686
- for (let i = 0; i < maxLength; i++) {
1687
- this._nodes.push({
1688
- id: null,
1689
- section: loadedSections[i] ?? null,
1690
- submessage: loadedSubmessages[i] ?? null,
1691
- });
1692
- }
1693
1760
 
1694
- this._extraPayload = {};
1761
+ return (...args) => {
1762
+ const opts = args.find((a) => a && typeof a === 'object' && !Array.isArray(a) && !Buffer.isBuffer(a) && ('id' in a || 'insertAt' in a));
1763
+ const id = opts?.id;
1764
+ const insertAt = opts?.insertAt;
1765
+
1766
+ const subBefore = target._submessages.length;
1767
+ const secBefore = target._sections.length;
1768
+
1769
+ // Vanz@Fix 23-08-26 --- was orig.apply(receiver, args): calling the real method bound to
1770
+ // the Proxy itself (`receiver`) makes any `this.#client` access inside throw
1771
+ // "Cannot read private member #client from an object whose class did not declare it",
1772
+ // because a Proxy is never the branded instance a private field was declared on —
1773
+ // this hit every add*() that touches #client via Toolkit.resolveMedia(this.#client, ...)
1774
+ // (addProduct/addPost/addReels/addSource, and would eventually hit addImage/addVideo
1775
+ // too once JIT/engine specifics changed). Binding to `target` (the real instance) instead
1776
+ // fixes it for good; `target._submessages`/`target._sections` below are unaffected since
1777
+ // they're plain properties, and `result === target ? receiver : result` still converts a
1778
+ // `this`-return back to the Proxy so chaining (`.addX().addY()`) keeps working.
1779
+ const result = orig.apply(target, args);
1780
+
1781
+
1782
+ const subItems = target._submessages.splice(subBefore);
1783
+ const secItems = target._sections.splice(secBefore);
1784
+
1785
+ if (insertAt) {
1786
+ const anchor = target._blocks.get(insertAt);
1787
+ if (!anchor) throw new Error(`insertAt: no block registered with id "${insertAt}" (register it by passing { id: "${insertAt}" } on an earlier add*() call)`);
1788
+
1789
+ const lastSub = anchor.subItems[anchor.subItems.length - 1];
1790
+ const subIdx = lastSub ? target._submessages.indexOf(lastSub) + 1 : target._submessages.length;
1791
+ target._submessages.splice(subIdx, 0, ...subItems);
1792
+
1793
+ const lastSec = anchor.secItems[anchor.secItems.length - 1];
1794
+ const secIdx = lastSec ? target._sections.indexOf(lastSec) + 1 : target._sections.length;
1795
+ target._sections.splice(secIdx, 0, ...secItems);
1796
+ } else {
1797
+ target._submessages.push(...subItems);
1798
+ target._sections.push(...secItems);
1799
+ }
1695
1800
 
1696
- for (const [key, value] of Object.entries(message)) {
1697
- if (key !== 'messageContextInfo' && key !== 'botForwardedMessage' && key !== 'richResponseMessage') {
1698
- this._extraPayload[key] = structuredClone(value);
1699
- }
1700
- }
1801
+ if (id) target._blocks.set(id, { subItems, secItems });
1701
1802
 
1702
- return this;
1803
+ return result === target ? receiver : result;
1804
+ };
1805
+ },
1806
+ });
1703
1807
  }
1704
1808
 
1705
- setResponseId(id) {
1706
- if (typeof id !== 'string') {
1707
- throw new TypeError('ID must be a string');
1708
- }
1709
- this._responseId = id;
1710
-
1711
- return this;
1809
+ /** Flatten every primitive pushed into `_sections` so far into one array — lets you build a
1810
+ * card set in one AIRich instance and re-embed it into another via addSection(AIRich.newLayout(...)). */
1811
+ get items() {
1812
+ return this._sections.flatMap((s) => {
1813
+ const vm = s?.view_model;
1814
+ if (!vm) return [];
1815
+ return vm.primitives ?? (vm.primitive !== undefined ? [vm.primitive] : []);
1816
+ });
1712
1817
  }
1713
1818
 
1714
- refreshResponseId() {
1715
- this._responseId = crypto.randomUUID();
1819
+ /** Push a raw pre-built submessage block (escape hatch for shapes not covered by the add*() helpers). */
1820
+ addSubmessage(submessage) {
1821
+ const items = Array.isArray(submessage) ? submessage : [submessage];
1716
1822
 
1717
- return this;
1718
- }
1823
+ for (const item of items) {
1824
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) {
1825
+ throw new TypeError('Submessage must be a plain object or array of plain objects');
1826
+ }
1719
1827
 
1720
- setBotResponseId(id) {
1721
- if (typeof id !== 'string') {
1722
- throw new TypeError('ID must be a string');
1828
+ this._submessages.push(item);
1723
1829
  }
1724
- this._botResponseId = id;
1725
1830
 
1726
1831
  return this;
1727
1832
  }
1728
1833
 
1729
- refreshBotResponseId() {
1730
- this._botResponseId = crypto.randomUUID();
1834
+ /** Push a raw pre-built section wrapper around one or more submessages. */
1835
+ addSection(section) {
1836
+ const items = Array.isArray(section) ? section : [section];
1731
1837
 
1732
- return this;
1733
- }
1838
+ for (const item of items) {
1839
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) {
1840
+ throw new TypeError('Section must be a plain object or array of plain objects');
1841
+ }
1734
1842
 
1735
- createAlert(type) {
1736
- if (this._unsupportedTypeAlert) {
1737
- return {
1738
- messageType: 2,
1739
- messageText: `[ UNSUPPORTED_TYPE - ${type}]`,
1740
- };
1843
+ this._sections.push(item);
1741
1844
  }
1742
1845
 
1743
- return undefined;
1846
+ return this;
1744
1847
  }
1745
1848
 
1746
- addText(text, { hyperlink = true, citation = true, latex = true, id, replace, insertAt } = {}) {
1747
- if (typeof text !== 'string') {
1849
+ /** 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. */
1850
+ addText(text, { hyperlink = true, citation = true, latex = true } = {}) {
1851
+ if (typeof text != 'string') {
1748
1852
  throw new TypeError('Text must be a string');
1749
1853
  }
1750
1854
 
@@ -1754,180 +1858,178 @@ class AIRich extends BaseBuilder {
1754
1858
  latex,
1755
1859
  });
1756
1860
 
1757
- const section = AIRich.newLayout('Single', {
1758
- text: extractedText,
1759
- ...(inline_entities.length && { inline_entities }),
1760
- __typename: 'GenAIMarkdownTextUXPrimitive',
1761
- });
1762
-
1763
- const submessages = [
1764
- {
1765
- messageType: 2,
1766
- messageText: text,
1767
- },
1768
- ].filter(Boolean);
1769
-
1770
- return this._addContent(section, submessages, {
1771
- id,
1772
- replace,
1773
- insertAt,
1774
- });
1775
- }
1776
-
1777
- addFOAText(text, { id, replace, insertAt } = {}) {
1778
- if (typeof text !== 'string') {
1779
- throw new TypeError('Text must be a string');
1780
- }
1781
-
1782
- const section = AIRich.newLayout('Single', {
1783
- text,
1784
- __typename: 'FOATextPrimitive',
1861
+ this._submessages.push({
1862
+ messageType: 2,
1863
+ messageText: extractedText,
1785
1864
  });
1786
1865
 
1787
- const submessages = [
1788
- {
1789
- messageType: 2,
1790
- messageText: text,
1791
- },
1792
- ];
1866
+ this._sections.push(
1867
+ AIRich.newLayout('Single', {
1868
+ text: extractedText,
1869
+ ...(inline_entities.length && {
1870
+ inline_entities,
1871
+ }),
1872
+ __typename: 'GenAIMarkdownTextUXPrimitive',
1873
+ })
1874
+ );
1793
1875
 
1794
- return this._addContent(section, submessages, {
1795
- id,
1796
- replace,
1797
- insertAt,
1798
- });
1876
+ return this;
1799
1877
  }
1800
1878
 
1801
- addCode(language, code, { id, replace, insertAt } = {}) {
1879
+ /** Add a syntax-highlighted code block. @param {string} language e.g. 'javascript', 'python'. */
1880
+ addCode(language, code) {
1802
1881
  if (typeof language !== 'string' || typeof code !== 'string') {
1803
1882
  throw new TypeError('Language and code must be a string');
1804
1883
  }
1805
1884
 
1806
1885
  const meta = AIRich.tokenizer(code, language);
1807
1886
 
1808
- const section = AIRich.newLayout('Single', {
1809
- language,
1810
- code_blocks: meta.unified_codeBlock,
1811
- __typename: 'GenAICodeUXPrimitive',
1887
+ this._submessages.push({
1888
+ messageType: 5,
1889
+ codeMetadata: {
1890
+ codeLanguage: language,
1891
+ codeBlocks: meta.codeBlock,
1892
+ },
1812
1893
  });
1813
1894
 
1814
- const submessages = [
1815
- {
1816
- messageType: 5,
1817
- codeMetadata: {
1818
- codeLanguage: language,
1819
- codeBlocks: meta.codeBlock,
1820
- },
1821
- },
1822
- ];
1895
+ this._sections.push(
1896
+ AIRich.newLayout('Single', {
1897
+ language,
1898
+ code_blocks: meta.unified_codeBlock,
1899
+ __typename: 'GenAICodeUXPrimitive',
1900
+ })
1901
+ );
1823
1902
 
1824
- return this._addContent(section, submessages, {
1825
- id,
1826
- replace,
1827
- insertAt,
1828
- });
1903
+ return this;
1829
1904
  }
1830
1905
 
1831
- addTable(table, { hyperlink = true, citation = true, latex = true, id, replace, insertAt } = {}) {
1906
+ /** Add a table. @param {string[][]} table Row-major grid, first row treated as the header. */
1907
+ addTable(table, { hyperlink = true, citation = true, latex = true } = {}) {
1832
1908
  if (!Array.isArray(table)) {
1833
1909
  throw new TypeError('Table must be an array');
1834
1910
  }
1835
1911
 
1836
- const meta = AIRich.toTableMetadata(table, {
1837
- hyperlink,
1838
- citation,
1839
- latex,
1840
- });
1912
+ const meta = AIRich.toTableMetadata(table, { hyperlink, citation, latex });
1841
1913
 
1842
- const section = AIRich.newLayout('Single', {
1843
- rows: meta.unified_rows,
1844
- __typename: 'GenATableUXPrimitive',
1914
+ this._submessages.push({
1915
+ messageType: 4,
1916
+ tableMetadata: {
1917
+ title: meta.title,
1918
+ rows: meta.rows,
1919
+ },
1845
1920
  });
1846
1921
 
1847
- const submessages = [
1848
- {
1849
- messageType: 4,
1850
- tableMetadata: {
1851
- title: meta.title,
1852
- rows: meta.rows,
1853
- },
1854
- },
1855
- ];
1922
+ this._sections.push(
1923
+ AIRich.newLayout('Single', {
1924
+ rows: meta.unified_rows,
1925
+ __typename: 'GenATableUXPrimitive',
1926
+ })
1927
+ );
1856
1928
 
1857
- return this._addContent(section, submessages, {
1858
- id,
1859
- replace,
1860
- insertAt,
1861
- });
1929
+ return this;
1862
1930
  }
1863
1931
 
1864
- addSource(sources = [], { id, replace, insertAt } = {}) {
1865
- if (!Array.isArray(sources)) {
1866
- throw new TypeError('Sources must be an array of strings, arrays, or objects');
1867
- }
1868
-
1869
- const isStringArray = sources.every((item) => typeof item === 'string');
1932
+ /** Add a "Sources" strip. @param {string[]|string[][]} sources Flat list of urls, or `[title, url]` pairs. */
1933
+
1934
+ /** Build rich-response citation/link submessages using the same shape as Baileys' `links` content shortcut. */
1935
+ addLinks(links = []) {
1936
+ if (!Array.isArray(links)) throw new TypeError('links must be an array');
1937
+ links.forEach((linkField, index) => {
1938
+ if (!linkField || typeof linkField !== 'object') throw new TypeError('Each link must be an object');
1939
+ const prefix = 'SS_' + index;
1940
+ const url = linkField.url || '';
1941
+ const text = String(linkField.text ?? '');
1942
+ const sources = Array.isArray(linkField.sources) ? linkField.sources.map((sourceField) => ({
1943
+ source_type: 'THIRD_PARTY',
1944
+ source_display_name: sourceField?.displayName || sourceField?.title || 'Source',
1945
+ source_subtitle: sourceField?.subtitle || '',
1946
+ source_url: sourceField?.url || url,
1947
+ })) : [];
1948
+ const entity = {
1949
+ key: prefix,
1950
+ metadata: {
1951
+ reference_id: index + 1,
1952
+ reference_url: url,
1953
+ reference_title: linkField.title || 'Source',
1954
+ reference_display_name: linkField.displayName || linkField.title || 'Source',
1955
+ sources,
1956
+ __typename: 'GenAISearchCitationItem',
1957
+ },
1958
+ };
1959
+ const section = AIRich.newLayout('Single', {
1960
+ text: `${text} {{${prefix}}}${url}{{/${prefix}}}`,
1961
+ inline_entities: [entity],
1962
+ __typename: 'GenAIMarkdownTextUXPrimitive',
1963
+ });
1964
+ this._sections.push(section);
1965
+ this._submessages.push({
1966
+ messageType: 2,
1967
+ messageText: `${text} {{${prefix}}}¹{{/${prefix}}} `,
1968
+ inlineEntities: [entity],
1969
+ });
1970
+ });
1971
+ return this;
1972
+ }
1870
1973
 
1871
- const isArrayFormat = sources.every((item) => Array.isArray(item) && item.every((value) => typeof value === 'string'));
1974
+ /** Add a raw rich-response content-items carousel, matching Baileys' `items` field. */
1975
+ addContentItems(items = []) {
1976
+ if (!Array.isArray(items)) throw new TypeError('items must be an array');
1977
+ this._submessages.push({
1978
+ messageType: 9,
1979
+ contentItemsMetadata: { itemsMetadata: items, contentType: 1 },
1980
+ });
1981
+ this._sections.push(AIRich.newLayout('Single', {
1982
+ items,
1983
+ content_type: 1,
1984
+ __typename: 'GenAIContentItemsUXPrimitive',
1985
+ }));
1986
+ return this;
1987
+ }
1872
1988
 
1873
- const isObjectFormat = sources.every((item) => item && typeof item === 'object' && !Array.isArray(item));
1989
+ /** Add Baileys-compatible inline-video marker. WhatsApp's current rich-response helper carries this as a text marker. */
1990
+ addInlineVideo() {
1991
+ this._submessages.push({ messageType: 2, messageText: 'INLINE_VIDEO' });
1992
+ this._sections.push(AIRich.newLayout('Single', {
1993
+ text: 'INLINE_VIDEO',
1994
+ __typename: 'GenAIMarkdownTextUXPrimitive',
1995
+ }));
1996
+ return this;
1997
+ }
1874
1998
 
1875
- if (!isStringArray && !isArrayFormat && !isObjectFormat) {
1876
- throw new TypeError('Sources must be a string array, array of string arrays, or array of objects');
1999
+ addSource(sources = [], { resolveUrl = false } = {}) {
2000
+ if (!(Array.isArray(sources) && (sources.every((item) => typeof item === 'string') || sources.every((item) => Array.isArray(item) && item.every((v) => typeof v === 'string'))))) {
2001
+ throw new TypeError('Sources must be a string array or an array of string arrays');
1877
2002
  }
1878
2003
 
1879
- if (isStringArray) {
2004
+ if (sources.every((item) => typeof item === 'string')) {
1880
2005
  sources = [sources];
1881
2006
  }
1882
2007
 
1883
- const normalizedSources = sources.map((source) => {
1884
- if (Array.isArray(source)) {
1885
- const [icon, url, title, subtitle] = source;
1886
-
1887
- return {
1888
- icon,
1889
- url,
1890
- title,
1891
- subtitle,
1892
- };
1893
- }
1894
-
1895
- return {
1896
- icon: source.favicon ?? source.icon ?? '',
1897
- url: source.url ?? '',
1898
- title: source.title ?? '',
1899
- subtitle: source.subtitle ?? '',
1900
- };
1901
- });
1902
-
1903
- const source = normalizedSources.map(({ icon, url, title, subtitle }) => ({
2008
+ const source = sources.map(([icon, url, text]) => ({
1904
2009
  source_type: 'THIRD_PARTY',
1905
- source_display_name: title,
1906
- source_subtitle: subtitle,
1907
- source_url: url,
2010
+ source_display_name: text ?? '',
2011
+ source_subtitle: 'AI',
2012
+ source_url: url ?? '',
1908
2013
  favicon: {
1909
- url: Toolkit.resolveMedia(this.#client, icon, 'image'),
2014
+ url: Toolkit.resolveMedia(this.#client, icon ?? '', 'image', { resolveUrl }),
1910
2015
  mime_type: 'image/jpeg',
1911
2016
  width: 16,
1912
2017
  height: 16,
1913
2018
  },
1914
2019
  }));
1915
2020
 
1916
- const submessage = this.createAlert('GenAISearchResultPrimitive');
1917
-
1918
- const section = AIRich.newLayout('Single', {
1919
- sources: source,
1920
- __typename: 'GenAISearchResultPrimitive',
1921
- });
2021
+ this._sections.push(
2022
+ AIRich.newLayout('Single', {
2023
+ sources: source,
2024
+ __typename: 'GenAISearchResultPrimitive',
2025
+ })
2026
+ );
1922
2027
 
1923
- return this._addContent(section, submessage, {
1924
- id,
1925
- replace,
1926
- insertAt,
1927
- });
2028
+ return this;
1928
2029
  }
1929
2030
 
1930
- addReels(reelsItems = [], { id, replace, insertAt } = {}) {
2031
+ /** Add a horizontally-scrollable reel of image/video items. */
2032
+ addReels(reelsItems = [], { resolveUrl = false } = {}) {
1931
2033
  if (
1932
2034
  !(
1933
2035
  (reelsItems && typeof reelsItems === 'object' && !Array.isArray(reelsItems)) ||
@@ -1937,64 +2039,88 @@ class AIRich extends BaseBuilder {
1937
2039
  throw new TypeError('Reels items must be an object or an array of objects');
1938
2040
  }
1939
2041
 
1940
- const items = Array.isArray(reelsItems) ? reelsItems : [reelsItems];
2042
+ if (!Array.isArray(reelsItems)) {
2043
+ reelsItems = [reelsItems];
2044
+ }
1941
2045
 
1942
- const reels = items.map((item) => ({
2046
+ const reels = reelsItems.map((item) => ({
1943
2047
  ...item,
1944
- _avatar: Toolkit.resolveMedia(this.#client, item.profileIconUrl ?? item.profile_url ?? item.profile ?? '', 'image'),
1945
- _thumbnail: Toolkit.resolveMedia(this.#client, item.thumbnailUrl ?? item.thumbnail ?? '', 'image'),
2048
+ _avatar: Toolkit.resolveMedia(this.#client, item.profileIconUrl ?? item.profile_url ?? item.profile ?? '', 'image', { resolveUrl }),
2049
+ _thumbnail: Toolkit.resolveMedia(this.#client, item.thumbnailUrl ?? item.thumbnail ?? '', 'image', { resolveUrl }),
1946
2050
  }));
1947
2051
 
1948
- const section = AIRich.newLayout(
1949
- 'HScroll',
1950
- reels.map((item) => ({
1951
- reels_url: item.videoUrl ?? item.url ?? '',
1952
- thumbnail_url: item._thumbnail,
1953
- creator: item.username ?? item.title ?? '',
1954
- avatar_url: item._avatar,
1955
- reels_title: item.reels_title ?? item.title ?? '',
1956
- likes_count: item.likes_count ?? item.like ?? 0,
1957
- shares_count: item.shares_count ?? item.share ?? 0,
1958
- view_count: item.view_count ?? item.view ?? 0,
1959
- reel_source: item.reel_source ?? item.source ?? 'IG',
1960
- is_verified: !!(item.is_verified || item.verified),
1961
- __typename: 'GenAIReelPrimitive',
1962
- }))
1963
- );
1964
-
1965
- const submessages = [
1966
- {
1967
- messageType: 9,
1968
- contentItemsMetadata: {
1969
- contentType: 1,
1970
- itemsMetadata: reels.map((item) => ({
1971
- reelItem: {
1972
- title: item.username ?? '',
1973
- profileIconUrl: item._avatar,
1974
- thumbnailUrl: item._thumbnail,
1975
- videoUrl: item.videoUrl ?? item.url ?? '',
1976
- },
1977
- })),
1978
- },
2052
+ this._submessages.push({
2053
+ messageType: 9,
2054
+ contentItemsMetadata: {
2055
+ contentType: 1,
2056
+ itemsMetadata: reels.map((item) => ({
2057
+ reelItem: {
2058
+ title: item.username ?? '',
2059
+ profileIconUrl: item._avatar,
2060
+ thumbnailUrl: item._thumbnail,
2061
+ videoUrl: item.videoUrl ?? item.url ?? '',
2062
+ },
2063
+ })),
1979
2064
  },
1980
- ];
2065
+ });
1981
2066
 
1982
- return this._addContent(section, submessages, {
1983
- id,
1984
- replace,
1985
- insertAt,
2067
+ reels.forEach((item, idx) => {
2068
+ this._richResponseSources.push({
2069
+ provider: 'Evernight AI',
2070
+ thumbnailCDNURL: item._thumbnail,
2071
+ sourceProviderURL: item.videoUrl ?? item.url ?? '',
2072
+ sourceQuery: '',
2073
+ faviconCDNURL: item._avatar,
2074
+ citationNumber: idx + 1,
2075
+ sourceTitle: item.username ?? '',
2076
+ });
1986
2077
  });
2078
+
2079
+ this._sections.push(
2080
+ AIRich.newLayout(
2081
+ 'HScroll',
2082
+ reels.map((item) => ({
2083
+ reels_url: item.videoUrl ?? item.url ?? '',
2084
+ thumbnail_url: item._thumbnail,
2085
+ creator: item.username ?? item.title ?? '',
2086
+ avatar_url: item._avatar,
2087
+ reels_title: item.reels_title ?? item.title ?? '',
2088
+ likes_count: item.likes_count ?? item.like ?? 0,
2089
+ shares_count: item.shares_count ?? item.share ?? 0,
2090
+ view_count: item.view_count ?? item.view ?? 0,
2091
+ reel_source: item.reel_source ?? item.source ?? 'IG',
2092
+ is_verified: !!(item.is_verified || item.verified),
2093
+ __typename: 'GenAIReelPrimitive',
2094
+ }))
2095
+ )
2096
+ );
2097
+
2098
+ return this;
1987
2099
  }
1988
2100
 
1989
- addImage(imageUrl, { width, height, status = 'READY', update_text, resolveUrl = false, id, replace, insertAt } = {}) {
2101
+ /** Add a full-width image (or grid of images if `imageUrl` is an array). */
2102
+ /**
2103
+ * @param {{ resolveUrl?: boolean, instant?: boolean|'only' }} [options]
2104
+ * `instant: true` — sends BOTH: the GRID_IMAGE card (still shows WA's "can't verify"
2105
+ * forwarded-download prompt, unavoidable per-design of botForwardedMessage) AND a plain
2106
+ * (non-forwarded) imageMessage via send()'s inline-image fallback queue (`_inlineImages`,
2107
+ * shared with addInlineImage()) that renders instantly with no prompt. Two images, by design.
2108
+ * `instant: 'only'` — Vanz@Add (v4.9.2): skips building the GRID_IMAGE card entirely (no
2109
+ * submessage, no GenAIImaginePrimitive section) and queues ONLY the plain imageMessage.
2110
+ * One image, no prompt, nothing to download — use this when you don't need the rich card,
2111
+ * just the picture to show up immediately.
2112
+ */
2113
+ addImage(imageUrl, { resolveUrl = false, instant = false } = {}) {
1990
2114
  if (!(typeof imageUrl === 'string' || Buffer.isBuffer(imageUrl) || (Array.isArray(imageUrl) && imageUrl.every((v) => typeof v === 'string' || Buffer.isBuffer(v))))) {
1991
2115
  throw new TypeError('imageUrl must be string | buffer | array of string/buffer');
1992
2116
  }
2117
+ if (instant !== false && instant !== true && instant !== 'only') {
2118
+ throw new TypeError(`instant must be false, true, or 'only' — got ${JSON.stringify(instant)}`);
2119
+ }
1993
2120
 
1994
2121
  const list = Array.isArray(imageUrl)
1995
2122
  ? imageUrl.map((v) => {
1996
2123
  const url = Toolkit.resolveMedia(this.#client, v, 'image', { resolveUrl });
1997
-
1998
2124
  return {
1999
2125
  imagePreviewUrl: url,
2000
2126
  imageHighResUrl: url,
@@ -2003,7 +2129,6 @@ class AIRich extends BaseBuilder {
2003
2129
  })
2004
2130
  : (() => {
2005
2131
  const url = Toolkit.resolveMedia(this.#client, imageUrl, 'image', { resolveUrl });
2006
-
2007
2132
  return [
2008
2133
  {
2009
2134
  imagePreviewUrl: url,
@@ -2013,46 +2138,115 @@ class AIRich extends BaseBuilder {
2013
2138
  ];
2014
2139
  })();
2015
2140
 
2016
- const sections = list.map(({ imagePreviewUrl }) =>
2017
- AIRich.newLayout('Single', {
2018
- media: {
2019
- url: imagePreviewUrl,
2020
- mime_type: 'image/png',
2021
- width,
2022
- height,
2023
- },
2024
- imagine_type: 'IMAGE',
2025
- status: {
2026
- status,
2027
- update_text,
2028
- },
2029
- __typename: 'GenAIImaginePrimitive',
2030
- })
2031
- );
2141
+ const buildCard = instant !== 'only';
2032
2142
 
2033
- const submessage = {
2034
- messageType: 1,
2035
- gridImageMetadata: {
2036
- gridImageUrl: {
2037
- imagePreviewUrl: list[0]?.imagePreviewUrl,
2143
+ if (buildCard) {
2144
+ this._submessages.push({
2145
+ messageType: 1,
2146
+ gridImageMetadata: {
2147
+ gridImageUrl: {
2148
+ imagePreviewUrl: list[0]?.imagePreviewUrl,
2149
+ },
2150
+ imageUrls: list,
2038
2151
  },
2039
- imageUrls: list,
2040
- },
2041
- };
2042
-
2043
- if (id && sections.length !== 1) {
2044
- throw new Error('Cannot assign one id to multiple image sections');
2152
+ });
2045
2153
  }
2046
2154
 
2047
- return this._addContent(sections, submessage, {
2048
- id,
2049
- replace,
2050
- insertAt,
2051
- });
2052
- }
2155
+ list.forEach(({ imagePreviewUrl }) => {
2156
+ if (buildCard) {
2157
+ this._sections.push(
2158
+ AIRich.newLayout('Single', {
2159
+ media: {
2160
+ url: imagePreviewUrl,
2161
+ mime_type: 'image/png',
2162
+ },
2163
+ imagine_type: 'IMAGE',
2164
+ status: { status: 'READY' },
2165
+ __typename: 'GenAIImaginePrimitive',
2166
+ })
2167
+ );
2168
+ }
2053
2169
 
2054
- addVideo(videoUrl, { autoFill = true, status = 'READY', estimatedTime, id, replace, insertAt } = {}) {
2055
- const isObjectVideo = (v) => v && typeof v === 'object' && !Array.isArray(v) && v.url;
2170
+ if (instant) {
2171
+ this._inlineImages.push({ url: imagePreviewUrl, caption: undefined });
2172
+ }
2173
+ });
2174
+
2175
+ return this;
2176
+ }
2177
+
2178
+ // Vanz@Fix 15-08-26 (bug 41) --- addImage() only builds GRID_IMAGE (messageType 1).
2179
+ // There was no helper for standalone INLINE_IMAGE (messageType 3): callers were manually
2180
+ // pushing addSubmessage() (correct proto shape) + addSection() (WRONG shape — reused the
2181
+ // GRID_IMAGE/GenAIImaginePrimitive section schema instead of GenAIInlineImageUXPrimitive),
2182
+ // which broke client-side unifiedResponse rendering even though the submessage itself was fine.
2183
+ // Mirrors RichSubMessageType.INLINE_IMAGE handling in rich-message-utils.js's toUnified().
2184
+ /** 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). */
2185
+ addInlineImage(imageUrl, { text = '', alignment = 'center', tapLinkUrl = '', resolveUrl = false } = {}) {
2186
+ if (!(typeof imageUrl === 'string' || Buffer.isBuffer(imageUrl) || (imageUrl && typeof imageUrl === 'object'))) {
2187
+ throw new TypeError('imageUrl must be string | buffer | { imagePreviewUrl, imageHighResUrl, sourceUrl }');
2188
+ }
2189
+
2190
+ const ALIGNMENT_ENUM = { leading: 0, trailing: 1, center: 2 };
2191
+ const ALIGNMENT_NAME = ['AI_RICH_RESPONSE_IMAGE_LAYOUT_LEADING_ALIGNED', 'AI_RICH_RESPONSE_IMAGE_LAYOUT_TRAILING_ALIGNED', 'AI_RICH_RESPONSE_IMAGE_LAYOUT_CENTER_ALIGNED'];
2192
+ const alignmentNum = typeof alignment === 'number' ? alignment : (ALIGNMENT_ENUM[String(alignment).toLowerCase()] ?? ALIGNMENT_ENUM.center);
2193
+
2194
+ const url =
2195
+ imageUrl && typeof imageUrl === 'object'
2196
+ ? {
2197
+ imagePreviewUrl: imageUrl.imagePreviewUrl || imageUrl.url,
2198
+ imageHighResUrl: imageUrl.imageHighResUrl || imageUrl.url,
2199
+ sourceUrl: imageUrl.sourceUrl || imageUrl.url,
2200
+ }
2201
+ : (() => {
2202
+ const resolved = Toolkit.resolveMedia(this.#client, imageUrl, 'image', { resolveUrl });
2203
+ return { imagePreviewUrl: resolved, imageHighResUrl: resolved, sourceUrl: resolved };
2204
+ })();
2205
+
2206
+ this._submessages.push({
2207
+ messageType: 3,
2208
+ imageMetadata: {
2209
+ imageUrl: url,
2210
+ imageText: text,
2211
+ alignment: alignmentNum,
2212
+ tapLinkUrl,
2213
+ },
2214
+ });
2215
+
2216
+ this._sections.push(
2217
+ AIRich.newLayout('Single', {
2218
+ image_url: {
2219
+ image_preview_url: url.imagePreviewUrl || '',
2220
+ image_high_res_url: url.imageHighResUrl || '',
2221
+ source_url: url.sourceUrl || '',
2222
+ },
2223
+ image_text: text,
2224
+ alignment: ALIGNMENT_NAME[alignmentNum],
2225
+ tap_link_url: tapLinkUrl,
2226
+ __typename: 'GenAIInlineImageUXPrimitive',
2227
+ })
2228
+ );
2229
+
2230
+ // Vanz@Fix (bug 42): stash for the imageMessage fallback in send()
2231
+ this._inlineImages.push({
2232
+ url: url.sourceUrl || url.imageHighResUrl || url.imagePreviewUrl,
2233
+ caption: text || undefined,
2234
+ });
2235
+
2236
+ return this;
2237
+ }
2238
+
2239
+ // Vanz@Perf 15-08-26 --- autoFill defaults to false (arslan-baileys behavior): skips the
2240
+ // fetch-full-video + ffmpeg-frame-extraction + duration-parse round trip per video, which
2241
+ // was the main source of blurose's slower response time. Pass { autoFill: true } to opt
2242
+ // back into the complete/slow path (real thumbnail + duration + file_length).
2243
+ // Vanz@Fix 23-08-26 --- addVideo() had no resolveUrl option at all (unlike addImage()), so the
2244
+ // video url always stayed a raw external link, which stock WA clients show a "download" state
2245
+ // for before rendering. Mirrors addImage()'s { resolveUrl } — when true, the url is uploaded to
2246
+ // WA's own media server first via Toolkit.toUrl() so it renders instantly like WA-native media.
2247
+ /** Add a video block. */
2248
+ addVideo(videoUrl, { autoFill = false, resolveUrl = false } = {}) {
2249
+ const isObjectVideo = (v) => v && typeof v === 'object' && v.url;
2056
2250
 
2057
2251
  const isValidPrimitive =
2058
2252
  typeof videoUrl === 'string' ||
@@ -2066,15 +2260,17 @@ class AIRich extends BaseBuilder {
2066
2260
 
2067
2261
  const items = Array.isArray(videoUrl) ? videoUrl : [videoUrl];
2068
2262
 
2069
- const alert = this.createAlert('GenAIImaginePrimitive (ANIMATE)');
2070
-
2071
- const sections = [];
2072
- const submessages = [];
2263
+ this._submessages.push({
2264
+ messageType: 2,
2265
+ messageText: '[ Video tidak dapat dimuat ]',
2266
+ });
2073
2267
 
2074
- for (const item of items) {
2268
+ items.forEach((item) => {
2075
2269
  const isObject = isObjectVideo(item);
2076
2270
 
2077
- const url = isObject ? Toolkit.resolveMedia(this.#client, item.url ?? '', 'video') : Toolkit.resolveMedia(this.#client, item, 'video');
2271
+ const url = isObject
2272
+ ? Toolkit.resolveMedia(this.#client, item.url ?? '', 'video', { resolveUrl })
2273
+ : Toolkit.resolveMedia(this.#client, item, 'video', { resolveUrl });
2078
2274
 
2079
2275
  const bufferPromise = autoFill ? Promise.resolve(url).then((u) => Toolkit.fetchBuffer(u)) : null;
2080
2276
 
@@ -2100,15 +2296,17 @@ class AIRich extends BaseBuilder {
2100
2296
  height: 300,
2101
2297
  })
2102
2298
  : autoFill
2103
- ? bufferPromise?.then((b) =>
2104
- Toolkit.getMp4Preview(b, {
2105
- time: 0,
2106
- result: 'base64',
2107
- })
2108
- )
2299
+ ? bufferPromise
2300
+ ? bufferPromise.then((b) =>
2301
+ Toolkit.getMp4Preview(b, {
2302
+ time: 0,
2303
+ result: 'base64',
2304
+ })
2305
+ )
2306
+ : null
2109
2307
  : null;
2110
2308
 
2111
- sections.push(
2309
+ this._sections.push(
2112
2310
  AIRich.newLayout('Single', {
2113
2311
  media: {
2114
2312
  url,
@@ -2117,38 +2315,35 @@ class AIRich extends BaseBuilder {
2117
2315
  duration,
2118
2316
  },
2119
2317
  imagine_type: 'ANIMATE',
2120
- status: {
2121
- status,
2122
- estimated_completion_time: estimatedTime != null ? Math.floor((Date.now() + estimatedTime) / 1000) : undefined,
2123
- },
2318
+ status: { status: 'READY' },
2124
2319
  thumbnail: {
2125
2320
  raw_media: thumbnail,
2126
2321
  },
2127
2322
  __typename: 'GenAIImaginePrimitive',
2128
2323
  })
2129
2324
  );
2130
- }
2131
-
2132
- if (alert !== undefined) {
2133
- submessages.push(alert);
2134
- }
2135
-
2136
- if (submessages.length > 1) {
2137
- throw new Error('Video content can only have one submessage');
2138
- }
2139
-
2140
- return this._addContent(sections, submessages[0], {
2141
- id,
2142
- replace,
2143
- insertAt,
2144
2325
  });
2326
+
2327
+ return this;
2145
2328
  }
2146
2329
 
2147
- addProduct(data = {}, { id, replace, insertAt } = {}) {
2330
+ /** Add an inline product card (or array of cards). Each item needs at least a `title`. */
2331
+ addProduct(data = {}, { resolveUrl = false } = {}) {
2148
2332
  if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
2149
2333
  throw new TypeError('Product items must be an object or an array of objects');
2150
2334
  }
2151
2335
 
2336
+ const itemsToCheck = Array.isArray(data) ? data : [data];
2337
+ const missingTitleAt = itemsToCheck.findIndex((item) => !item.title);
2338
+ if (missingTitleAt !== -1) {
2339
+ throw new TypeError(`addProduct() item[${missingTitleAt}] is missing a required "title"`);
2340
+ }
2341
+
2342
+ this._submessages.push({
2343
+ messageType: 2,
2344
+ messageText: '[ Produk tidak dapat dimuat ]',
2345
+ });
2346
+
2152
2347
  const items = Array.isArray(data) ? data : [data];
2153
2348
 
2154
2349
  const product = items.map((item) => ({
@@ -2158,41 +2353,41 @@ class AIRich extends BaseBuilder {
2158
2353
  sale_price: item.sale_price,
2159
2354
  product_url: item.product_url ?? item.url,
2160
2355
  image: {
2161
- url: Toolkit.resolveMedia(this.#client, item.image_url ?? item.image, 'image'),
2356
+ url: Toolkit.resolveMedia(this.#client, item.image_url ?? item.image, 'image', { resolveUrl }),
2162
2357
  },
2163
2358
  additional_images: [
2164
2359
  {
2165
- url: Toolkit.resolveMedia(this.#client, item.icon_url ?? item.icon, 'image'),
2360
+ url: Toolkit.resolveMedia(this.#client, item.icon_url ?? item.icon, 'image', { resolveUrl }),
2166
2361
  },
2167
2362
  ],
2168
2363
  __typename: 'GenAIProductItemCardPrimitive',
2169
2364
  }));
2170
2365
 
2171
- const section = AIRich.newLayout(Array.isArray(data) ? 'HScroll' : 'Single', Array.isArray(data) ? product : product[0]);
2366
+ this._sections.push(AIRich.newLayout(Array.isArray(data) ? 'HScroll' : 'Single', Array.isArray(data) ? product : product[0]));
2172
2367
 
2173
- const submessage = this.createAlert('GenAIProductItemCardPrimitive');
2174
-
2175
- return this._addContent(section, submessage, {
2176
- id,
2177
- replace,
2178
- insertAt,
2179
- });
2368
+ return this;
2180
2369
  }
2181
2370
 
2182
- addPost(data = {}, { id, replace, insertAt } = {}) {
2371
+ /** Add an inline social-post style card (or array of cards). */
2372
+ addPost(data = {}, { resolveUrl = false } = {}) {
2183
2373
  if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
2184
2374
  throw new TypeError('Post items must be an object or an array of objects');
2185
2375
  }
2186
2376
 
2187
2377
  const posts = Array.isArray(data) ? data : [data];
2188
2378
 
2379
+ this._submessages.push({
2380
+ messageType: 2,
2381
+ messageText: '[ Postingan tidak dapat dimuat ]',
2382
+ });
2383
+
2189
2384
  const primitives = posts.map((p) => ({
2190
2385
  title: p.title ?? '',
2191
2386
  subtitle: p.subtitle ?? '',
2192
2387
  username: p.username ?? '',
2193
- profile_picture_url: Toolkit.resolveMedia(this.#client, p.profile_picture_url ?? p.profile_url ?? p.profile ?? '', 'image'),
2388
+ profile_picture_url: Toolkit.resolveMedia(this.#client, p.profile_picture_url ?? p.profile_url ?? p.profile ?? '', 'image', { resolveUrl }),
2194
2389
  is_verified: !!(p.is_verified || p.verified),
2195
- thumbnail_url: Toolkit.resolveMedia(this.#client, p.thumbnail_url ?? p.thumbnail ?? '', 'image'),
2390
+ thumbnail_url: Toolkit.resolveMedia(this.#client, p.thumbnail_url ?? p.thumbnail ?? '', 'image', { resolveUrl }),
2196
2391
  post_caption: p.post_caption ?? p.caption ?? '',
2197
2392
  likes_count: p.likes_count ?? p.like ?? 0,
2198
2393
  comments_count: p.comments_count ?? p.comment ?? 0,
@@ -2201,151 +2396,419 @@ class AIRich extends BaseBuilder {
2201
2396
  post_deeplink: p.post_deeplink ?? p.deeplink ?? '',
2202
2397
  source_app: p.source_app || p.source || 'INSTAGRAM',
2203
2398
  footer_label: p.footer_label ?? p.footer ?? '',
2204
- footer_icon: Toolkit.resolveMedia(this.#client, p.footer_icon ?? p.icon ?? '', 'image'),
2399
+ footer_icon: Toolkit.resolveMedia(this.#client, p.footer_icon ?? p.icon ?? '', 'image', { resolveUrl }),
2205
2400
  is_carousel: posts.length > 1,
2206
2401
  orientation: p.orientation ?? 'LANDSCAPE',
2207
2402
  post_type: p.post_type ?? 'VIDEO',
2208
2403
  __typename: 'GenAIPostPrimitive',
2209
2404
  }));
2210
2405
 
2211
- const section = AIRich.newLayout('HScroll', primitives);
2406
+ this._sections.push(AIRich.newLayout('HScroll', primitives));
2212
2407
 
2213
- const submessage = this.createAlert('GenAIPostPrimitive');
2408
+ return this;
2409
+ }
2214
2410
 
2215
- return this._addContent(section, submessage, {
2216
- id,
2217
- replace,
2218
- insertAt,
2219
- });
2411
+ // Vanz@Add 24-08-26 --- ported from temen's MessageBuilderV4.7 (setResponseId/setBotResponseId/
2412
+ // refreshResponseId/refreshBotResponseId), rewritten for this fork. Pins the two ids build()
2413
+ // generates (see constructor comment) so a rebuilt message can reuse the same response_id/
2414
+ // botResponseId — needed for editing an already-sent AIRich message in place.
2415
+
2416
+ /** Pin `unifiedResponse.response_id` to a specific value instead of a fresh random one each build() — needed to re-send an edited version of an already-sent message in place. */
2417
+ setResponseId(id) {
2418
+ if (typeof id !== 'string' || !id) throw new TypeError('setResponseId(id) requires a non-empty string');
2419
+ this._responseId = id;
2420
+ return this;
2220
2421
  }
2221
2422
 
2222
- addMetadata(text, { id, replace, insertAt } = {}) {
2223
- if (typeof text !== 'string') {
2224
- throw new TypeError('Text must be a string');
2225
- }
2423
+ /** Un-pin `unifiedResponse.response_id`, generating a fresh crypto.randomUUID() immediately (not deferred to the next build()). */
2424
+ refreshResponseId() {
2425
+ this._responseId = crypto.randomUUID();
2426
+ return this;
2427
+ }
2226
2428
 
2227
- const section = AIRich.newLayout('Single', {
2228
- text,
2229
- __typename: 'GenAIMetadataTextPrimitive',
2230
- });
2429
+ /** Pin `botMetadata.botResponseId` to a specific value instead of a fresh random one each build(). */
2430
+ setBotResponseId(id) {
2431
+ if (typeof id !== 'string' || !id) throw new TypeError('setBotResponseId(id) requires a non-empty string');
2432
+ this._botResponseId = id;
2433
+ return this;
2434
+ }
2435
+
2436
+ /** Un-pin `botMetadata.botResponseId`, generating a fresh crypto.randomUUID() immediately. */
2437
+ refreshBotResponseId() {
2438
+ this._botResponseId = crypto.randomUUID();
2439
+ return this;
2440
+ }
2441
+
2442
+ /** Add a small metadata-style text line (`GenAIMetadataTextPrimitive`) — same visual style as the auto-appended footer/`addTip()`'s callout, but insertable anywhere and without `addTip()`'s icon prefix. */
2443
+ addMetadata(text) {
2444
+ if (typeof text !== 'string' || !text) throw new TypeError('addMetadata(text) requires a non-empty string');
2231
2445
 
2232
- const submessage = {
2446
+ this._submessages.push({
2233
2447
  messageType: 2,
2234
2448
  messageText: text,
2235
- };
2236
-
2237
- return this._addContent(section, submessage, {
2238
- id,
2239
- replace,
2240
- insertAt,
2241
2449
  });
2450
+
2451
+ this._sections.push(
2452
+ AIRich.newLayout('Single', {
2453
+ text,
2454
+ __typename: 'GenAIMetadataTextPrimitive',
2455
+ })
2456
+ );
2457
+
2458
+ return this;
2242
2459
  }
2243
2460
 
2244
- addTip(text, { id, replace, insertAt } = {}) {
2245
- if (typeof text !== 'string') {
2246
- throw new TypeError('Text must be a string');
2461
+ /** Add a small "tip" callout banner. @param {string} text */
2462
+ addTip(text) {
2463
+ if (typeof text !== 'string' || !text) {
2464
+ throw new TypeError('addTip(text) requires a non-empty string');
2247
2465
  }
2248
2466
 
2249
- const section = AIRich.newLayout('Single', {
2250
- text: 'ⓘ ' + text,
2251
- __typename: 'GenAIMetadataTextPrimitive',
2252
- });
2253
-
2254
- const submessage = {
2467
+ this._submessages.push({
2255
2468
  messageType: 2,
2256
2469
  messageText: text,
2257
- };
2258
-
2259
- return this._addContent(section, submessage, {
2260
- id,
2261
- replace,
2262
- insertAt,
2263
2470
  });
2471
+
2472
+ this._sections.push(
2473
+ AIRich.newLayout('Single', {
2474
+ text,
2475
+ __typename: 'GenAIMetadataTextPrimitive',
2476
+ })
2477
+ );
2478
+
2479
+ return this;
2264
2480
  }
2265
2481
 
2266
- addWidget(data, { layout, id, replace, insertAt, ...options } = {}) {
2267
- if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
2268
- throw new TypeError('Widget must be an object or an array of objects');
2482
+ // Vanz@Add 22-08-26 (v4.7) --- addHeading/addImageCard/addWidget/addFooterAction: 4 primitives
2483
+ // reverse-engineered from captured Meta-AI-in-WhatsApp traffic that this project's own crm/snip
2484
+ // tooling (see rich-message-utils.js) dumps for study. Not in any public Baileys schema, so
2485
+ // unknown enum values (kind/state on addWidget's ctas) are passed through as observed rather
2486
+ // than guessed at, and documented as experimental below.
2487
+
2488
+ /** Add a large heading-style text block (`FOATextPrimitive`) — visually distinct from `addText()`'s regular paragraph text. */
2489
+ addHeading(text) {
2490
+ if (typeof text !== 'string' || !text) {
2491
+ throw new TypeError('addHeading(text) requires a non-empty string');
2269
2492
  }
2270
2493
 
2271
- const isArray = Array.isArray(data);
2494
+ this._submessages.push({
2495
+ messageType: 2,
2496
+ messageText: text,
2497
+ });
2272
2498
 
2273
- const items = isArray ? data : [data];
2499
+ this._sections.push(
2500
+ AIRich.newLayout('Single', {
2501
+ text,
2502
+ __typename: 'FOATextPrimitive',
2503
+ })
2504
+ );
2274
2505
 
2275
- const widgets = items.map((item) => ({
2276
- __typename: 'GenAI3PExtWidgetPrimitive',
2506
+ return this;
2507
+ }
2277
2508
 
2278
- header: {
2279
- __typename: 'GenAI3PExtWidgetStandardHeader',
2280
- title: item.title ?? '',
2281
- ...(item.header ?? {}),
2282
- },
2509
+ /**
2510
+ * Add a "ready" static image card (`GenAIImagePrimitive`: preview + full-res, no generating/status
2511
+ * state) distinct from `addImage()`'s AI-generation-style `GenAIImaginePrimitive`.
2512
+ * @param {string|Buffer} previewUrl Preview/thumbnail image.
2513
+ * @param {string|Buffer} [fullUrl] Full-resolution image; defaults to `previewUrl`.
2514
+ */
2515
+ addImageCard(previewUrl, fullUrl = previewUrl, { resolveUrl = false } = {}) {
2516
+ if (!(typeof previewUrl === 'string' || Buffer.isBuffer(previewUrl))) {
2517
+ throw new TypeError('addImageCard(previewUrl) requires a string url or buffer');
2518
+ }
2283
2519
 
2284
- body: {
2285
- __typename: 'GenAI3PExtCalendarEventList',
2286
- sections: item.sections ?? [],
2287
-
2288
- ctas: (item.actions ?? []).map((action) => ({
2289
- __typename: 'GenAI3PExtWidgetCTA',
2290
- label: action.label ?? '',
2291
- state: action.state ?? 'PENDING',
2292
- kind: action.kind ?? 'OTHER',
2293
- tool_call_id: action.tool_call_id ?? action.id ?? '',
2294
-
2295
- ...(action.toast && {
2296
- toast: {
2297
- __typename: 'GenAI3PExtWidgetToast',
2298
- label: action.toast.label ?? action.label ?? '',
2299
- },
2300
- }),
2301
- })),
2520
+ const preview = Toolkit.resolveMedia(this.#client, previewUrl, 'image', { resolveUrl });
2521
+ const full = fullUrl === previewUrl ? preview : Toolkit.resolveMedia(this.#client, fullUrl, 'image', { resolveUrl });
2302
2522
 
2303
- ...(item.body ?? {}),
2523
+ this._submessages.push({
2524
+ messageType: 1,
2525
+ gridImageMetadata: {
2526
+ gridImageUrl: { imagePreviewUrl: preview },
2527
+ imageUrls: [{ imagePreviewUrl: preview, imageHighResUrl: full, sourceUrl: full }],
2304
2528
  },
2305
- }));
2529
+ });
2306
2530
 
2307
- const section = AIRich.newLayout(layout ?? (isArray ? 'HScroll' : 'Single'), isArray ? widgets : widgets[0], options);
2531
+ this._sections.push(
2532
+ AIRich.newLayout('Single', {
2533
+ preview_image: { url: preview, mime_type: 'image/jpeg', __typename: 'GenAIMediaItem' },
2534
+ full_image: { url: full, mime_type: 'image/jpeg', __typename: 'GenAIMediaItem' },
2535
+ __typename: 'GenAIImagePrimitive',
2536
+ })
2537
+ );
2308
2538
 
2309
- const submessage = this.createAlert('GenAI3PExtWidgetStandardHeader');
2539
+ return this;
2540
+ }
2541
+
2542
+ /**
2543
+ * Add a "3P extension" widget card (`GenAI3PExtWidgetPrimitive`) — a small panel with a title and
2544
+ * a row of tappable CTA chips. Per captured traffic these CTAs call back into a tool (`tool_call_id`)
2545
+ * rather than opening a url; `kind`/`state` semantics beyond the observed `'OTHER'`/`'PENDING'`
2546
+ * defaults aren't publicly documented, so treat this as experimental.
2547
+ *
2548
+ * Vanz@Add (v4.8) --- accepts an `{ layout }` override so consecutive `addWidget()` calls can
2549
+ * pick different renderings (e.g. one `HScroll` row, one `ActionRow` stack) instead of always
2550
+ * inferring HScroll-for-array/Single-for-object from the shape of `data`. Also accepts either
2551
+ * `ctas` (original key, matches the wire field) or `actions` (alias) on each item — whichever
2552
+ * is present is used; `ctas` wins if both are somehow given.
2553
+ * @param {Record<string, any>|Record<string, any>[]} data `{ title, ctas|actions: [{ label, tool_call_id?, kind?, state?, toast? }] }` (single or array).
2554
+ * @param {{layout?: 'Single'|'HScroll'|'ActionRow'|string}} [options] `layout` overrides the default single/array inference.
2555
+ */
2556
+ addWidget(data = {}, { layout } = {}) {
2557
+ const items = Array.isArray(data) ? data : [data];
2310
2558
 
2311
- return this._addContent(section, submessage, {
2312
- id,
2313
- replace,
2314
- insertAt,
2559
+ items.forEach((item, i) => {
2560
+ if (!item?.title) {
2561
+ throw new TypeError(`addWidget() item[${i}] is missing a required "title"`);
2562
+ }
2563
+ const ctas = item.ctas ?? item.actions;
2564
+ if (!Array.isArray(ctas) || !ctas.length) {
2565
+ throw new TypeError(`addWidget() item[${i}] requires a non-empty "ctas" (or "actions") array`);
2566
+ }
2315
2567
  });
2316
- }
2317
2568
 
2318
- addFooterAction(data, { layout, id, replace, insertAt, ...options } = {}) {
2319
- if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
2320
- throw new TypeError('Footer action must be an object or an array of objects');
2321
- }
2569
+ this._submessages.push({
2570
+ messageType: 2,
2571
+ messageText: items.map((item) => item.title).join(', '),
2572
+ });
2573
+
2574
+ const widgets = items.map((item) => {
2575
+ const ctas = item.ctas ?? item.actions;
2576
+ return {
2577
+ header: { title: item.title, __typename: 'GenAI3PExtWidgetStandardHeader' },
2578
+ body: {
2579
+ sections: item.sections ?? [],
2580
+ ctas: ctas.map((cta, idx) => ({
2581
+ label: cta.label ?? '',
2582
+ state: cta.state ?? 'PENDING',
2583
+ kind: cta.kind ?? 'OTHER',
2584
+ tool_call_id: cta.tool_call_id ?? String(idx).padStart(2, '0'),
2585
+ ...(cta.toast !== false && {
2586
+ toast: { label: typeof cta.toast === 'string' ? cta.toast : item.title, __typename: 'GenAI3PExtWidgetToast' },
2587
+ }),
2588
+ __typename: 'GenAI3PExtWidgetCTA',
2589
+ })),
2590
+ __typename: item.body_typename ?? 'GenAI3PExtCalendarEventList',
2591
+ },
2592
+ __typename: 'GenAI3PExtWidgetPrimitive',
2593
+ };
2594
+ });
2322
2595
 
2323
- const isArray = Array.isArray(data);
2596
+ const resolvedLayout = layout ?? (Array.isArray(data) ? 'HScroll' : 'Single');
2597
+ const asArray = resolvedLayout !== 'Single';
2324
2598
 
2325
- const items = isArray ? data : [data];
2599
+ this._sections.push(AIRich.newLayout(resolvedLayout, asArray ? widgets : widgets[0]));
2326
2600
 
2327
- const actions = items.map((item) => ({
2328
- __typename: 'GenAIFooterActionPrimitive',
2601
+ return this;
2602
+ }
2329
2603
 
2330
- cta_text: item.text ?? item.cta_text ?? '',
2604
+ /**
2605
+ * Add footer action link(s) (`GenAIFooterActionPrimitive`) — e.g. "Join our WhatsApp Group/Channel"
2606
+ * chips shown below the response, separate from `setFooter()`'s plain text footer.
2607
+ * @param {{text: string, url: string, type?: string}|{text: string, url: string, type?: string}[]} actions
2608
+ */
2609
+ addFooterAction(actions) {
2610
+ const items = Array.isArray(actions) ? actions : [actions];
2331
2611
 
2332
- cta_type: item.type ?? item.cta_type ?? 'OPEN_URL',
2612
+ items.forEach((item, i) => {
2613
+ if (!item?.text || !item?.url) {
2614
+ throw new TypeError(`addFooterAction() item[${i}] requires both "text" and "url"`);
2615
+ }
2616
+ });
2333
2617
 
2334
- cta_url: item.url ?? item.cta_url ?? '',
2618
+ const primitives = items.map((item) => ({
2619
+ cta_text: item.text,
2620
+ cta_type: item.type ?? 'OPEN_URL',
2621
+ cta_url: item.url,
2622
+ __typename: 'GenAIFooterActionPrimitive',
2335
2623
  }));
2336
2624
 
2337
- const section = AIRich.newLayout(layout ?? (isArray ? 'HScroll' : 'Single'), isArray ? actions : actions[0], options);
2625
+ this._sections.push(AIRich.newLayout('HScroll', primitives));
2626
+
2627
+ return this;
2628
+ }
2629
+
2630
+ // Vanz@Add (v4.8) --- 8 primitives from the 20-item reference test script that had no
2631
+ // add*() helper yet (Divider/Spacer/Task/ProgressStatus/ThinkingStatus/QuotaUpsell/FOABloks
2632
+ // have no dedicated AIRichResponseSubMessageType — WA carries them purely in the
2633
+ // unifiedResponse view-model JSON, so their submessage falls back to plain AI_RICH_RESPONSE_TEXT
2634
+ // like addTip/addHeading already do. Latex is the one exception: it has a real proto type
2635
+ // (AI_RICH_RESPONSE_LATEX = 8, confirmed in WAProto) with its own latexMetadata, so that one
2636
+ // gets a proper submessage instead of the text fallback.
2637
+
2638
+ /** Add a plain horizontal divider line (`GenAIDividerPrimitive`, no content). */
2639
+ addDivider() {
2640
+ this._submessages.push({ messageType: 2, messageText: '---' });
2641
+ this._sections.push(AIRich.newLayout('Single', { __typename: 'GenAIDividerPrimitive' }));
2642
+ return this;
2643
+ }
2338
2644
 
2339
- const submessage = this.createAlert('GenAIFooterActionPrimitive');
2645
+ /** Add blank vertical spacing (`GenAISpacerPrimitive`). @param {number} [spacing=1] Spacing unit, per observed traffic. */
2646
+ addSpacer(spacing = 1) {
2647
+ if (typeof spacing !== 'number' || spacing < 0) {
2648
+ throw new TypeError('addSpacer(spacing) requires a non-negative number');
2649
+ }
2650
+ this._submessages.push({ messageType: 2, messageText: `spasi ${spacing}` });
2651
+ this._sections.push(AIRich.newLayout('Single', { spacing, __typename: 'GenAISpacerPrimitive' }));
2652
+ return this;
2653
+ }
2340
2654
 
2341
- return this._addContent(section, submessage, {
2342
- id,
2343
- replace,
2344
- insertAt,
2655
+ /**
2656
+ * Add a rendered LaTeX expression (`GenAILatexUXPrimitive`), with a real `AI_RICH_RESPONSE_LATEX`
2657
+ * submessage (unlike most primitives in this block, this one has a proper proto type).
2658
+ * @param {string} expression LaTeX source, e.g. `'$$E = mc^2$$'`.
2659
+ */
2660
+ addLatex(expression) {
2661
+ if (typeof expression !== 'string' || !expression) {
2662
+ throw new TypeError('addLatex(expression) requires a non-empty string');
2663
+ }
2664
+ this._submessages.push({
2665
+ messageType: 8,
2666
+ latexMetadata: { text: expression, expressions: [{ latexExpression: expression }] },
2345
2667
  });
2668
+ this._sections.push(AIRich.newLayout('Single', { latex_expression: expression, __typename: 'GenAILatexUXPrimitive' }));
2669
+ return this;
2670
+ }
2671
+
2672
+ /**
2673
+ * Add a task/checklist card (`GenAITaskPrimitive`).
2674
+ * @param {{task_id?: string, title: string, subtitle?: string, status?: string}} data
2675
+ */
2676
+ addTask(data = {}) {
2677
+ if (!data?.title) {
2678
+ throw new TypeError('addTask() requires a "title"');
2679
+ }
2680
+ this._submessages.push({ messageType: 2, messageText: `Tugas: ${data.title}` });
2681
+ this._sections.push(
2682
+ AIRich.newLayout('Single', {
2683
+ task_id: data.task_id ?? '',
2684
+ title: data.title,
2685
+ subtitle: data.subtitle ?? '',
2686
+ status: data.status ?? 'IN_PROGRESS',
2687
+ __typename: 'GenAITaskPrimitive',
2688
+ })
2689
+ );
2690
+ // Safety net: GenAITaskPrimitive is a custom AI-only component the stock WA client
2691
+ // doesn't render visibly. Append a plain text section so the task is still visible.
2692
+ // Set data.textFallback = false to skip.
2693
+ if (data.textFallback !== false) {
2694
+ const fallbackText = data.subtitle ? `${data.title} — ${data.subtitle}` : data.title;
2695
+ this._sections.push(AIRich.newLayout('Single', { text: `Tugas: ${fallbackText}`, __typename: 'FOATextPrimitive' }));
2696
+ }
2697
+ return this;
2698
+ }
2699
+
2700
+ /**
2701
+ * Add a "searching/working" progress banner (`GenAIBotProgressStatusPrimitive`) — a one-shot
2702
+ * status chip (unlike `addSuggest`, this isn't tappable). Distinct from `addThinkingStatus()`'s
2703
+ * icon/typename.
2704
+ * @param {string} title
2705
+ * @param {{icon?: string, is_in_progress?: boolean}} [options]
2706
+ */
2707
+ addProgressStatus(title, { icon = 'SEARCH', is_in_progress = true, target_secondary_screen_id, target_secondary_screen_tab_id } = {}) {
2708
+ if (typeof title !== 'string' || !title) {
2709
+ throw new TypeError('addProgressStatus(title) requires a non-empty string');
2710
+ }
2711
+ this._submessages.push({ messageType: 2, messageText: title });
2712
+ const primitive = {
2713
+ title,
2714
+ icon,
2715
+ is_in_progress,
2716
+ meta_search_apps: [],
2717
+ __typename: 'GenAIBotProgressStatusPrimitive',
2718
+ };
2719
+ // NOTE: these two fields must be OMITTED when unset, not sent as `null` —
2720
+ // an explicit null here was reproducibly crashing the WA client renderer
2721
+ // on group-open/media-download. Only include when the caller actually passes one.
2722
+ if (target_secondary_screen_id != null) primitive.target_secondary_screen_id = target_secondary_screen_id;
2723
+ if (target_secondary_screen_tab_id != null) primitive.target_secondary_screen_tab_id = target_secondary_screen_tab_id;
2724
+ this._sections.push(AIRich.newLayout('Single', primitive));
2725
+ return this;
2726
+ }
2727
+
2728
+ /** Add a "thinking" status banner (`GenAIBotThinkingStatusPrimitive`). See `addProgressStatus()`. */
2729
+ addThinkingStatus(title, { icon = 'THINKING', is_in_progress = true, target_secondary_screen_id, target_secondary_screen_tab_id, textFallback = true } = {}) {
2730
+ if (typeof title !== 'string' || !title) {
2731
+ throw new TypeError('addThinkingStatus(title) requires a non-empty string');
2732
+ }
2733
+ this._submessages.push({ messageType: 2, messageText: title });
2734
+ const primitive = {
2735
+ title,
2736
+ icon,
2737
+ is_in_progress,
2738
+ meta_search_apps: [],
2739
+ __typename: 'GenAIBotThinkingStatusPrimitive',
2740
+ };
2741
+ // Same crash-avoidance rule as addProgressStatus(): omit, never null.
2742
+ if (target_secondary_screen_id != null) primitive.target_secondary_screen_id = target_secondary_screen_id;
2743
+ if (target_secondary_screen_tab_id != null) primitive.target_secondary_screen_tab_id = target_secondary_screen_tab_id;
2744
+ this._sections.push(AIRich.newLayout('Single', primitive));
2745
+ // Safety net: stock WA client doesn't render this primitive's own view (it's meant
2746
+ // as a transient spinner in the official app), so the card shows blank when forwarded.
2747
+ // Append a plain text section so the title is still visible. Set { textFallback: false } to skip.
2748
+ if (textFallback) {
2749
+ this._sections.push(AIRich.newLayout('Single', { text: title, __typename: 'FOATextPrimitive' }));
2750
+ }
2751
+ return this;
2752
+ }
2753
+
2754
+ /**
2755
+ * Add a subscription-quota-limit upsell card (`GenAIMetaSubsQuotaUpsellPrimitive`).
2756
+ * @param {{title: string, body?: string, body_line1?: string, body_line2?: string, buttons?: {label: string, action?: string, deeplink?: string}[]}} data
2757
+ */
2758
+ addQuotaUpsell(data = {}) {
2759
+ if (!data?.title) {
2760
+ throw new TypeError('addQuotaUpsell() requires a "title"');
2761
+ }
2762
+ this._submessages.push({ messageType: 2, messageText: data.title });
2763
+ this._sections.push(
2764
+ AIRich.newLayout('Single', {
2765
+ title: data.title,
2766
+ body: data.body ?? '',
2767
+ body_line1: data.body_line1 ?? '',
2768
+ body_line2: data.body_line2 ?? '',
2769
+ buttons: (data.buttons ?? []).map((b) => ({
2770
+ label: b.label ?? '',
2771
+ action: b.action ?? 'OPEN_DEEPLINK',
2772
+ deeplink: b.deeplink ?? '',
2773
+ })),
2774
+ __typename: 'GenAIMetaSubsQuotaUpsellPrimitive',
2775
+ })
2776
+ );
2777
+ return this;
2778
+ }
2779
+
2780
+ /**
2781
+ * Add a raw Bloks payload (`FOABloksPrimitive`) — Meta's internal UI-description format.
2782
+ * Escape hatch: field meaning beyond what's passed through is undocumented, so this is the
2783
+ * most experimental primitive in this block; pass whatever your captured traffic shows.
2784
+ * @param {{type: string, data: string, uuid?: string, initial_response?: any, versioning_id?: string}} data
2785
+ */
2786
+ addBloks(data = {}) {
2787
+ if (!data?.type) {
2788
+ throw new TypeError('addBloks() requires a "type"');
2789
+ }
2790
+ this._submessages.push({ messageType: 2, messageText: 'Bloks' });
2791
+ const primitive = {
2792
+ type: data.type,
2793
+ data: data.data ?? '{}',
2794
+ uuid: data.uuid ?? '',
2795
+ versioning_id: data.versioning_id ?? '',
2796
+ __typename: 'FOABloksPrimitive',
2797
+ };
2798
+ // Omit initial_response entirely when unset — same null-field crash as addProgressStatus/addThinkingStatus.
2799
+ if (data.initial_response != null) primitive.initial_response = data.initial_response;
2800
+ this._sections.push(AIRich.newLayout('Single', primitive));
2801
+ // Safety net: FOABloksPrimitive needs a real, client-registered Bloks screen to render
2802
+ // anything — arbitrary/placeholder payloads show up blank. Append a plain text section
2803
+ // so the card isn't empty. Set data.textFallback = false to skip.
2804
+ if (data.textFallback !== false) {
2805
+ this._sections.push(AIRich.newLayout('Single', { text: `Bloks: ${data.type}`, __typename: 'FOATextPrimitive' }));
2806
+ }
2807
+ return this;
2346
2808
  }
2347
2809
 
2348
- addSuggest(suggestion, { scroll = true, layout, id, replace, insertAt } = {}) {
2810
+ /** Add tappable follow-up suggestion chips below the message. @param {string|string[]} suggestion */
2811
+ addSuggest(suggestion, { scroll = true, layout } = {}) {
2349
2812
  if (!(typeof suggestion === 'string' || (Array.isArray(suggestion) && suggestion.every((v) => typeof v === 'string')))) {
2350
2813
  throw new TypeError('Suggestion must be a string or array of strings');
2351
2814
  }
@@ -2366,28 +2829,18 @@ class AIRich extends BaseBuilder {
2366
2829
 
2367
2830
  const type = layout ?? (suggest.length === 1 ? 'Single' : scroll ? 'HScroll' : 'ActionRow');
2368
2831
 
2369
- const section = AIRich.newLayout(type, type === 'Single' ? suggest[0] : suggest, {
2370
- __typename: 'GenAIUnifiedResponseSection',
2371
- });
2372
-
2373
- const submessage = this.createAlert('GenAIFollowUpSuggestionPillPrimitive');
2832
+ this._sections.push(AIRich.newLayout(type, type === 'Single' ? suggest[0] : suggest, { __typename: 'GenAIUnifiedResponseSection' }));
2374
2833
 
2375
- return this._addContent(section, submessage, {
2376
- id,
2377
- replace,
2378
- insertAt,
2379
- });
2834
+ return this;
2380
2835
  }
2381
2836
 
2382
- async build(
2383
- jid,
2384
- { bypassDownload = true, forwarded = true, notification = false, includesUnifiedResponse = true, includesSubmessages = true, quoted, quotedParticipant, messageId, ...options } = {}
2385
- ) {
2837
+ /** @returns {Promise<Record<string, any>>} The generated AI-rich message content (without wrapping/sending it). */
2838
+ async build({ forwarded = true, notification = false, includesUnifiedResponse = true, includesSubmessages = true, quoted, quotedParticipant, ...options } = {}) {
2386
2839
  const forward = forwarded
2387
2840
  ? {
2388
2841
  forwardingScore: 1,
2389
2842
  isForwarded: true,
2390
- forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' },
2843
+ forwardedAiBotMessageInfo: { botJid: '0@bot' },
2391
2844
  forwardOrigin: 4,
2392
2845
  }
2393
2846
  : {};
@@ -2405,7 +2858,7 @@ class AIRich extends BaseBuilder {
2405
2858
  const qObj = quoted
2406
2859
  ? {
2407
2860
  stanzaId: quoted?.key?.id || quoted?.id,
2408
- participant: quotedParticipant || quoted?.key?.participant || quoted?.participant || quoted?.key?.remoteJid,
2861
+ participant: quotedParticipant || quoted?.key?.participant || quoted?.key?.remoteJid,
2409
2862
  quotedType: 0,
2410
2863
  quotedMessage: typeof quoted === 'object' && quoted !== null ? (quoted.message ?? quoted) : undefined,
2411
2864
  }
@@ -2421,88 +2874,140 @@ class AIRich extends BaseBuilder {
2421
2874
  ]
2422
2875
  : [...(await waitAllPromises(this._sections))];
2423
2876
 
2424
- if (this._dynamic) {
2425
- this.refreshResponseId();
2426
- this.refreshBotResponseId();
2427
- }
2877
+ // Vanz@Merge 15-08-26 --- Neither blurose nor arslan sign the bot metadata with
2878
+ // verificationMetadata (proofs/certificateChain). Backported from this project's own
2879
+ // rich-message-utils.js botMetadataSignature/botMetadataCertificate helpers, plus a
2880
+ // botResponseId tying the signed metadata to unifiedResponse.response_id.
2881
+ // Vanz@Fix 24-08-26 --- was `const responseId = crypto.randomUUID()` shared for BOTH
2882
+ // unifiedResponse.response_id and botMetadata.botResponseId, generated fresh every build()
2883
+ // with no override. Now each has its own id, pinned via setResponseId()/setBotResponseId()
2884
+ // if the caller set one (for sendEdit()-style in-place message updates), otherwise still
2885
+ // defaults to a fresh randomUUID() per build() exactly like before.
2886
+ const responseId = this._responseId ?? crypto.randomUUID();
2887
+ const botResponseId = this._botResponseId ?? crypto.randomUUID();
2428
2888
 
2429
- return generateWAMessageFromContent(
2430
- jid,
2431
- {
2432
- messageContextInfo: {
2433
- deviceListMetadata: {},
2434
- deviceListMetadataVersion: 2,
2435
- botMetadata: {
2436
- messageDisclaimerText: this._title,
2437
- ...notif,
2438
- verificationMetadata: AIRich.generateVerificationMetadata(),
2439
- botResponseId: this._botResponseId,
2889
+ return {
2890
+ messageContextInfo: {
2891
+ deviceListMetadata: {},
2892
+ deviceListMetadataVersion: 2,
2893
+ botMetadata: {
2894
+ messageDisclaimerText: this._title,
2895
+ richResponseSourcesMetadata: { sources: this._richResponseSources },
2896
+ botResponseId: botResponseId,
2897
+ verificationMetadata: {
2898
+ proofs: [
2899
+ {
2900
+ certificateChain: [botMetadataCertificate(), botMetadataCertificate(892)],
2901
+ version: 1,
2902
+ useCase: 1,
2903
+ signature: botMetadataSignature(),
2904
+ },
2905
+ ],
2440
2906
  },
2907
+ ...notif,
2441
2908
  },
2442
- ...this._extraPayload,
2443
- botForwardedMessage: {
2444
- message: {
2445
- richResponseMessage: {
2446
- messageType: 1,
2447
- submessages: includesSubmessages ? await waitAllPromises(this._submessages) : [],
2448
- unifiedResponse: {
2449
- data: includesUnifiedResponse ? Buffer.from(Toolkit.stringifyEscaped({ response_id: this._responseId, sections })).toString('base64') : '',
2450
- },
2451
- contextInfo: {
2452
- ...forward,
2453
- ...qObj,
2454
- ...this._contextInfo,
2455
- },
2909
+ },
2910
+ ...this._extraPayload,
2911
+ botForwardedMessage: {
2912
+ message: {
2913
+ richResponseMessage: {
2914
+ messageType: 1,
2915
+ submessages: includesSubmessages ? await waitAllPromises(this._submessages) : [],
2916
+ unifiedResponse: {
2917
+ data: includesUnifiedResponse ? Buffer.from(JSON.stringify({ response_id: responseId, sections })).toString('base64') : '',
2918
+ },
2919
+ contextInfo: {
2920
+ ...forward,
2921
+ ...qObj,
2922
+ ...this._contextInfo,
2456
2923
  },
2457
2924
  },
2458
2925
  },
2459
2926
  },
2460
- { messageId: messageId || generateMessageIDV2(), ...options }
2461
- );
2927
+ };
2462
2928
  }
2463
2929
 
2464
- async buildEdit(targetJid, targetId, { msg, messageId, ...options } = {}) {
2465
- if (!msg) {
2466
- msg = (await this.build(targetJid, options)).message;
2930
+ // Vanz@Fix (bug 42 / inline image fallback) --- WA won't render AIRichResponseInlineImageMetadata
2931
+ // for bot-sent messages (confirmed: even a valid WA-CDN url with mediaKey stays blank), so any
2932
+ // image added via addInlineImage() is sent here as a normal imageMessage instead. Pass
2933
+ // { skipImageFallback: true } to opt out and send only the (image-less-looking) rich card.
2934
+ // Vanz@Fix: don't spread relayMessage-shaped `options` into sendMessage()'s options param —
2935
+ // the two calls expect different option shapes, so the fallback now only forwards `quoted`
2936
+ // (the one option that clearly applies to both) instead of blindly spreading everything.
2937
+ /** 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. */
2938
+ async send(jid, { forwarded, notification, includesUnifiedResponse, includesSubmessages, skipImageFallback = false, quoted, messageId, ...options } = {}) {
2939
+ const msg = await this.build({ forwarded, notification, includesUnifiedResponse, includesSubmessages, quoted, ...options });
2940
+
2941
+ if (!skipImageFallback && this._inlineImages.length) {
2942
+ for (const { url, caption } of this._inlineImages) {
2943
+ try {
2944
+ await this.#client.sendMessage(jid, { image: { url }, caption }, quoted ? { quoted } : {});
2945
+ } catch (err) {
2946
+ // Vanz@Fix: don't let a fallback image failure block the actual rich card from sending
2947
+ this.#client.logger?.warn?.({ err, url }, 'inline image fallback failed, continuing with rich card');
2948
+ }
2949
+ }
2467
2950
  }
2468
2951
 
2469
- const editedMessage = msg;
2952
+ // Vanz@Add --- pin our own messageId (instead of letting relayMessage mint one internally)
2953
+ // so we know exactly which id was sent, and stash it as _lastMessageKey. That's what lets
2954
+ // sendEdit() be called with no args afterwards and still know which message to patch.
2955
+ messageId = messageId || generateMessageIDV2();
2956
+
2957
+ await this.#client.relayMessage(jid, msg, { messageId, ...options });
2958
+
2959
+ this._lastMessageKey = { remoteJid: jid, fromMe: true, id: messageId };
2960
+
2961
+ return { key: this._lastMessageKey, message: msg };
2962
+ }
2963
+
2964
+ /**
2965
+ * Build a `protocolMessage` (type EDIT) that patches an already-sent AIRich message in place.
2966
+ * @param {string} targetJid Chat the original message lives in.
2967
+ * @param {string} targetId `key.id` of the original message (the id `send()`/`sendEdit()` returned).
2968
+ * @param {object} [opts] Pass `{ msg }` to reuse an already-built content object instead of rebuilding via build().
2969
+ */
2970
+ async buildEdit(targetJid, targetId, { msg, messageId, ...options } = {}) {
2971
+ const editedMessage = msg || (await this.build({ ...options }));
2470
2972
 
2471
2973
  if (!editedMessage) {
2472
- throw new Error('buildEdit: msg does not contain botForwardedMessage');
2974
+ throw new Error('buildEdit: no message content to edit (build() returned nothing)');
2473
2975
  }
2474
2976
 
2475
2977
  return generateWAMessageFromContent(
2476
2978
  targetJid,
2477
2979
  {
2478
- botForwardedMessage: {
2479
- message: {
2480
- protocolMessage: {
2481
- key: {
2482
- remoteJid: targetJid,
2483
- fromMe: true,
2484
- id: targetId,
2485
- },
2486
- type: 14,
2487
- editedMessage,
2488
- },
2980
+ protocolMessage: {
2981
+ key: {
2982
+ remoteJid: targetJid,
2983
+ fromMe: true,
2984
+ id: targetId,
2489
2985
  },
2986
+ type: 14, // MESSAGE_EDIT
2987
+ editedMessage,
2490
2988
  },
2491
2989
  },
2492
2990
  { messageId: messageId || generateMessageIDV2(), ...options }
2493
2991
  );
2494
2992
  }
2495
2993
 
2994
+ /**
2995
+ * Rebuild this AIRich message's current content and patch it into an already-sent message in place
2996
+ * (WA edits the bubble instead of showing a new one). With no args, edits the message from the last
2997
+ * send()/sendEdit() call — that's the flow `.addX(...); await rich.sendEdit();` relies on.
2998
+ * @param {string} [jid] Defaults to the jid from the last send()/sendEdit().
2999
+ * @param {string} [id] Defaults to the message id from the last send()/sendEdit().
3000
+ */
2496
3001
  async sendEdit(jid, id, { msg, messageId, additionalNodes = [], ...options } = {}) {
2497
3002
  jid = jid ?? this._lastMessageKey?.remoteJid;
2498
3003
  id = id ?? this._lastMessageKey?.id;
2499
3004
 
2500
3005
  if (!jid) {
2501
- throw new Error('JID is required');
3006
+ throw new Error('sendEdit: no jid — pass one explicitly, or call send() first');
2502
3007
  }
2503
3008
 
2504
3009
  if (!id) {
2505
- throw new Error('Message id is required');
3010
+ throw new Error('sendEdit: no message id pass one explicitly, or call send() first');
2506
3011
  }
2507
3012
 
2508
3013
  const msgEdit = await this.buildEdit(jid, id, {
@@ -2516,36 +3021,13 @@ class AIRich extends BaseBuilder {
2516
3021
  additionalNodes,
2517
3022
  });
2518
3023
 
3024
+ // Vanz@Note --- deliberately NOT overwriting _lastMessageKey with msgEdit.key here: the
3025
+ // protocolMessage envelope has its own id, but the message the user actually sees (and the
3026
+ // one future sendEdit() calls need to keep patching) is still `id`/`jid` above.
2519
3027
  return msgEdit;
2520
3028
  }
2521
3029
 
2522
- async send(jid, { bypassDownload = true, forwarded = true, notification = false, includesUnifiedResponse = true, includesSubmessages = true, messageId, additionalNodes = [], ...options } = {}) {
2523
- const msg = await this.build(jid, {
2524
- forwarded,
2525
- notification,
2526
- includesUnifiedResponse,
2527
- includesSubmessages,
2528
- messageId,
2529
- ...options,
2530
- });
2531
-
2532
- await this.#client.relayMessage(msg.key.remoteJid, msg.message, {
2533
- messageId: msg.key.id,
2534
- additionalNodes,
2535
- ...options,
2536
- });
2537
-
2538
- if (includesUnifiedResponse && bypassDownload) {
2539
- await this.sendEdit(jid, msg.key.id, {
2540
- msg: msg.message,
2541
- });
2542
- }
2543
-
2544
- this._lastMessageKey = msg.key;
2545
-
2546
- return msg;
2547
- }
2548
-
3030
+ /** 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. */
2549
3031
  static tokenizer(code, lang = 'javascript') {
2550
3032
  const keywordsMap = {
2551
3033
  javascript: new Set([
@@ -3211,6 +3693,7 @@ class AIRich extends BaseBuilder {
3211
3693
  };
3212
3694
  }
3213
3695
 
3696
+ /** Convert a raw `string[][]` grid into the table metadata shape addTable()/addText() produce internally. */
3214
3697
  static toTableMetadata(arr, { hyperlink = true, citation = true, latex = true } = {}) {
3215
3698
  if (!Array.isArray(arr) || !arr.every((row) => Array.isArray(row) && row.every((cell) => typeof cell === 'string'))) {
3216
3699
  throw new TypeError('Table must be a nested array of strings');
@@ -3259,358 +3742,129 @@ class AIRich extends BaseBuilder {
3259
3742
  };
3260
3743
  }
3261
3744
 
3262
- static generateVerificationMetadata() {
3263
- const signatureMaterial = Buffer.from(
3264
- `\u004E\u0049\u0058\u0045\u004C\u002E\u004D\u0065\u0073\u0073\u0061\u0067\u0065\u0042\u0075\u0069\u006C\u0064\u0065\u0072\u0056${VERSION}\u002D\u0056\u0065\u0072\u0069\u0066\u0069\u0063\u0061\u0074\u0069\u006F\u006E\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065\u002E\u004D\u0065\u0074\u0061\u0064\u0061\u0074\u0061`
3265
- );
3266
-
3267
- const certificateMaterial = Buffer.from(
3268
- `\u004E\u0049\u0058\u0045\u004C\u002E\u004D\u0065\u0073\u0073\u0061\u0067\u0065\u0042\u0075\u0069\u006C\u0064\u0065\u0072\u0056${VERSION}\u002D\u0043\u0065\u0072\u0074\u0069\u0066\u0069\u0063\u0061\u0074\u0065\u0043\u0068\u0061\u0069\u006E\u002E\u004D\u0065\u0074\u0061\u0064\u0061\u0074\u0061`
3269
- );
3270
-
3271
- const signature = Buffer.concat([signatureMaterial, crypto.randomBytes(64 - signatureMaterial.length)]).toString('base64');
3272
-
3273
- const certificateChain = [
3274
- Buffer.concat([certificateMaterial, crypto.randomBytes(684 - certificateMaterial.length)]).toString('base64'),
3275
-
3276
- Buffer.concat([certificateMaterial, crypto.randomBytes(892 - certificateMaterial.length)]).toString('base64'),
3277
- ];
3278
-
3279
- return {
3280
- proofs: [
3281
- {
3282
- version: 1,
3283
- useCase: 1,
3284
- signature,
3285
- certificateChain,
3745
+ /**
3746
+ * Add an "AI is generating..." placeholder card (`GenAIImaginePrimitive` with status
3747
+ * GENERATING) — distinct from addImage()/addVideo() which always send status READY.
3748
+ * Use this to show a pending-generation state before the real media is ready.
3749
+ * @param {{ imagine_type?: 'IMAGE'|'ANIMATE', estimated_completion_time?: number }} [options]
3750
+ */
3751
+ addGenerating({ imagine_type = 'IMAGE', estimated_completion_time, textFallback = true } = {}) {
3752
+ this._submessages.push({ messageType: 2, messageText: '[ Sedang diproses... ]' });
3753
+ this._sections.push(
3754
+ AIRich.newLayout('Single', {
3755
+ media: { url: '', mime_type: imagine_type === 'ANIMATE' ? 'video/mp4' : 'image/png' },
3756
+ imagine_type,
3757
+ status: {
3758
+ status: 'GENERATING',
3759
+ estimated_completion_time: estimated_completion_time ?? Math.floor(Date.now() / 1000) + 30,
3286
3760
  },
3287
- ],
3288
- };
3761
+ __typename: 'GenAIImaginePrimitive',
3762
+ })
3763
+ );
3764
+ // Vanz@Fix 23-08-26 (v4.8) --- media.url kosong + status GENERATING gak punya renderer
3765
+ // visual instan di stock WA client; sebelumnya cuma diem sampe WA nge-timeout sendiri
3766
+ // dan nampilin fallback bawaannya ("Saat ini, saya tidak bisa membuat gambar itu...").
3767
+ // Same fix class kayak addTask/addBloks: append FOATextPrimitive biar ada fallback
3768
+ // instan, gak perlu nunggu timeout WA. Set { textFallback: false } buat skip.
3769
+ if (textFallback) {
3770
+ this._sections.push(AIRich.newLayout('Single', { text: '[ Sedang diproses... ]', __typename: 'FOATextPrimitive' }));
3771
+ }
3772
+ return this;
3289
3773
  }
3290
3774
 
3291
- static newLayout(name, data, extra = {}) {
3292
- return {
3293
- ...extra,
3294
- view_model: {
3295
- [Array.isArray(data) ? 'primitives' : 'primitive']: data,
3296
- __typename: `GenAI${name}LayoutViewModel`,
3775
+ /**
3776
+ * Send a support-ticket marker message (`messageContextInfo.supportPayload`) — a plain
3777
+ * conversation message tagged as an AI/support-bot ticket, distinct from richResponseMessage.
3778
+ * @param {import('../../WAProto/index.js').WASocket} client
3779
+ * @param {string} jid
3780
+ * @param {string} text
3781
+ * @param {{ ticketId?: string, isAiMessage?: boolean, shouldShowSystemMessage?: boolean, version?: number }} [options]
3782
+ */
3783
+ static async sendSupportPayload(client, jid, text, { ticketId = crypto.randomUUID(), isAiMessage = true, shouldShowSystemMessage = true, version = 1 } = {}) {
3784
+ if (!client) throw new Error('Socket is required');
3785
+ if (typeof text !== 'string' || !text) throw new TypeError('sendSupportPayload(client, jid, text) requires a non-empty string text');
3786
+
3787
+ const msg = {
3788
+ conversation: text,
3789
+ messageContextInfo: {
3790
+ messageSecret: crypto.randomBytes(32),
3791
+ supportPayload: JSON.stringify({
3792
+ version,
3793
+ is_ai_message: isAiMessage,
3794
+ should_show_system_message: shouldShowSystemMessage,
3795
+ ticket_id: ticketId,
3796
+ }),
3297
3797
  },
3298
3798
  };
3299
- }
3300
3799
 
3301
- _makeNode(id, section, submessage) {
3302
- return { id: id ?? null, section: section ?? null, submessage: submessage ?? null };
3800
+ return client.relayMessage(jid, msg, {
3801
+ additionalNodes: [
3802
+ { tag: 'bot', attrs: { biz_bot: '1' } },
3803
+ { tag: 'biz', attrs: {} },
3804
+ ],
3805
+ });
3303
3806
  }
3304
3807
 
3305
- _registerId(node, id) {
3306
- if (id === undefined || id === null || id === '') return;
3307
-
3308
- if (typeof id !== 'string') {
3309
- throw new ContentValidationError('Item id must be a string', { id });
3310
- }
3808
+ /**
3809
+ * Send an image and video as one paired-media unit (image sent first, video linked to it via
3810
+ * `messageAssociation`). Distinct from a plain album — the client treats them as a single group.
3811
+ * @param {import('../../WAProto/index.js').WASocket} client
3812
+ * @param {string} jid
3813
+ * @param {{ image: string|Buffer, video: string|Buffer }} media
3814
+ */
3815
+ static async sendPairedMedia(client, jid, { image, video } = {}) {
3816
+ if (!client) throw new Error('Socket is required');
3817
+ if (!image || !video) throw new TypeError('sendPairedMedia() requires both "image" and "video"');
3311
3818
 
3312
- if (this._idIndex.has(id)) {
3313
- throw new DuplicateIdError(id);
3314
- }
3819
+ const imagePrepared = await prepareWAMessageMedia(
3820
+ { image: typeof image === 'string' ? { url: image } : image },
3821
+ { upload: client.waUploadToServer }
3822
+ );
3823
+ const videoPrepared = await prepareWAMessageMedia(
3824
+ { video: typeof video === 'string' ? { url: video } : video },
3825
+ { upload: client.waUploadToServer }
3826
+ );
3315
3827
 
3316
- node.id = id;
3317
- this._idIndex.set(id, node);
3318
- }
3828
+ const imageMsg = generateWAMessageFromContent(
3829
+ jid,
3830
+ {
3831
+ imageMessage: {
3832
+ ...imagePrepared.imageMessage,
3833
+ contextInfo: { pairedMediaType: 5, statusSourceType: 0 },
3834
+ },
3835
+ },
3836
+ {}
3837
+ );
3319
3838
 
3320
- _unregisterId(node) {
3321
- if (node.id && this._idIndex.get(node.id) === node) {
3322
- this._idIndex.delete(node.id);
3323
- }
3324
- }
3839
+ await client.relayMessage(jid, imageMsg.message, { messageId: imageMsg.key.id });
3325
3840
 
3326
- hasId(id) {
3327
- return typeof id === 'string' && this._idIndex.has(id);
3328
- }
3841
+ await client.relayMessage(
3842
+ jid,
3843
+ {
3844
+ videoMessage: {
3845
+ ...videoPrepared.videoMessage,
3846
+ contextInfo: { pairedMediaType: 6, statusSourceType: 0 },
3847
+ },
3848
+ messageContextInfo: {
3849
+ messageAssociation: { associationType: 12, parentMessageKey: imageMsg.key },
3850
+ },
3851
+ },
3852
+ {}
3853
+ );
3329
3854
 
3330
- getIds() {
3331
- return [...this._idIndex.keys()];
3855
+ return imageMsg.key;
3332
3856
  }
3333
3857
 
3334
- peek(id) {
3335
- const node = this._idIndex.get(id);
3336
-
3337
- if (!node) return null;
3338
-
3858
+ /** Build a raw submessage layout block by name — escape hatch for layouts not covered by the add*() helpers. */
3859
+ static newLayout(name, data, extra = {}) {
3339
3860
  return {
3340
- id: node.id,
3341
- section: node.section,
3342
- submessage: node.submessage,
3861
+ ...extra,
3862
+ view_model: {
3863
+ [Array.isArray(data) ? 'primitives' : 'primitive']: data,
3864
+ __typename: `GenAI${name}LayoutViewModel`,
3865
+ },
3343
3866
  };
3344
3867
  }
3345
-
3346
- assignId(index, id) {
3347
- if (!Number.isInteger(index) || index < 0 || index >= this._nodes.length) {
3348
- throw new InvalidTargetError(`Node index ${index} is out of range (0-${this._nodes.length - 1})`, { index });
3349
- }
3350
-
3351
- const node = this._nodes[index];
3352
-
3353
- if (node.id) {
3354
- throw new AIRichError(`Node at index ${index} already has id "${node.id}"`, 'ALREADY_HAS_ID', { index, id: node.id });
3355
- }
3356
-
3357
- this._registerId(node, id);
3358
-
3359
- return this;
3360
- }
3361
-
3362
- _getNode(id) {
3363
- if (typeof id !== 'string' || !id) {
3364
- throw new ContentValidationError('Item id must be a non-empty string', { id });
3365
- }
3366
-
3367
- const node = this._idIndex.get(id);
3368
-
3369
- if (!node) {
3370
- throw new ItemNotFoundError(id, this.getIds());
3371
- }
3372
-
3373
- return node;
3374
- }
3375
-
3376
- _resolveTarget(target) {
3377
- if (Array.isArray(target)) {
3378
- if (target.length < 1 || target.length > 2) {
3379
- throw new ContentValidationError('Target must be id or [id, offset]', { target });
3380
- }
3381
-
3382
- const [id, offset = 0] = target;
3383
-
3384
- if (typeof id !== 'string' || !id) {
3385
- throw new ContentValidationError('Target id must be a non-empty string', { target });
3386
- }
3387
-
3388
- if (!Number.isInteger(offset)) {
3389
- throw new ContentValidationError('Offset must be an integer', { target });
3390
- }
3391
-
3392
- return { id, offset };
3393
- }
3394
-
3395
- if (typeof target !== 'string' || !target) {
3396
- throw new ContentValidationError('Target must be a non-empty id or [id, offset]', { target });
3397
- }
3398
-
3399
- return { id: target, offset: 0 };
3400
- }
3401
-
3402
- _resolveNodeIndex(target) {
3403
- const { id, offset } = this._resolveTarget(target);
3404
- const node = this._getNode(id);
3405
- const baseIndex = this._nodes.indexOf(node);
3406
-
3407
- if (baseIndex === -1) {
3408
- throw new InvalidTargetError(`Item id "${id}" is registered but not present in the node list (internal desync)`, { id });
3409
- }
3410
-
3411
- const index = baseIndex + offset;
3412
-
3413
- if (index < 0 || index >= this._nodes.length) {
3414
- throw new InvalidTargetError(`Target "${id}" with offset ${offset} resolves to index ${index}, which is out of range (0-${this._nodes.length - 1})`, { id, offset, index });
3415
- }
3416
-
3417
- return { id, offset, baseIndex, index };
3418
- }
3419
-
3420
- _validateSections(section) {
3421
- const items = Array.isArray(section) ? section : [section];
3422
-
3423
- if (!items.length) {
3424
- throw new ContentValidationError('At least one section is required');
3425
- }
3426
-
3427
- for (const item of items) {
3428
- if (!item || typeof item !== 'object' || Array.isArray(item)) {
3429
- throw new ContentValidationError('Sections must be plain objects');
3430
- }
3431
- }
3432
-
3433
- return items;
3434
- }
3435
-
3436
- _validateSubmessages(submessage) {
3437
- if (submessage === undefined || submessage === null) {
3438
- return [];
3439
- }
3440
-
3441
- const items = Array.isArray(submessage) ? submessage : [submessage];
3442
-
3443
- for (const item of items) {
3444
- if (!item || typeof item !== 'object' || Array.isArray(item)) {
3445
- throw new ContentValidationError('Submessages must be plain objects');
3446
- }
3447
- }
3448
-
3449
- return items;
3450
- }
3451
-
3452
- _pairSubmessages(sections, submessages) {
3453
- const n = sections.length;
3454
- const m = submessages.length;
3455
-
3456
- if (m === 0) return sections.map(() => null);
3457
- if (m === 1) return sections.map((_, i) => (i === 0 ? submessages[0] : null));
3458
- if (m === n) return submessages;
3459
-
3460
- throw new ContentValidationError(`Cannot pair ${m} submessage(s) with ${n} section(s): expected 0, 1, or ${n}`, { sectionCount: n, submessageCount: m });
3461
- }
3462
-
3463
- _addContent(section, submessage, { id, replace, insertAt } = {}) {
3464
- const hasReplace = replace !== undefined && replace !== null && replace !== '';
3465
-
3466
- const hasInsertAt = insertAt !== undefined && insertAt !== null && insertAt !== '';
3467
-
3468
- if (hasReplace && hasInsertAt) {
3469
- throw new ContentValidationError('replace and insertAt cannot be used together');
3470
- }
3471
-
3472
- const sections = this._validateSections(section);
3473
- const submessages = this._validateSubmessages(submessage);
3474
-
3475
- if (!sections.length) {
3476
- throw new ContentValidationError('At least one section is required');
3477
- }
3478
-
3479
- if (id !== undefined && id !== null && id !== '' && sections.length !== 1) {
3480
- throw new ContentValidationError('One id can only be assigned to one node', {
3481
- id,
3482
- sectionCount: sections.length,
3483
- });
3484
- }
3485
-
3486
- if (submessages.length && submessages.length !== sections.length && submessages.length !== 1) {
3487
- throw new ContentValidationError('Section and submessage count must match');
3488
- }
3489
-
3490
- const pairedSubmessages = sections.map((_, index) => {
3491
- if (!submessages.length) return undefined;
3492
-
3493
- return submessages.length === 1 ? submessages[0] : submessages[index];
3494
- });
3495
-
3496
- if (id && this._idIndex.has(id) && !(hasReplace && this._resolveTarget(replace)?.id === id)) {
3497
- throw new DuplicateIdError(id);
3498
- }
3499
-
3500
- const newNodes = sections.map((currentSection, index) => {
3501
- return this._makeNode(index === 0 ? id : null, currentSection, pairedSubmessages[index]);
3502
- });
3503
-
3504
- if (hasReplace) {
3505
- if (newNodes.length !== 1) {
3506
- throw new ContentValidationError('replace only supports adding exactly one node');
3507
- }
3508
-
3509
- const target = this._resolveNodeIndex(replace);
3510
-
3511
- if (!target) {
3512
- throw new ContentValidationError('Target node could not be resolved');
3513
- }
3514
-
3515
- const oldNode = this._nodes[target.index];
3516
- const newNode = newNodes[0];
3517
-
3518
- if (!newNode.id && oldNode?.id) {
3519
- newNode.id = oldNode.id;
3520
- }
3521
-
3522
- this._unregisterId(oldNode);
3523
-
3524
- this._nodes.splice(target.index, 1, newNode);
3525
-
3526
- if (newNode.id) {
3527
- this._idIndex.set(newNode.id, newNode);
3528
- }
3529
-
3530
- return this;
3531
- }
3532
-
3533
- if (hasInsertAt) {
3534
- const target = this._resolveNodeIndex(insertAt);
3535
-
3536
- if (!target) {
3537
- throw new ContentValidationError('Target node could not be resolved');
3538
- }
3539
-
3540
- const insertIndex = target.offset < 0 ? target.index : target.index + 1;
3541
-
3542
- this._nodes.splice(insertIndex, 0, ...newNodes);
3543
-
3544
- for (const node of newNodes) {
3545
- if (node.id) {
3546
- this._idIndex.set(node.id, node);
3547
- }
3548
- }
3549
-
3550
- return this;
3551
- }
3552
-
3553
- this._nodes.push(...newNodes);
3554
-
3555
- for (const node of newNodes) {
3556
- if (node.id) {
3557
- this._idIndex.set(node.id, node);
3558
- }
3559
- }
3560
-
3561
- return this;
3562
- }
3563
-
3564
- addSection(section, options = {}) {
3565
- return this._addContent(section, undefined, options);
3566
- }
3567
-
3568
- addSubmessage(submessage, options = {}) {
3569
- const items = this._validateSubmessages(submessage);
3570
-
3571
- if (!items.length) {
3572
- throw new ContentValidationError('At least one submessage is required');
3573
- }
3574
-
3575
- return this._addContent(undefined, items, options);
3576
- }
3577
-
3578
- delete(target) {
3579
- const { index } = this._resolveNodeIndex(target);
3580
- const [oldNode] = this._nodes.splice(index, 1);
3581
-
3582
- this._unregisterId(oldNode);
3583
-
3584
- return this;
3585
- }
3586
-
3587
- get _sections() {
3588
- return this._nodes.filter((n) => n.section !== null).map((n) => n.section);
3589
- }
3590
-
3591
- get _submessages() {
3592
- return this._nodes.filter((n) => n.submessage !== null).map((n) => n.submessage);
3593
- }
3594
-
3595
- get sections() {
3596
- return this._sections;
3597
- }
3598
-
3599
- get items() {
3600
- return this._sections.flatMap((section) => {
3601
- const vm = section?.view_model;
3602
-
3603
- if (Array.isArray(vm?.primitives)) {
3604
- return vm.primitives;
3605
- }
3606
-
3607
- if (vm?.primitive) {
3608
- return [vm.primitive];
3609
- }
3610
-
3611
- return [];
3612
- });
3613
- }
3614
3868
  }
3615
3869
 
3616
3870
  // Vanz@Alias --- AIRich diekspos ulang pake nama sendiri. Implementasi & referensi