@vanzxy/baileys 1.5.3 → 1.5.5

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.
@@ -47,6 +47,7 @@
47
47
  const MESSAGE_BUILDER_VERSION = '4.9';
48
48
 
49
49
  import { generateWAMessageFromContent, prepareWAMessageMedia } from './messages.js';
50
+ import { generateMessageIDV2 } from './generics.js';
50
51
  import { botMetadataSignature, botMetadataCertificate } from './rich-message-utils.js';
51
52
  import crypto from 'crypto';
52
53
  import { PassThrough, Readable } from 'stream';
@@ -293,15 +294,19 @@ class Toolkit {
293
294
  return await waitAllPromises(input);
294
295
  }
295
296
 
296
- /** Fetch `url` into a Buffer. @param {boolean} [silent] Return an empty Buffer instead of throwing on failure. */
297
- static async fetchBuffer(url, options = {}, { silent = true } = {}) {
297
+ /** Fetch `url` into a Buffer. @param {boolean} [silent] Return an empty Buffer instead of throwing on failure. @param {number} [timeout] Abort after this many ms (default 15s) instead of hanging indefinitely on a dead/slow host. */
298
+ static async fetchBuffer(url, options = {}, { silent = true, timeout = 15000 } = {}) {
299
+ const controller = new AbortController();
300
+ const timer = setTimeout(() => controller.abort(), timeout);
298
301
  try {
299
- let response = await fetch(url, options);
302
+ let response = await fetch(url, { ...options, signal: options.signal ?? controller.signal });
300
303
  if (!response.ok) throw Error(`HTTP ${response.status}`);
301
304
  return Buffer.from(await response.arrayBuffer());
302
305
  } catch (error) {
303
306
  if (silent) return Buffer.alloc(0);
304
307
  throw error;
308
+ } finally {
309
+ clearTimeout(timer);
305
310
  }
306
311
  }
307
312
 
@@ -527,6 +532,10 @@ class Toolkit {
527
532
  }
528
533
  });
529
534
  }
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
+ }
530
539
  }
531
540
 
532
541
  /**
@@ -1568,10 +1577,48 @@ class Poll extends BaseBuilder {
1568
1577
  * everything ChatGPT/Gemini-in-WhatsApp-style bots typically render.
1569
1578
  * Also exported as `AIVanzxy` / `LeafRich` / `VanzxyAI` / `VanzxyRich` (identical class, alternate names).
1570
1579
  */
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
+
1571
1618
  class AIRich extends BaseBuilder {
1572
1619
  #client;
1573
1620
 
1574
- constructor(client) {
1621
+ constructor(client, { dynamic = true, unsupportedTypeAlert = true } = {}) {
1575
1622
  if (!client) {
1576
1623
  throw new Error('Socket is required');
1577
1624
  }
@@ -1579,148 +1626,125 @@ class AIRich extends BaseBuilder {
1579
1626
  super();
1580
1627
  this.#client = client;
1581
1628
  this._contextInfo = {};
1582
- this._submessages = [];
1583
- this._sections = [];
1584
- this._richResponseSources = [];
1585
- // Vanz@Fix (bug 42 / inline image fallback): WA rejects rendering AIRichResponseInlineImageMetadata
1586
- // for third-party bots regardless of URL (confirmed empirically — Meta/WA CDN url with valid
1587
- // mediaKey still doesn't render, so it's a trust-chain gate, not a domain/encoding issue).
1588
- // Track every addInlineImage() call here so send() can fall back to a normal imageMessage.
1589
- this._inlineImages = [];
1590
-
1591
- // Vanz@Add 24-08-26 --- ported from temen's MessageBuilderV4.7 (setResponseId/setBotResponseId
1592
- // below), rewritten to fit this fork's conventions. build() used to always mint a fresh
1593
- // crypto.randomUUID() for both unifiedResponse.response_id and botMetadata.botResponseId on
1594
- // every call, with no way to reuse one — so a `sendEdit()`-style flow (rebuild the same
1595
- // message with updated content, same response_id, so WA patches it in place instead of
1596
- // showing a new message) was never actually possible despite being in the gist example.
1597
- // null here means "not pinned yet" — build() falls back to a fresh randomUUID() same as before
1598
- // when neither setResponseId() nor setBotResponseId() has been called.
1599
- this._responseId = null;
1600
- this._botResponseId = null;
1601
-
1602
- // Vanz@Add (v4.9.1) --- { id, insertAt } support for every add*()/set*() call, without
1603
- // touching each method's own body/signature. Every add*() call ends up pushing 0-N items
1604
- // onto _submessages and 0-N onto _sections (some push to both, some to just one — e.g.
1605
- // addSuggest only touches _submessages, addSection only touches _sections). A Proxy wraps
1606
- // every add*/set* call: it snapshots array lengths before calling the real method, lets the
1607
- // method push onto the tail as it always has, then — if the caller passed { insertAt } —
1608
- // peels those freshly-pushed items back off the tail and re-splices them right after the
1609
- // last item that belongs to the block named by insertAt. Blocks are tracked by *object
1610
- // reference*, not saved numeric index, so earlier insertions shifting the array around never
1611
- // invalidates a later insertAt lookup (indexOf on the reference always finds the live position).
1612
- this._blocks = new Map(); // id -> { subItems: object[], secItems: object[] }
1613
- return new Proxy(this, {
1614
- get(target, prop, receiver) {
1615
- const orig = Reflect.get(target, prop, receiver);
1616
- if (typeof orig !== 'function') return orig;
1617
-
1618
- // Vanz@Fix 23-08-26 (part 2) --- the add*/set* filter below only wrapped methods whose
1619
- // name starts with "add"/"set". Everything else (send(), build(), ...) fell through to
1620
- // `return orig` unwrapped, so calling e.g. `richInstance.send(...)` still invoked the
1621
- // real method with `this` = the Proxy (`receiver`), hitting the exact same
1622
- // "Cannot read private member #client..." brand-check error the add*/set* fix was for —
1623
- // just one level up, in send()/build() themselves. Every function property now gets
1624
- // bound to `target` (the real instance) at minimum; add*/set* additionally get the
1625
- // insertAt/id bookkeeping below.
1626
- if (!/^(add|set)/.test(String(prop))) {
1627
- return (...args) => {
1628
- const result = orig.apply(target, args);
1629
- return result === target ? receiver : result;
1630
- };
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();
1635
+ 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
+
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);
1631
1677
  }
1678
+ } catch {}
1679
+ }
1632
1680
 
1633
- return (...args) => {
1634
- const opts = args.find((a) => a && typeof a === 'object' && !Array.isArray(a) && !Buffer.isBuffer(a) && ('id' in a || 'insertAt' in a));
1635
- const id = opts?.id;
1636
- const insertAt = opts?.insertAt;
1637
-
1638
- const subBefore = target._submessages.length;
1639
- const secBefore = target._sections.length;
1640
-
1641
- // Vanz@Fix 23-08-26 --- was orig.apply(receiver, args): calling the real method bound to
1642
- // the Proxy itself (`receiver`) makes any `this.#client` access inside throw
1643
- // "Cannot read private member #client from an object whose class did not declare it",
1644
- // because a Proxy is never the branded instance a private field was declared on —
1645
- // this hit every add*() that touches #client via Toolkit.resolveMedia(this.#client, ...)
1646
- // (addProduct/addPost/addReels/addSource, and would eventually hit addImage/addVideo
1647
- // too once JIT/engine specifics changed). Binding to `target` (the real instance) instead
1648
- // fixes it for good; `target._submessages`/`target._sections` below are unaffected since
1649
- // they're plain properties, and `result === target ? receiver : result` still converts a
1650
- // `this`-return back to the Proxy so chaining (`.addX().addY()`) keeps working.
1651
- const result = orig.apply(target, args);
1652
-
1653
-
1654
- const subItems = target._submessages.splice(subBefore);
1655
- const secItems = target._sections.splice(secBefore);
1656
-
1657
- if (insertAt) {
1658
- const anchor = target._blocks.get(insertAt);
1659
- if (!anchor) throw new Error(`insertAt: no block registered with id "${insertAt}" (register it by passing { id: "${insertAt}" } on an earlier add*() call)`);
1660
-
1661
- const lastSub = anchor.subItems[anchor.subItems.length - 1];
1662
- const subIdx = lastSub ? target._submessages.indexOf(lastSub) + 1 : target._submessages.length;
1663
- target._submessages.splice(subIdx, 0, ...subItems);
1664
-
1665
- const lastSec = anchor.secItems[anchor.secItems.length - 1];
1666
- const secIdx = lastSec ? target._sections.indexOf(lastSec) + 1 : target._sections.length;
1667
- target._sections.splice(secIdx, 0, ...secItems);
1668
- } else {
1669
- target._submessages.push(...subItems);
1670
- target._sections.push(...secItems);
1671
- }
1681
+ this._nodes = [];
1682
+ this._idIndex = new Map();
1672
1683
 
1673
- if (id) target._blocks.set(id, { subItems, secItems });
1684
+ const maxLength = Math.max(loadedSections.length, loadedSubmessages.length);
1674
1685
 
1675
- return result === target ? receiver : result;
1676
- };
1677
- },
1678
- });
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
+
1694
+ this._extraPayload = {};
1695
+
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
+ }
1701
+
1702
+ return this;
1679
1703
  }
1680
1704
 
1681
- /** Flatten every primitive pushed into `_sections` so far into one array — lets you build a
1682
- * card set in one AIRich instance and re-embed it into another via addSection(AIRich.newLayout(...)). */
1683
- get items() {
1684
- return this._sections.flatMap((s) => {
1685
- const vm = s?.view_model;
1686
- if (!vm) return [];
1687
- return vm.primitives ?? (vm.primitive !== undefined ? [vm.primitive] : []);
1688
- });
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;
1689
1712
  }
1690
1713
 
1691
- /** Push a raw pre-built submessage block (escape hatch for shapes not covered by the add*() helpers). */
1692
- addSubmessage(submessage) {
1693
- const items = Array.isArray(submessage) ? submessage : [submessage];
1714
+ refreshResponseId() {
1715
+ this._responseId = crypto.randomUUID();
1694
1716
 
1695
- for (const item of items) {
1696
- if (typeof item !== 'object' || item === null || Array.isArray(item)) {
1697
- throw new TypeError('Submessage must be a plain object or array of plain objects');
1698
- }
1717
+ return this;
1718
+ }
1699
1719
 
1700
- this._submessages.push(item);
1720
+ setBotResponseId(id) {
1721
+ if (typeof id !== 'string') {
1722
+ throw new TypeError('ID must be a string');
1701
1723
  }
1724
+ this._botResponseId = id;
1702
1725
 
1703
1726
  return this;
1704
1727
  }
1705
1728
 
1706
- /** Push a raw pre-built section wrapper around one or more submessages. */
1707
- addSection(section) {
1708
- const items = Array.isArray(section) ? section : [section];
1729
+ refreshBotResponseId() {
1730
+ this._botResponseId = crypto.randomUUID();
1709
1731
 
1710
- for (const item of items) {
1711
- if (typeof item !== 'object' || item === null || Array.isArray(item)) {
1712
- throw new TypeError('Section must be a plain object or array of plain objects');
1713
- }
1732
+ return this;
1733
+ }
1714
1734
 
1715
- this._sections.push(item);
1735
+ createAlert(type) {
1736
+ if (this._unsupportedTypeAlert) {
1737
+ return {
1738
+ messageType: 2,
1739
+ messageText: `[ UNSUPPORTED_TYPE - ${type}]`,
1740
+ };
1716
1741
  }
1717
1742
 
1718
- return this;
1743
+ return undefined;
1719
1744
  }
1720
1745
 
1721
- /** 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. */
1722
- addText(text, { hyperlink = true, citation = true, latex = true } = {}) {
1723
- if (typeof text != 'string') {
1746
+ addText(text, { hyperlink = true, citation = true, latex = true, id, replace, insertAt } = {}) {
1747
+ if (typeof text !== 'string') {
1724
1748
  throw new TypeError('Text must be a string');
1725
1749
  }
1726
1750
 
@@ -1730,112 +1754,180 @@ class AIRich extends BaseBuilder {
1730
1754
  latex,
1731
1755
  });
1732
1756
 
1733
- this._submessages.push({
1734
- messageType: 2,
1735
- messageText: extractedText,
1757
+ const section = AIRich.newLayout('Single', {
1758
+ text: extractedText,
1759
+ ...(inline_entities.length && { inline_entities }),
1760
+ __typename: 'GenAIMarkdownTextUXPrimitive',
1736
1761
  });
1737
1762
 
1738
- this._sections.push(
1739
- AIRich.newLayout('Single', {
1740
- text: extractedText,
1741
- ...(inline_entities.length && {
1742
- inline_entities,
1743
- }),
1744
- __typename: 'GenAIMarkdownTextUXPrimitive',
1745
- })
1746
- );
1763
+ const submessages = [
1764
+ {
1765
+ messageType: 2,
1766
+ messageText: text,
1767
+ },
1768
+ ].filter(Boolean);
1747
1769
 
1748
- return this;
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',
1785
+ });
1786
+
1787
+ const submessages = [
1788
+ {
1789
+ messageType: 2,
1790
+ messageText: text,
1791
+ },
1792
+ ];
1793
+
1794
+ return this._addContent(section, submessages, {
1795
+ id,
1796
+ replace,
1797
+ insertAt,
1798
+ });
1749
1799
  }
1750
1800
 
1751
- /** Add a syntax-highlighted code block. @param {string} language e.g. 'javascript', 'python'. */
1752
- addCode(language, code) {
1801
+ addCode(language, code, { id, replace, insertAt } = {}) {
1753
1802
  if (typeof language !== 'string' || typeof code !== 'string') {
1754
1803
  throw new TypeError('Language and code must be a string');
1755
1804
  }
1756
1805
 
1757
1806
  const meta = AIRich.tokenizer(code, language);
1758
1807
 
1759
- this._submessages.push({
1760
- messageType: 5,
1761
- codeMetadata: {
1762
- codeLanguage: language,
1763
- codeBlocks: meta.codeBlock,
1764
- },
1808
+ const section = AIRich.newLayout('Single', {
1809
+ language,
1810
+ code_blocks: meta.unified_codeBlock,
1811
+ __typename: 'GenAICodeUXPrimitive',
1765
1812
  });
1766
1813
 
1767
- this._sections.push(
1768
- AIRich.newLayout('Single', {
1769
- language,
1770
- code_blocks: meta.unified_codeBlock,
1771
- __typename: 'GenAICodeUXPrimitive',
1772
- })
1773
- );
1814
+ const submessages = [
1815
+ {
1816
+ messageType: 5,
1817
+ codeMetadata: {
1818
+ codeLanguage: language,
1819
+ codeBlocks: meta.codeBlock,
1820
+ },
1821
+ },
1822
+ ];
1774
1823
 
1775
- return this;
1824
+ return this._addContent(section, submessages, {
1825
+ id,
1826
+ replace,
1827
+ insertAt,
1828
+ });
1776
1829
  }
1777
1830
 
1778
- /** Add a table. @param {string[][]} table Row-major grid, first row treated as the header. */
1779
- addTable(table, { hyperlink = true, citation = true, latex = true } = {}) {
1831
+ addTable(table, { hyperlink = true, citation = true, latex = true, id, replace, insertAt } = {}) {
1780
1832
  if (!Array.isArray(table)) {
1781
1833
  throw new TypeError('Table must be an array');
1782
1834
  }
1783
1835
 
1784
- const meta = AIRich.toTableMetadata(table, { hyperlink, citation, latex });
1836
+ const meta = AIRich.toTableMetadata(table, {
1837
+ hyperlink,
1838
+ citation,
1839
+ latex,
1840
+ });
1841
+
1842
+ const section = AIRich.newLayout('Single', {
1843
+ rows: meta.unified_rows,
1844
+ __typename: 'GenATableUXPrimitive',
1845
+ });
1785
1846
 
1786
- this._submessages.push({
1787
- messageType: 4,
1788
- tableMetadata: {
1789
- title: meta.title,
1790
- rows: meta.rows,
1847
+ const submessages = [
1848
+ {
1849
+ messageType: 4,
1850
+ tableMetadata: {
1851
+ title: meta.title,
1852
+ rows: meta.rows,
1853
+ },
1791
1854
  },
1855
+ ];
1856
+
1857
+ return this._addContent(section, submessages, {
1858
+ id,
1859
+ replace,
1860
+ insertAt,
1792
1861
  });
1862
+ }
1793
1863
 
1794
- this._sections.push(
1795
- AIRich.newLayout('Single', {
1796
- rows: meta.unified_rows,
1797
- __typename: 'GenATableUXPrimitive',
1798
- })
1799
- );
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
+ }
1800
1868
 
1801
- return this;
1802
- }
1869
+ const isStringArray = sources.every((item) => typeof item === 'string');
1870
+
1871
+ const isArrayFormat = sources.every((item) => Array.isArray(item) && item.every((value) => typeof value === 'string'));
1803
1872
 
1804
- /** Add a "Sources" strip. @param {string[]|string[][]} sources Flat list of urls, or `[title, url]` pairs. */
1805
- addSource(sources = [], { resolveUrl = false } = {}) {
1806
- if (!(Array.isArray(sources) && (sources.every((item) => typeof item === 'string') || sources.every((item) => Array.isArray(item) && item.every((v) => typeof v === 'string'))))) {
1807
- throw new TypeError('Sources must be a string array or an array of string arrays');
1873
+ const isObjectFormat = sources.every((item) => item && typeof item === 'object' && !Array.isArray(item));
1874
+
1875
+ if (!isStringArray && !isArrayFormat && !isObjectFormat) {
1876
+ throw new TypeError('Sources must be a string array, array of string arrays, or array of objects');
1808
1877
  }
1809
1878
 
1810
- if (sources.every((item) => typeof item === 'string')) {
1879
+ if (isStringArray) {
1811
1880
  sources = [sources];
1812
1881
  }
1813
1882
 
1814
- const source = sources.map(([icon, url, text]) => ({
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 }) => ({
1815
1904
  source_type: 'THIRD_PARTY',
1816
- source_display_name: text ?? '',
1817
- source_subtitle: 'AI',
1818
- source_url: url ?? '',
1905
+ source_display_name: title,
1906
+ source_subtitle: subtitle,
1907
+ source_url: url,
1819
1908
  favicon: {
1820
- url: Toolkit.resolveMedia(this.#client, icon ?? '', 'image', { resolveUrl }),
1909
+ url: Toolkit.resolveMedia(this.#client, icon, 'image'),
1821
1910
  mime_type: 'image/jpeg',
1822
1911
  width: 16,
1823
1912
  height: 16,
1824
1913
  },
1825
1914
  }));
1826
1915
 
1827
- this._sections.push(
1828
- AIRich.newLayout('Single', {
1829
- sources: source,
1830
- __typename: 'GenAISearchResultPrimitive',
1831
- })
1832
- );
1916
+ const submessage = this.createAlert('GenAISearchResultPrimitive');
1833
1917
 
1834
- return this;
1918
+ const section = AIRich.newLayout('Single', {
1919
+ sources: source,
1920
+ __typename: 'GenAISearchResultPrimitive',
1921
+ });
1922
+
1923
+ return this._addContent(section, submessage, {
1924
+ id,
1925
+ replace,
1926
+ insertAt,
1927
+ });
1835
1928
  }
1836
1929
 
1837
- /** Add a horizontally-scrollable reel of image/video items. */
1838
- addReels(reelsItems = [], { resolveUrl = false } = {}) {
1930
+ addReels(reelsItems = [], { id, replace, insertAt } = {}) {
1839
1931
  if (
1840
1932
  !(
1841
1933
  (reelsItems && typeof reelsItems === 'object' && !Array.isArray(reelsItems)) ||
@@ -1845,88 +1937,64 @@ class AIRich extends BaseBuilder {
1845
1937
  throw new TypeError('Reels items must be an object or an array of objects');
1846
1938
  }
1847
1939
 
1848
- if (!Array.isArray(reelsItems)) {
1849
- reelsItems = [reelsItems];
1850
- }
1940
+ const items = Array.isArray(reelsItems) ? reelsItems : [reelsItems];
1851
1941
 
1852
- const reels = reelsItems.map((item) => ({
1942
+ const reels = items.map((item) => ({
1853
1943
  ...item,
1854
- _avatar: Toolkit.resolveMedia(this.#client, item.profileIconUrl ?? item.profile_url ?? item.profile ?? '', 'image', { resolveUrl }),
1855
- _thumbnail: Toolkit.resolveMedia(this.#client, item.thumbnailUrl ?? item.thumbnail ?? '', 'image', { resolveUrl }),
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'),
1856
1946
  }));
1857
1947
 
1858
- this._submessages.push({
1859
- messageType: 9,
1860
- contentItemsMetadata: {
1861
- contentType: 1,
1862
- itemsMetadata: reels.map((item) => ({
1863
- reelItem: {
1864
- title: item.username ?? '',
1865
- profileIconUrl: item._avatar,
1866
- thumbnailUrl: item._thumbnail,
1867
- videoUrl: item.videoUrl ?? item.url ?? '',
1868
- },
1869
- })),
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
+ },
1870
1979
  },
1871
- });
1980
+ ];
1872
1981
 
1873
- reels.forEach((item, idx) => {
1874
- this._richResponseSources.push({
1875
- provider: 'Evernight AI',
1876
- thumbnailCDNURL: item._thumbnail,
1877
- sourceProviderURL: item.videoUrl ?? item.url ?? '',
1878
- sourceQuery: '',
1879
- faviconCDNURL: item._avatar,
1880
- citationNumber: idx + 1,
1881
- sourceTitle: item.username ?? '',
1882
- });
1982
+ return this._addContent(section, submessages, {
1983
+ id,
1984
+ replace,
1985
+ insertAt,
1883
1986
  });
1884
-
1885
- this._sections.push(
1886
- AIRich.newLayout(
1887
- 'HScroll',
1888
- reels.map((item) => ({
1889
- reels_url: item.videoUrl ?? item.url ?? '',
1890
- thumbnail_url: item._thumbnail,
1891
- creator: item.username ?? item.title ?? '',
1892
- avatar_url: item._avatar,
1893
- reels_title: item.reels_title ?? item.title ?? '',
1894
- likes_count: item.likes_count ?? item.like ?? 0,
1895
- shares_count: item.shares_count ?? item.share ?? 0,
1896
- view_count: item.view_count ?? item.view ?? 0,
1897
- reel_source: item.reel_source ?? item.source ?? 'IG',
1898
- is_verified: !!(item.is_verified || item.verified),
1899
- __typename: 'GenAIReelPrimitive',
1900
- }))
1901
- )
1902
- );
1903
-
1904
- return this;
1905
1987
  }
1906
1988
 
1907
- /** Add a full-width image (or grid of images if `imageUrl` is an array). */
1908
- /**
1909
- * @param {{ resolveUrl?: boolean, instant?: boolean|'only' }} [options]
1910
- * `instant: true` — sends BOTH: the GRID_IMAGE card (still shows WA's "can't verify"
1911
- * forwarded-download prompt, unavoidable per-design of botForwardedMessage) AND a plain
1912
- * (non-forwarded) imageMessage via send()'s inline-image fallback queue (`_inlineImages`,
1913
- * shared with addInlineImage()) that renders instantly with no prompt. Two images, by design.
1914
- * `instant: 'only'` — Vanz@Add (v4.9.2): skips building the GRID_IMAGE card entirely (no
1915
- * submessage, no GenAIImaginePrimitive section) and queues ONLY the plain imageMessage.
1916
- * One image, no prompt, nothing to download — use this when you don't need the rich card,
1917
- * just the picture to show up immediately.
1918
- */
1919
- addImage(imageUrl, { resolveUrl = false, instant = false } = {}) {
1989
+ addImage(imageUrl, { width, height, status = 'READY', update_text, resolveUrl = false, id, replace, insertAt } = {}) {
1920
1990
  if (!(typeof imageUrl === 'string' || Buffer.isBuffer(imageUrl) || (Array.isArray(imageUrl) && imageUrl.every((v) => typeof v === 'string' || Buffer.isBuffer(v))))) {
1921
1991
  throw new TypeError('imageUrl must be string | buffer | array of string/buffer');
1922
1992
  }
1923
- if (instant !== false && instant !== true && instant !== 'only') {
1924
- throw new TypeError(`instant must be false, true, or 'only' — got ${JSON.stringify(instant)}`);
1925
- }
1926
1993
 
1927
1994
  const list = Array.isArray(imageUrl)
1928
1995
  ? imageUrl.map((v) => {
1929
1996
  const url = Toolkit.resolveMedia(this.#client, v, 'image', { resolveUrl });
1997
+
1930
1998
  return {
1931
1999
  imagePreviewUrl: url,
1932
2000
  imageHighResUrl: url,
@@ -1935,6 +2003,7 @@ class AIRich extends BaseBuilder {
1935
2003
  })
1936
2004
  : (() => {
1937
2005
  const url = Toolkit.resolveMedia(this.#client, imageUrl, 'image', { resolveUrl });
2006
+
1938
2007
  return [
1939
2008
  {
1940
2009
  imagePreviewUrl: url,
@@ -1944,115 +2013,46 @@ class AIRich extends BaseBuilder {
1944
2013
  ];
1945
2014
  })();
1946
2015
 
1947
- const buildCard = instant !== 'only';
1948
-
1949
- if (buildCard) {
1950
- this._submessages.push({
1951
- messageType: 1,
1952
- gridImageMetadata: {
1953
- gridImageUrl: {
1954
- imagePreviewUrl: list[0]?.imagePreviewUrl,
1955
- },
1956
- imageUrls: list,
1957
- },
1958
- });
1959
- }
1960
-
1961
- list.forEach(({ imagePreviewUrl }) => {
1962
- if (buildCard) {
1963
- this._sections.push(
1964
- AIRich.newLayout('Single', {
1965
- media: {
1966
- url: imagePreviewUrl,
1967
- mime_type: 'image/png',
1968
- },
1969
- imagine_type: 'IMAGE',
1970
- status: { status: 'READY' },
1971
- __typename: 'GenAIImaginePrimitive',
1972
- })
1973
- );
1974
- }
1975
-
1976
- if (instant) {
1977
- this._inlineImages.push({ url: imagePreviewUrl, caption: undefined });
1978
- }
1979
- });
1980
-
1981
- return this;
1982
- }
1983
-
1984
- // Vanz@Fix 15-08-26 (bug 41) --- addImage() only builds GRID_IMAGE (messageType 1).
1985
- // There was no helper for standalone INLINE_IMAGE (messageType 3): callers were manually
1986
- // pushing addSubmessage() (correct proto shape) + addSection() (WRONG shape — reused the
1987
- // GRID_IMAGE/GenAIImaginePrimitive section schema instead of GenAIInlineImageUXPrimitive),
1988
- // which broke client-side unifiedResponse rendering even though the submessage itself was fine.
1989
- // Mirrors RichSubMessageType.INLINE_IMAGE handling in rich-message-utils.js's toUnified().
1990
- /** 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). */
1991
- addInlineImage(imageUrl, { text = '', alignment = 'center', tapLinkUrl = '', resolveUrl = false } = {}) {
1992
- if (!(typeof imageUrl === 'string' || Buffer.isBuffer(imageUrl) || (imageUrl && typeof imageUrl === 'object'))) {
1993
- throw new TypeError('imageUrl must be string | buffer | { imagePreviewUrl, imageHighResUrl, sourceUrl }');
1994
- }
1995
-
1996
- const ALIGNMENT_ENUM = { leading: 0, trailing: 1, center: 2 };
1997
- const ALIGNMENT_NAME = ['AI_RICH_RESPONSE_IMAGE_LAYOUT_LEADING_ALIGNED', 'AI_RICH_RESPONSE_IMAGE_LAYOUT_TRAILING_ALIGNED', 'AI_RICH_RESPONSE_IMAGE_LAYOUT_CENTER_ALIGNED'];
1998
- const alignmentNum = typeof alignment === 'number' ? alignment : (ALIGNMENT_ENUM[String(alignment).toLowerCase()] ?? ALIGNMENT_ENUM.center);
1999
-
2000
- const url =
2001
- imageUrl && typeof imageUrl === 'object'
2002
- ? {
2003
- imagePreviewUrl: imageUrl.imagePreviewUrl || imageUrl.url,
2004
- imageHighResUrl: imageUrl.imageHighResUrl || imageUrl.url,
2005
- sourceUrl: imageUrl.sourceUrl || imageUrl.url,
2006
- }
2007
- : (() => {
2008
- const resolved = Toolkit.resolveMedia(this.#client, imageUrl, 'image', { resolveUrl });
2009
- return { imagePreviewUrl: resolved, imageHighResUrl: resolved, sourceUrl: resolved };
2010
- })();
2011
-
2012
- this._submessages.push({
2013
- messageType: 3,
2014
- imageMetadata: {
2015
- imageUrl: url,
2016
- imageText: text,
2017
- alignment: alignmentNum,
2018
- tapLinkUrl,
2019
- },
2020
- });
2021
-
2022
- this._sections.push(
2016
+ const sections = list.map(({ imagePreviewUrl }) =>
2023
2017
  AIRich.newLayout('Single', {
2024
- image_url: {
2025
- image_preview_url: url.imagePreviewUrl || '',
2026
- image_high_res_url: url.imageHighResUrl || '',
2027
- source_url: url.sourceUrl || '',
2018
+ media: {
2019
+ url: imagePreviewUrl,
2020
+ mime_type: 'image/png',
2021
+ width,
2022
+ height,
2028
2023
  },
2029
- image_text: text,
2030
- alignment: ALIGNMENT_NAME[alignmentNum],
2031
- tap_link_url: tapLinkUrl,
2032
- __typename: 'GenAIInlineImageUXPrimitive',
2024
+ imagine_type: 'IMAGE',
2025
+ status: {
2026
+ status,
2027
+ update_text,
2028
+ },
2029
+ __typename: 'GenAIImaginePrimitive',
2033
2030
  })
2034
2031
  );
2035
2032
 
2036
- // Vanz@Fix (bug 42): stash for the imageMessage fallback in send()
2037
- this._inlineImages.push({
2038
- url: url.sourceUrl || url.imageHighResUrl || url.imagePreviewUrl,
2039
- caption: text || undefined,
2040
- });
2033
+ const submessage = {
2034
+ messageType: 1,
2035
+ gridImageMetadata: {
2036
+ gridImageUrl: {
2037
+ imagePreviewUrl: list[0]?.imagePreviewUrl,
2038
+ },
2039
+ imageUrls: list,
2040
+ },
2041
+ };
2041
2042
 
2042
- return this;
2043
+ if (id && sections.length !== 1) {
2044
+ throw new Error('Cannot assign one id to multiple image sections');
2045
+ }
2046
+
2047
+ return this._addContent(sections, submessage, {
2048
+ id,
2049
+ replace,
2050
+ insertAt,
2051
+ });
2043
2052
  }
2044
2053
 
2045
- // Vanz@Perf 15-08-26 --- autoFill defaults to false (arslan-baileys behavior): skips the
2046
- // fetch-full-video + ffmpeg-frame-extraction + duration-parse round trip per video, which
2047
- // was the main source of blurose's slower response time. Pass { autoFill: true } to opt
2048
- // back into the complete/slow path (real thumbnail + duration + file_length).
2049
- // Vanz@Fix 23-08-26 --- addVideo() had no resolveUrl option at all (unlike addImage()), so the
2050
- // video url always stayed a raw external link, which stock WA clients show a "download" state
2051
- // for before rendering. Mirrors addImage()'s { resolveUrl } — when true, the url is uploaded to
2052
- // WA's own media server first via Toolkit.toUrl() so it renders instantly like WA-native media.
2053
- /** Add a video block. */
2054
- addVideo(videoUrl, { autoFill = false, resolveUrl = false } = {}) {
2055
- const isObjectVideo = (v) => v && typeof v === 'object' && v.url;
2054
+ addVideo(videoUrl, { autoFill = true, status = 'READY', estimatedTime, id, replace, insertAt } = {}) {
2055
+ const isObjectVideo = (v) => v && typeof v === 'object' && !Array.isArray(v) && v.url;
2056
2056
 
2057
2057
  const isValidPrimitive =
2058
2058
  typeof videoUrl === 'string' ||
@@ -2066,17 +2066,15 @@ class AIRich extends BaseBuilder {
2066
2066
 
2067
2067
  const items = Array.isArray(videoUrl) ? videoUrl : [videoUrl];
2068
2068
 
2069
- this._submessages.push({
2070
- messageType: 2,
2071
- messageText: '[ Video tidak dapat dimuat ]',
2072
- });
2069
+ const alert = this.createAlert('GenAIImaginePrimitive (ANIMATE)');
2070
+
2071
+ const sections = [];
2072
+ const submessages = [];
2073
2073
 
2074
- items.forEach((item) => {
2074
+ for (const item of items) {
2075
2075
  const isObject = isObjectVideo(item);
2076
2076
 
2077
- const url = isObject
2078
- ? Toolkit.resolveMedia(this.#client, item.url ?? '', 'video', { resolveUrl })
2079
- : Toolkit.resolveMedia(this.#client, item, 'video', { resolveUrl });
2077
+ const url = isObject ? Toolkit.resolveMedia(this.#client, item.url ?? '', 'video') : Toolkit.resolveMedia(this.#client, item, 'video');
2080
2078
 
2081
2079
  const bufferPromise = autoFill ? Promise.resolve(url).then((u) => Toolkit.fetchBuffer(u)) : null;
2082
2080
 
@@ -2102,17 +2100,15 @@ class AIRich extends BaseBuilder {
2102
2100
  height: 300,
2103
2101
  })
2104
2102
  : autoFill
2105
- ? bufferPromise
2106
- ? bufferPromise.then((b) =>
2107
- Toolkit.getMp4Preview(b, {
2108
- time: 0,
2109
- result: 'base64',
2110
- })
2111
- )
2112
- : null
2103
+ ? bufferPromise?.then((b) =>
2104
+ Toolkit.getMp4Preview(b, {
2105
+ time: 0,
2106
+ result: 'base64',
2107
+ })
2108
+ )
2113
2109
  : null;
2114
2110
 
2115
- this._sections.push(
2111
+ sections.push(
2116
2112
  AIRich.newLayout('Single', {
2117
2113
  media: {
2118
2114
  url,
@@ -2121,34 +2117,37 @@ class AIRich extends BaseBuilder {
2121
2117
  duration,
2122
2118
  },
2123
2119
  imagine_type: 'ANIMATE',
2124
- status: { status: 'READY' },
2120
+ status: {
2121
+ status,
2122
+ estimated_completion_time: estimatedTime != null ? Math.floor((Date.now() + estimatedTime) / 1000) : undefined,
2123
+ },
2125
2124
  thumbnail: {
2126
2125
  raw_media: thumbnail,
2127
2126
  },
2128
2127
  __typename: 'GenAIImaginePrimitive',
2129
2128
  })
2130
2129
  );
2131
- });
2132
-
2133
- return this;
2134
- }
2130
+ }
2135
2131
 
2136
- /** Add an inline product card (or array of cards). Each item needs at least a `title`. */
2137
- addProduct(data = {}, { resolveUrl = false } = {}) {
2138
- if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
2139
- throw new TypeError('Product items must be an object or an array of objects');
2132
+ if (alert !== undefined) {
2133
+ submessages.push(alert);
2140
2134
  }
2141
2135
 
2142
- const itemsToCheck = Array.isArray(data) ? data : [data];
2143
- const missingTitleAt = itemsToCheck.findIndex((item) => !item.title);
2144
- if (missingTitleAt !== -1) {
2145
- throw new TypeError(`addProduct() item[${missingTitleAt}] is missing a required "title"`);
2136
+ if (submessages.length > 1) {
2137
+ throw new Error('Video content can only have one submessage');
2146
2138
  }
2147
2139
 
2148
- this._submessages.push({
2149
- messageType: 2,
2150
- messageText: '[ Produk tidak dapat dimuat ]',
2140
+ return this._addContent(sections, submessages[0], {
2141
+ id,
2142
+ replace,
2143
+ insertAt,
2151
2144
  });
2145
+ }
2146
+
2147
+ addProduct(data = {}, { id, replace, insertAt } = {}) {
2148
+ if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
2149
+ throw new TypeError('Product items must be an object or an array of objects');
2150
+ }
2152
2151
 
2153
2152
  const items = Array.isArray(data) ? data : [data];
2154
2153
 
@@ -2159,41 +2158,41 @@ class AIRich extends BaseBuilder {
2159
2158
  sale_price: item.sale_price,
2160
2159
  product_url: item.product_url ?? item.url,
2161
2160
  image: {
2162
- url: Toolkit.resolveMedia(this.#client, item.image_url ?? item.image, 'image', { resolveUrl }),
2161
+ url: Toolkit.resolveMedia(this.#client, item.image_url ?? item.image, 'image'),
2163
2162
  },
2164
2163
  additional_images: [
2165
2164
  {
2166
- url: Toolkit.resolveMedia(this.#client, item.icon_url ?? item.icon, 'image', { resolveUrl }),
2165
+ url: Toolkit.resolveMedia(this.#client, item.icon_url ?? item.icon, 'image'),
2167
2166
  },
2168
2167
  ],
2169
2168
  __typename: 'GenAIProductItemCardPrimitive',
2170
2169
  }));
2171
2170
 
2172
- this._sections.push(AIRich.newLayout(Array.isArray(data) ? 'HScroll' : 'Single', Array.isArray(data) ? product : product[0]));
2171
+ const section = AIRich.newLayout(Array.isArray(data) ? 'HScroll' : 'Single', Array.isArray(data) ? product : product[0]);
2173
2172
 
2174
- return this;
2173
+ const submessage = this.createAlert('GenAIProductItemCardPrimitive');
2174
+
2175
+ return this._addContent(section, submessage, {
2176
+ id,
2177
+ replace,
2178
+ insertAt,
2179
+ });
2175
2180
  }
2176
2181
 
2177
- /** Add an inline social-post style card (or array of cards). */
2178
- addPost(data = {}, { resolveUrl = false } = {}) {
2182
+ addPost(data = {}, { id, replace, insertAt } = {}) {
2179
2183
  if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
2180
2184
  throw new TypeError('Post items must be an object or an array of objects');
2181
2185
  }
2182
2186
 
2183
2187
  const posts = Array.isArray(data) ? data : [data];
2184
2188
 
2185
- this._submessages.push({
2186
- messageType: 2,
2187
- messageText: '[ Postingan tidak dapat dimuat ]',
2188
- });
2189
-
2190
2189
  const primitives = posts.map((p) => ({
2191
2190
  title: p.title ?? '',
2192
2191
  subtitle: p.subtitle ?? '',
2193
2192
  username: p.username ?? '',
2194
- profile_picture_url: Toolkit.resolveMedia(this.#client, p.profile_picture_url ?? p.profile_url ?? p.profile ?? '', 'image', { resolveUrl }),
2193
+ profile_picture_url: Toolkit.resolveMedia(this.#client, p.profile_picture_url ?? p.profile_url ?? p.profile ?? '', 'image'),
2195
2194
  is_verified: !!(p.is_verified || p.verified),
2196
- thumbnail_url: Toolkit.resolveMedia(this.#client, p.thumbnail_url ?? p.thumbnail ?? '', 'image', { resolveUrl }),
2195
+ thumbnail_url: Toolkit.resolveMedia(this.#client, p.thumbnail_url ?? p.thumbnail ?? '', 'image'),
2197
2196
  post_caption: p.post_caption ?? p.caption ?? '',
2198
2197
  likes_count: p.likes_count ?? p.like ?? 0,
2199
2198
  comments_count: p.comments_count ?? p.comment ?? 0,
@@ -2202,419 +2201,151 @@ class AIRich extends BaseBuilder {
2202
2201
  post_deeplink: p.post_deeplink ?? p.deeplink ?? '',
2203
2202
  source_app: p.source_app || p.source || 'INSTAGRAM',
2204
2203
  footer_label: p.footer_label ?? p.footer ?? '',
2205
- footer_icon: Toolkit.resolveMedia(this.#client, p.footer_icon ?? p.icon ?? '', 'image', { resolveUrl }),
2204
+ footer_icon: Toolkit.resolveMedia(this.#client, p.footer_icon ?? p.icon ?? '', 'image'),
2206
2205
  is_carousel: posts.length > 1,
2207
2206
  orientation: p.orientation ?? 'LANDSCAPE',
2208
2207
  post_type: p.post_type ?? 'VIDEO',
2209
2208
  __typename: 'GenAIPostPrimitive',
2210
2209
  }));
2211
2210
 
2212
- this._sections.push(AIRich.newLayout('HScroll', primitives));
2211
+ const section = AIRich.newLayout('HScroll', primitives);
2213
2212
 
2214
- return this;
2213
+ const submessage = this.createAlert('GenAIPostPrimitive');
2214
+
2215
+ return this._addContent(section, submessage, {
2216
+ id,
2217
+ replace,
2218
+ insertAt,
2219
+ });
2215
2220
  }
2216
2221
 
2217
- // Vanz@Add 24-08-26 --- ported from temen's MessageBuilderV4.7 (setResponseId/setBotResponseId/
2218
- // refreshResponseId/refreshBotResponseId), rewritten for this fork. Pins the two ids build()
2219
- // generates (see constructor comment) so a rebuilt message can reuse the same response_id/
2220
- // botResponseId — needed for editing an already-sent AIRich message in place.
2222
+ addMetadata(text, { id, replace, insertAt } = {}) {
2223
+ if (typeof text !== 'string') {
2224
+ throw new TypeError('Text must be a string');
2225
+ }
2221
2226
 
2222
- /** 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. */
2223
- setResponseId(id) {
2224
- if (typeof id !== 'string' || !id) throw new TypeError('setResponseId(id) requires a non-empty string');
2225
- this._responseId = id;
2226
- return this;
2227
- }
2227
+ const section = AIRich.newLayout('Single', {
2228
+ text,
2229
+ __typename: 'GenAIMetadataTextPrimitive',
2230
+ });
2228
2231
 
2229
- /** Un-pin `unifiedResponse.response_id`, generating a fresh crypto.randomUUID() immediately (not deferred to the next build()). */
2230
- refreshResponseId() {
2231
- this._responseId = crypto.randomUUID();
2232
- return this;
2233
- }
2232
+ const submessage = {
2233
+ messageType: 2,
2234
+ messageText: text,
2235
+ };
2234
2236
 
2235
- /** Pin `botMetadata.botResponseId` to a specific value instead of a fresh random one each build(). */
2236
- setBotResponseId(id) {
2237
- if (typeof id !== 'string' || !id) throw new TypeError('setBotResponseId(id) requires a non-empty string');
2238
- this._botResponseId = id;
2239
- return this;
2237
+ return this._addContent(section, submessage, {
2238
+ id,
2239
+ replace,
2240
+ insertAt,
2241
+ });
2240
2242
  }
2241
2243
 
2242
- /** Un-pin `botMetadata.botResponseId`, generating a fresh crypto.randomUUID() immediately. */
2243
- refreshBotResponseId() {
2244
- this._botResponseId = crypto.randomUUID();
2245
- return this;
2246
- }
2244
+ addTip(text, { id, replace, insertAt } = {}) {
2245
+ if (typeof text !== 'string') {
2246
+ throw new TypeError('Text must be a string');
2247
+ }
2247
2248
 
2248
- /** 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. */
2249
- addMetadata(text) {
2250
- if (typeof text !== 'string' || !text) throw new TypeError('addMetadata(text) requires a non-empty string');
2249
+ const section = AIRich.newLayout('Single', {
2250
+ text: 'ⓘ ' + text,
2251
+ __typename: 'GenAIMetadataTextPrimitive',
2252
+ });
2251
2253
 
2252
- this._submessages.push({
2254
+ const submessage = {
2253
2255
  messageType: 2,
2254
2256
  messageText: text,
2255
- });
2256
-
2257
- this._sections.push(
2258
- AIRich.newLayout('Single', {
2259
- text,
2260
- __typename: 'GenAIMetadataTextPrimitive',
2261
- })
2262
- );
2263
-
2264
- return this;
2265
- }
2266
-
2267
- /** Add a small "tip" callout banner. @param {string} text */
2268
- addTip(text) {
2269
- if (typeof text !== 'string' || !text) {
2270
- throw new TypeError('addTip(text) requires a non-empty string');
2271
- }
2257
+ };
2272
2258
 
2273
- this._submessages.push({
2274
- messageType: 2,
2275
- messageText: text,
2259
+ return this._addContent(section, submessage, {
2260
+ id,
2261
+ replace,
2262
+ insertAt,
2276
2263
  });
2277
-
2278
- this._sections.push(
2279
- AIRich.newLayout('Single', {
2280
- text,
2281
- __typename: 'GenAIMetadataTextPrimitive',
2282
- })
2283
- );
2284
-
2285
- return this;
2286
2264
  }
2287
2265
 
2288
- // Vanz@Add 22-08-26 (v4.7) --- addHeading/addImageCard/addWidget/addFooterAction: 4 primitives
2289
- // reverse-engineered from captured Meta-AI-in-WhatsApp traffic that this project's own crm/snip
2290
- // tooling (see rich-message-utils.js) dumps for study. Not in any public Baileys schema, so
2291
- // unknown enum values (kind/state on addWidget's ctas) are passed through as observed rather
2292
- // than guessed at, and documented as experimental below.
2293
-
2294
- /** Add a large heading-style text block (`FOATextPrimitive`) — visually distinct from `addText()`'s regular paragraph text. */
2295
- addHeading(text) {
2296
- if (typeof text !== 'string' || !text) {
2297
- throw new TypeError('addHeading(text) requires a non-empty string');
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');
2298
2269
  }
2299
2270
 
2300
- this._submessages.push({
2301
- messageType: 2,
2302
- messageText: text,
2303
- });
2304
-
2305
- this._sections.push(
2306
- AIRich.newLayout('Single', {
2307
- text,
2308
- __typename: 'FOATextPrimitive',
2309
- })
2310
- );
2311
-
2312
- return this;
2313
- }
2271
+ const isArray = Array.isArray(data);
2314
2272
 
2315
- /**
2316
- * Add a "ready" static image card (`GenAIImagePrimitive`: preview + full-res, no generating/status
2317
- * state) — distinct from `addImage()`'s AI-generation-style `GenAIImaginePrimitive`.
2318
- * @param {string|Buffer} previewUrl Preview/thumbnail image.
2319
- * @param {string|Buffer} [fullUrl] Full-resolution image; defaults to `previewUrl`.
2320
- */
2321
- addImageCard(previewUrl, fullUrl = previewUrl, { resolveUrl = false } = {}) {
2322
- if (!(typeof previewUrl === 'string' || Buffer.isBuffer(previewUrl))) {
2323
- throw new TypeError('addImageCard(previewUrl) requires a string url or buffer');
2324
- }
2273
+ const items = isArray ? data : [data];
2325
2274
 
2326
- const preview = Toolkit.resolveMedia(this.#client, previewUrl, 'image', { resolveUrl });
2327
- const full = fullUrl === previewUrl ? preview : Toolkit.resolveMedia(this.#client, fullUrl, 'image', { resolveUrl });
2275
+ const widgets = items.map((item) => ({
2276
+ __typename: 'GenAI3PExtWidgetPrimitive',
2328
2277
 
2329
- this._submessages.push({
2330
- messageType: 1,
2331
- gridImageMetadata: {
2332
- gridImageUrl: { imagePreviewUrl: preview },
2333
- imageUrls: [{ imagePreviewUrl: preview, imageHighResUrl: full, sourceUrl: full }],
2278
+ header: {
2279
+ __typename: 'GenAI3PExtWidgetStandardHeader',
2280
+ title: item.title ?? '',
2281
+ ...(item.header ?? {}),
2334
2282
  },
2335
- });
2336
-
2337
- this._sections.push(
2338
- AIRich.newLayout('Single', {
2339
- preview_image: { url: preview, mime_type: 'image/jpeg', __typename: 'GenAIMediaItem' },
2340
- full_image: { url: full, mime_type: 'image/jpeg', __typename: 'GenAIMediaItem' },
2341
- __typename: 'GenAIImagePrimitive',
2342
- })
2343
- );
2344
2283
 
2345
- return this;
2346
- }
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
+ })),
2347
2302
 
2348
- /**
2349
- * Add a "3P extension" widget card (`GenAI3PExtWidgetPrimitive`) — a small panel with a title and
2350
- * a row of tappable CTA chips. Per captured traffic these CTAs call back into a tool (`tool_call_id`)
2351
- * rather than opening a url; `kind`/`state` semantics beyond the observed `'OTHER'`/`'PENDING'`
2352
- * defaults aren't publicly documented, so treat this as experimental.
2353
- *
2354
- * Vanz@Add (v4.8) --- accepts an `{ layout }` override so consecutive `addWidget()` calls can
2355
- * pick different renderings (e.g. one `HScroll` row, one `ActionRow` stack) instead of always
2356
- * inferring HScroll-for-array/Single-for-object from the shape of `data`. Also accepts either
2357
- * `ctas` (original key, matches the wire field) or `actions` (alias) on each item — whichever
2358
- * is present is used; `ctas` wins if both are somehow given.
2359
- * @param {Record<string, any>|Record<string, any>[]} data `{ title, ctas|actions: [{ label, tool_call_id?, kind?, state?, toast? }] }` (single or array).
2360
- * @param {{layout?: 'Single'|'HScroll'|'ActionRow'|string}} [options] `layout` overrides the default single/array inference.
2361
- */
2362
- addWidget(data = {}, { layout } = {}) {
2363
- const items = Array.isArray(data) ? data : [data];
2303
+ ...(item.body ?? {}),
2304
+ },
2305
+ }));
2364
2306
 
2365
- items.forEach((item, i) => {
2366
- if (!item?.title) {
2367
- throw new TypeError(`addWidget() item[${i}] is missing a required "title"`);
2368
- }
2369
- const ctas = item.ctas ?? item.actions;
2370
- if (!Array.isArray(ctas) || !ctas.length) {
2371
- throw new TypeError(`addWidget() item[${i}] requires a non-empty "ctas" (or "actions") array`);
2372
- }
2373
- });
2307
+ const section = AIRich.newLayout(layout ?? (isArray ? 'HScroll' : 'Single'), isArray ? widgets : widgets[0], options);
2374
2308
 
2375
- this._submessages.push({
2376
- messageType: 2,
2377
- messageText: items.map((item) => item.title).join(', '),
2378
- });
2309
+ const submessage = this.createAlert('GenAI3PExtWidgetStandardHeader');
2379
2310
 
2380
- const widgets = items.map((item) => {
2381
- const ctas = item.ctas ?? item.actions;
2382
- return {
2383
- header: { title: item.title, __typename: 'GenAI3PExtWidgetStandardHeader' },
2384
- body: {
2385
- sections: item.sections ?? [],
2386
- ctas: ctas.map((cta, idx) => ({
2387
- label: cta.label ?? '',
2388
- state: cta.state ?? 'PENDING',
2389
- kind: cta.kind ?? 'OTHER',
2390
- tool_call_id: cta.tool_call_id ?? String(idx).padStart(2, '0'),
2391
- ...(cta.toast !== false && {
2392
- toast: { label: typeof cta.toast === 'string' ? cta.toast : item.title, __typename: 'GenAI3PExtWidgetToast' },
2393
- }),
2394
- __typename: 'GenAI3PExtWidgetCTA',
2395
- })),
2396
- __typename: item.body_typename ?? 'GenAI3PExtCalendarEventList',
2397
- },
2398
- __typename: 'GenAI3PExtWidgetPrimitive',
2399
- };
2311
+ return this._addContent(section, submessage, {
2312
+ id,
2313
+ replace,
2314
+ insertAt,
2400
2315
  });
2401
-
2402
- const resolvedLayout = layout ?? (Array.isArray(data) ? 'HScroll' : 'Single');
2403
- const asArray = resolvedLayout !== 'Single';
2404
-
2405
- this._sections.push(AIRich.newLayout(resolvedLayout, asArray ? widgets : widgets[0]));
2406
-
2407
- return this;
2408
2316
  }
2409
2317
 
2410
- /**
2411
- * Add footer action link(s) (`GenAIFooterActionPrimitive`) e.g. "Join our WhatsApp Group/Channel"
2412
- * chips shown below the response, separate from `setFooter()`'s plain text footer.
2413
- * @param {{text: string, url: string, type?: string}|{text: string, url: string, type?: string}[]} actions
2414
- */
2415
- addFooterAction(actions) {
2416
- const items = Array.isArray(actions) ? actions : [actions];
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
+ }
2417
2322
 
2418
- items.forEach((item, i) => {
2419
- if (!item?.text || !item?.url) {
2420
- throw new TypeError(`addFooterAction() item[${i}] requires both "text" and "url"`);
2421
- }
2422
- });
2323
+ const isArray = Array.isArray(data);
2423
2324
 
2424
- const primitives = items.map((item) => ({
2425
- cta_text: item.text,
2426
- cta_type: item.type ?? 'OPEN_URL',
2427
- cta_url: item.url,
2325
+ const items = isArray ? data : [data];
2326
+
2327
+ const actions = items.map((item) => ({
2428
2328
  __typename: 'GenAIFooterActionPrimitive',
2429
- }));
2430
2329
 
2431
- this._sections.push(AIRich.newLayout('HScroll', primitives));
2330
+ cta_text: item.text ?? item.cta_text ?? '',
2432
2331
 
2433
- return this;
2434
- }
2332
+ cta_type: item.type ?? item.cta_type ?? 'OPEN_URL',
2435
2333
 
2436
- // Vanz@Add (v4.8) --- 8 primitives from the 20-item reference test script that had no
2437
- // add*() helper yet (Divider/Spacer/Task/ProgressStatus/ThinkingStatus/QuotaUpsell/FOABloks
2438
- // have no dedicated AIRichResponseSubMessageType — WA carries them purely in the
2439
- // unifiedResponse view-model JSON, so their submessage falls back to plain AI_RICH_RESPONSE_TEXT
2440
- // like addTip/addHeading already do. Latex is the one exception: it has a real proto type
2441
- // (AI_RICH_RESPONSE_LATEX = 8, confirmed in WAProto) with its own latexMetadata, so that one
2442
- // gets a proper submessage instead of the text fallback.
2334
+ cta_url: item.url ?? item.cta_url ?? '',
2335
+ }));
2443
2336
 
2444
- /** Add a plain horizontal divider line (`GenAIDividerPrimitive`, no content). */
2445
- addDivider() {
2446
- this._submessages.push({ messageType: 2, messageText: '---' });
2447
- this._sections.push(AIRich.newLayout('Single', { __typename: 'GenAIDividerPrimitive' }));
2448
- return this;
2449
- }
2337
+ const section = AIRich.newLayout(layout ?? (isArray ? 'HScroll' : 'Single'), isArray ? actions : actions[0], options);
2450
2338
 
2451
- /** Add blank vertical spacing (`GenAISpacerPrimitive`). @param {number} [spacing=1] Spacing unit, per observed traffic. */
2452
- addSpacer(spacing = 1) {
2453
- if (typeof spacing !== 'number' || spacing < 0) {
2454
- throw new TypeError('addSpacer(spacing) requires a non-negative number');
2455
- }
2456
- this._submessages.push({ messageType: 2, messageText: `spasi ${spacing}` });
2457
- this._sections.push(AIRich.newLayout('Single', { spacing, __typename: 'GenAISpacerPrimitive' }));
2458
- return this;
2459
- }
2339
+ const submessage = this.createAlert('GenAIFooterActionPrimitive');
2460
2340
 
2461
- /**
2462
- * Add a rendered LaTeX expression (`GenAILatexUXPrimitive`), with a real `AI_RICH_RESPONSE_LATEX`
2463
- * submessage (unlike most primitives in this block, this one has a proper proto type).
2464
- * @param {string} expression LaTeX source, e.g. `'$$E = mc^2$$'`.
2465
- */
2466
- addLatex(expression) {
2467
- if (typeof expression !== 'string' || !expression) {
2468
- throw new TypeError('addLatex(expression) requires a non-empty string');
2469
- }
2470
- this._submessages.push({
2471
- messageType: 8,
2472
- latexMetadata: { text: expression, expressions: [{ latexExpression: expression }] },
2341
+ return this._addContent(section, submessage, {
2342
+ id,
2343
+ replace,
2344
+ insertAt,
2473
2345
  });
2474
- this._sections.push(AIRich.newLayout('Single', { latex_expression: expression, __typename: 'GenAILatexUXPrimitive' }));
2475
- return this;
2476
- }
2477
-
2478
- /**
2479
- * Add a task/checklist card (`GenAITaskPrimitive`).
2480
- * @param {{task_id?: string, title: string, subtitle?: string, status?: string}} data
2481
- */
2482
- addTask(data = {}) {
2483
- if (!data?.title) {
2484
- throw new TypeError('addTask() requires a "title"');
2485
- }
2486
- this._submessages.push({ messageType: 2, messageText: `Tugas: ${data.title}` });
2487
- this._sections.push(
2488
- AIRich.newLayout('Single', {
2489
- task_id: data.task_id ?? '',
2490
- title: data.title,
2491
- subtitle: data.subtitle ?? '',
2492
- status: data.status ?? 'IN_PROGRESS',
2493
- __typename: 'GenAITaskPrimitive',
2494
- })
2495
- );
2496
- // Safety net: GenAITaskPrimitive is a custom AI-only component the stock WA client
2497
- // doesn't render visibly. Append a plain text section so the task is still visible.
2498
- // Set data.textFallback = false to skip.
2499
- if (data.textFallback !== false) {
2500
- const fallbackText = data.subtitle ? `${data.title} — ${data.subtitle}` : data.title;
2501
- this._sections.push(AIRich.newLayout('Single', { text: `Tugas: ${fallbackText}`, __typename: 'FOATextPrimitive' }));
2502
- }
2503
- return this;
2504
2346
  }
2505
2347
 
2506
- /**
2507
- * Add a "searching/working" progress banner (`GenAIBotProgressStatusPrimitive`) — a one-shot
2508
- * status chip (unlike `addSuggest`, this isn't tappable). Distinct from `addThinkingStatus()`'s
2509
- * icon/typename.
2510
- * @param {string} title
2511
- * @param {{icon?: string, is_in_progress?: boolean}} [options]
2512
- */
2513
- addProgressStatus(title, { icon = 'SEARCH', is_in_progress = true, target_secondary_screen_id, target_secondary_screen_tab_id } = {}) {
2514
- if (typeof title !== 'string' || !title) {
2515
- throw new TypeError('addProgressStatus(title) requires a non-empty string');
2516
- }
2517
- this._submessages.push({ messageType: 2, messageText: title });
2518
- const primitive = {
2519
- title,
2520
- icon,
2521
- is_in_progress,
2522
- meta_search_apps: [],
2523
- __typename: 'GenAIBotProgressStatusPrimitive',
2524
- };
2525
- // NOTE: these two fields must be OMITTED when unset, not sent as `null` —
2526
- // an explicit null here was reproducibly crashing the WA client renderer
2527
- // on group-open/media-download. Only include when the caller actually passes one.
2528
- if (target_secondary_screen_id != null) primitive.target_secondary_screen_id = target_secondary_screen_id;
2529
- if (target_secondary_screen_tab_id != null) primitive.target_secondary_screen_tab_id = target_secondary_screen_tab_id;
2530
- this._sections.push(AIRich.newLayout('Single', primitive));
2531
- return this;
2532
- }
2533
-
2534
- /** Add a "thinking" status banner (`GenAIBotThinkingStatusPrimitive`). See `addProgressStatus()`. */
2535
- addThinkingStatus(title, { icon = 'THINKING', is_in_progress = true, target_secondary_screen_id, target_secondary_screen_tab_id, textFallback = true } = {}) {
2536
- if (typeof title !== 'string' || !title) {
2537
- throw new TypeError('addThinkingStatus(title) requires a non-empty string');
2538
- }
2539
- this._submessages.push({ messageType: 2, messageText: title });
2540
- const primitive = {
2541
- title,
2542
- icon,
2543
- is_in_progress,
2544
- meta_search_apps: [],
2545
- __typename: 'GenAIBotThinkingStatusPrimitive',
2546
- };
2547
- // Same crash-avoidance rule as addProgressStatus(): omit, never null.
2548
- if (target_secondary_screen_id != null) primitive.target_secondary_screen_id = target_secondary_screen_id;
2549
- if (target_secondary_screen_tab_id != null) primitive.target_secondary_screen_tab_id = target_secondary_screen_tab_id;
2550
- this._sections.push(AIRich.newLayout('Single', primitive));
2551
- // Safety net: stock WA client doesn't render this primitive's own view (it's meant
2552
- // as a transient spinner in the official app), so the card shows blank when forwarded.
2553
- // Append a plain text section so the title is still visible. Set { textFallback: false } to skip.
2554
- if (textFallback) {
2555
- this._sections.push(AIRich.newLayout('Single', { text: title, __typename: 'FOATextPrimitive' }));
2556
- }
2557
- return this;
2558
- }
2559
-
2560
- /**
2561
- * Add a subscription-quota-limit upsell card (`GenAIMetaSubsQuotaUpsellPrimitive`).
2562
- * @param {{title: string, body?: string, body_line1?: string, body_line2?: string, buttons?: {label: string, action?: string, deeplink?: string}[]}} data
2563
- */
2564
- addQuotaUpsell(data = {}) {
2565
- if (!data?.title) {
2566
- throw new TypeError('addQuotaUpsell() requires a "title"');
2567
- }
2568
- this._submessages.push({ messageType: 2, messageText: data.title });
2569
- this._sections.push(
2570
- AIRich.newLayout('Single', {
2571
- title: data.title,
2572
- body: data.body ?? '',
2573
- body_line1: data.body_line1 ?? '',
2574
- body_line2: data.body_line2 ?? '',
2575
- buttons: (data.buttons ?? []).map((b) => ({
2576
- label: b.label ?? '',
2577
- action: b.action ?? 'OPEN_DEEPLINK',
2578
- deeplink: b.deeplink ?? '',
2579
- })),
2580
- __typename: 'GenAIMetaSubsQuotaUpsellPrimitive',
2581
- })
2582
- );
2583
- return this;
2584
- }
2585
-
2586
- /**
2587
- * Add a raw Bloks payload (`FOABloksPrimitive`) — Meta's internal UI-description format.
2588
- * Escape hatch: field meaning beyond what's passed through is undocumented, so this is the
2589
- * most experimental primitive in this block; pass whatever your captured traffic shows.
2590
- * @param {{type: string, data: string, uuid?: string, initial_response?: any, versioning_id?: string}} data
2591
- */
2592
- addBloks(data = {}) {
2593
- if (!data?.type) {
2594
- throw new TypeError('addBloks() requires a "type"');
2595
- }
2596
- this._submessages.push({ messageType: 2, messageText: 'Bloks' });
2597
- const primitive = {
2598
- type: data.type,
2599
- data: data.data ?? '{}',
2600
- uuid: data.uuid ?? '',
2601
- versioning_id: data.versioning_id ?? '',
2602
- __typename: 'FOABloksPrimitive',
2603
- };
2604
- // Omit initial_response entirely when unset — same null-field crash as addProgressStatus/addThinkingStatus.
2605
- if (data.initial_response != null) primitive.initial_response = data.initial_response;
2606
- this._sections.push(AIRich.newLayout('Single', primitive));
2607
- // Safety net: FOABloksPrimitive needs a real, client-registered Bloks screen to render
2608
- // anything — arbitrary/placeholder payloads show up blank. Append a plain text section
2609
- // so the card isn't empty. Set data.textFallback = false to skip.
2610
- if (data.textFallback !== false) {
2611
- this._sections.push(AIRich.newLayout('Single', { text: `Bloks: ${data.type}`, __typename: 'FOATextPrimitive' }));
2612
- }
2613
- return this;
2614
- }
2615
-
2616
- /** Add tappable follow-up suggestion chips below the message. @param {string|string[]} suggestion */
2617
- addSuggest(suggestion, { scroll = true, layout } = {}) {
2348
+ addSuggest(suggestion, { scroll = true, layout, id, replace, insertAt } = {}) {
2618
2349
  if (!(typeof suggestion === 'string' || (Array.isArray(suggestion) && suggestion.every((v) => typeof v === 'string')))) {
2619
2350
  throw new TypeError('Suggestion must be a string or array of strings');
2620
2351
  }
@@ -2635,18 +2366,28 @@ class AIRich extends BaseBuilder {
2635
2366
 
2636
2367
  const type = layout ?? (suggest.length === 1 ? 'Single' : scroll ? 'HScroll' : 'ActionRow');
2637
2368
 
2638
- this._sections.push(AIRich.newLayout(type, type === 'Single' ? suggest[0] : suggest, { __typename: 'GenAIUnifiedResponseSection' }));
2369
+ const section = AIRich.newLayout(type, type === 'Single' ? suggest[0] : suggest, {
2370
+ __typename: 'GenAIUnifiedResponseSection',
2371
+ });
2372
+
2373
+ const submessage = this.createAlert('GenAIFollowUpSuggestionPillPrimitive');
2639
2374
 
2640
- return this;
2375
+ return this._addContent(section, submessage, {
2376
+ id,
2377
+ replace,
2378
+ insertAt,
2379
+ });
2641
2380
  }
2642
2381
 
2643
- /** @returns {Promise<Record<string, any>>} The generated AI-rich message content (without wrapping/sending it). */
2644
- async build({ forwarded = true, notification = false, includesUnifiedResponse = true, includesSubmessages = true, quoted, quotedParticipant, ...options } = {}) {
2382
+ async build(
2383
+ jid,
2384
+ { bypassDownload = true, forwarded = true, notification = false, includesUnifiedResponse = true, includesSubmessages = true, quoted, quotedParticipant, messageId, ...options } = {}
2385
+ ) {
2645
2386
  const forward = forwarded
2646
2387
  ? {
2647
2388
  forwardingScore: 1,
2648
2389
  isForwarded: true,
2649
- forwardedAiBotMessageInfo: { botJid: '0@bot' },
2390
+ forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' },
2650
2391
  forwardOrigin: 4,
2651
2392
  }
2652
2393
  : {};
@@ -2664,7 +2405,7 @@ class AIRich extends BaseBuilder {
2664
2405
  const qObj = quoted
2665
2406
  ? {
2666
2407
  stanzaId: quoted?.key?.id || quoted?.id,
2667
- participant: quotedParticipant || quoted?.key?.participant || quoted?.key?.remoteJid,
2408
+ participant: quotedParticipant || quoted?.key?.participant || quoted?.participant || quoted?.key?.remoteJid,
2668
2409
  quotedType: 0,
2669
2410
  quotedMessage: typeof quoted === 'object' && quoted !== null ? (quoted.message ?? quoted) : undefined,
2670
2411
  }
@@ -2680,85 +2421,131 @@ class AIRich extends BaseBuilder {
2680
2421
  ]
2681
2422
  : [...(await waitAllPromises(this._sections))];
2682
2423
 
2683
- // Vanz@Merge 15-08-26 --- Neither blurose nor arslan sign the bot metadata with
2684
- // verificationMetadata (proofs/certificateChain). Backported from this project's own
2685
- // rich-message-utils.js botMetadataSignature/botMetadataCertificate helpers, plus a
2686
- // botResponseId tying the signed metadata to unifiedResponse.response_id.
2687
- // Vanz@Fix 24-08-26 --- was `const responseId = crypto.randomUUID()` shared for BOTH
2688
- // unifiedResponse.response_id and botMetadata.botResponseId, generated fresh every build()
2689
- // with no override. Now each has its own id, pinned via setResponseId()/setBotResponseId()
2690
- // if the caller set one (for sendEdit()-style in-place message updates), otherwise still
2691
- // defaults to a fresh randomUUID() per build() exactly like before.
2692
- const responseId = this._responseId ?? crypto.randomUUID();
2693
- const botResponseId = this._botResponseId ?? crypto.randomUUID();
2424
+ if (this._dynamic) {
2425
+ this.refreshResponseId();
2426
+ this.refreshBotResponseId();
2427
+ }
2694
2428
 
2695
- return {
2696
- messageContextInfo: {
2697
- deviceListMetadata: {},
2698
- deviceListMetadataVersion: 2,
2699
- botMetadata: {
2700
- messageDisclaimerText: this._title,
2701
- richResponseSourcesMetadata: { sources: this._richResponseSources },
2702
- botResponseId: botResponseId,
2703
- verificationMetadata: {
2704
- proofs: [
2705
- {
2706
- certificateChain: [botMetadataCertificate(), botMetadataCertificate(892)],
2707
- version: 1,
2708
- useCase: 1,
2709
- signature: botMetadataSignature(),
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,
2440
+ },
2441
+ },
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') : '',
2710
2450
  },
2711
- ],
2451
+ contextInfo: {
2452
+ ...forward,
2453
+ ...qObj,
2454
+ ...this._contextInfo,
2455
+ },
2456
+ },
2712
2457
  },
2713
- ...notif,
2714
2458
  },
2715
2459
  },
2716
- ...this._extraPayload,
2717
- botForwardedMessage: {
2718
- message: {
2719
- richResponseMessage: {
2720
- messageType: 1,
2721
- submessages: includesSubmessages ? await waitAllPromises(this._submessages) : [],
2722
- unifiedResponse: {
2723
- data: includesUnifiedResponse ? Buffer.from(JSON.stringify({ response_id: responseId, sections })).toString('base64') : '',
2724
- },
2725
- contextInfo: {
2726
- ...forward,
2727
- ...qObj,
2728
- ...this._contextInfo,
2460
+ { messageId: messageId || generateMessageIDV2(), ...options }
2461
+ );
2462
+ }
2463
+
2464
+ async buildEdit(targetJid, targetId, { msg, messageId, ...options } = {}) {
2465
+ if (!msg) {
2466
+ msg = (await this.build(targetJid, options)).message;
2467
+ }
2468
+
2469
+ const editedMessage = msg;
2470
+
2471
+ if (!editedMessage) {
2472
+ throw new Error('buildEdit: msg does not contain botForwardedMessage');
2473
+ }
2474
+
2475
+ return generateWAMessageFromContent(
2476
+ targetJid,
2477
+ {
2478
+ botForwardedMessage: {
2479
+ message: {
2480
+ protocolMessage: {
2481
+ key: {
2482
+ remoteJid: targetJid,
2483
+ fromMe: true,
2484
+ id: targetId,
2485
+ },
2486
+ type: 14,
2487
+ editedMessage,
2729
2488
  },
2730
2489
  },
2731
2490
  },
2732
2491
  },
2733
- };
2492
+ { messageId: messageId || generateMessageIDV2(), ...options }
2493
+ );
2734
2494
  }
2735
2495
 
2736
- // Vanz@Fix (bug 42 / inline image fallback) --- WA won't render AIRichResponseInlineImageMetadata
2737
- // for bot-sent messages (confirmed: even a valid WA-CDN url with mediaKey stays blank), so any
2738
- // image added via addInlineImage() is sent here as a normal imageMessage instead. Pass
2739
- // { skipImageFallback: true } to opt out and send only the (image-less-looking) rich card.
2740
- // Vanz@Fix: don't spread relayMessage-shaped `options` into sendMessage()'s options param —
2741
- // the two calls expect different option shapes, so the fallback now only forwards `quoted`
2742
- // (the one option that clearly applies to both) instead of blindly spreading everything.
2743
- /** 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. */
2744
- async send(jid, { forwarded, notification, includesUnifiedResponse, includesSubmessages, skipImageFallback = false, quoted, ...options } = {}) {
2745
- const msg = await this.build({ forwarded, notification, includesUnifiedResponse, includesSubmessages, quoted, ...options });
2746
-
2747
- if (!skipImageFallback && this._inlineImages.length) {
2748
- for (const { url, caption } of this._inlineImages) {
2749
- try {
2750
- await this.#client.sendMessage(jid, { image: { url }, caption }, quoted ? { quoted } : {});
2751
- } catch (err) {
2752
- // Vanz@Fix: don't let a fallback image failure block the actual rich card from sending
2753
- this.#client.logger?.warn?.({ err, url }, 'inline image fallback failed, continuing with rich card');
2754
- }
2755
- }
2496
+ async sendEdit(jid, id, { msg, messageId, additionalNodes = [], ...options } = {}) {
2497
+ jid = jid ?? this._lastMessageKey?.remoteJid;
2498
+ id = id ?? this._lastMessageKey?.id;
2499
+
2500
+ if (!jid) {
2501
+ throw new Error('JID is required');
2502
+ }
2503
+
2504
+ if (!id) {
2505
+ throw new Error('Message id is required');
2506
+ }
2507
+
2508
+ const msgEdit = await this.buildEdit(jid, id, {
2509
+ msg,
2510
+ messageId: messageId || generateMessageIDV2(),
2511
+ ...options,
2512
+ });
2513
+
2514
+ await this.#client.relayMessage(jid, msgEdit.message, {
2515
+ messageId: msgEdit.key.id,
2516
+ additionalNodes,
2517
+ });
2518
+
2519
+ return msgEdit;
2520
+ }
2521
+
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
+ });
2756
2542
  }
2757
2543
 
2758
- return await this.#client.relayMessage(jid, msg, { ...options });
2544
+ this._lastMessageKey = msg.key;
2545
+
2546
+ return msg;
2759
2547
  }
2760
2548
 
2761
- /** 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. */
2762
2549
  static tokenizer(code, lang = 'javascript') {
2763
2550
  const keywordsMap = {
2764
2551
  javascript: new Set([
@@ -3424,7 +3211,6 @@ class AIRich extends BaseBuilder {
3424
3211
  };
3425
3212
  }
3426
3213
 
3427
- /** Convert a raw `string[][]` grid into the table metadata shape addTable()/addText() produce internally. */
3428
3214
  static toTableMetadata(arr, { hyperlink = true, citation = true, latex = true } = {}) {
3429
3215
  if (!Array.isArray(arr) || !arr.every((row) => Array.isArray(row) && row.every((cell) => typeof cell === 'string'))) {
3430
3216
  throw new TypeError('Table must be a nested array of strings');
@@ -3473,120 +3259,35 @@ class AIRich extends BaseBuilder {
3473
3259
  };
3474
3260
  }
3475
3261
 
3476
- /**
3477
- * Add an "AI is generating..." placeholder card (`GenAIImaginePrimitive` with status
3478
- * GENERATING) — distinct from addImage()/addVideo() which always send status READY.
3479
- * Use this to show a pending-generation state before the real media is ready.
3480
- * @param {{ imagine_type?: 'IMAGE'|'ANIMATE', estimated_completion_time?: number }} [options]
3481
- */
3482
- addGenerating({ imagine_type = 'IMAGE', estimated_completion_time, textFallback = true } = {}) {
3483
- this._submessages.push({ messageType: 2, messageText: '[ Sedang diproses... ]' });
3484
- this._sections.push(
3485
- AIRich.newLayout('Single', {
3486
- media: { url: '', mime_type: imagine_type === 'ANIMATE' ? 'video/mp4' : 'image/png' },
3487
- imagine_type,
3488
- status: {
3489
- status: 'GENERATING',
3490
- estimated_completion_time: estimated_completion_time ?? Math.floor(Date.now() / 1000) + 30,
3491
- },
3492
- __typename: 'GenAIImaginePrimitive',
3493
- })
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`
3494
3265
  );
3495
- // Vanz@Fix 23-08-26 (v4.8) --- media.url kosong + status GENERATING gak punya renderer
3496
- // visual instan di stock WA client; sebelumnya cuma diem sampe WA nge-timeout sendiri
3497
- // dan nampilin fallback bawaannya ("Saat ini, saya tidak bisa membuat gambar itu...").
3498
- // Same fix class kayak addTask/addBloks: append FOATextPrimitive biar ada fallback
3499
- // instan, gak perlu nunggu timeout WA. Set { textFallback: false } buat skip.
3500
- if (textFallback) {
3501
- this._sections.push(AIRich.newLayout('Single', { text: '[ Sedang diproses... ]', __typename: 'FOATextPrimitive' }));
3502
- }
3503
- return this;
3504
- }
3505
3266
 
3506
- /**
3507
- * Send a support-ticket marker message (`messageContextInfo.supportPayload`) — a plain
3508
- * conversation message tagged as an AI/support-bot ticket, distinct from richResponseMessage.
3509
- * @param {import('../../WAProto/index.js').WASocket} client
3510
- * @param {string} jid
3511
- * @param {string} text
3512
- * @param {{ ticketId?: string, isAiMessage?: boolean, shouldShowSystemMessage?: boolean, version?: number }} [options]
3513
- */
3514
- static async sendSupportPayload(client, jid, text, { ticketId = crypto.randomUUID(), isAiMessage = true, shouldShowSystemMessage = true, version = 1 } = {}) {
3515
- if (!client) throw new Error('Socket is required');
3516
- if (typeof text !== 'string' || !text) throw new TypeError('sendSupportPayload(client, jid, text) requires a non-empty string text');
3517
-
3518
- const msg = {
3519
- conversation: text,
3520
- messageContextInfo: {
3521
- messageSecret: crypto.randomBytes(32),
3522
- supportPayload: JSON.stringify({
3523
- version,
3524
- is_ai_message: isAiMessage,
3525
- should_show_system_message: shouldShowSystemMessage,
3526
- ticket_id: ticketId,
3527
- }),
3528
- },
3529
- };
3530
-
3531
- return client.relayMessage(jid, msg, {
3532
- additionalNodes: [
3533
- { tag: 'bot', attrs: { biz_bot: '1' } },
3534
- { tag: 'biz', attrs: {} },
3535
- ],
3536
- });
3537
- }
3538
-
3539
- /**
3540
- * Send an image and video as one paired-media unit (image sent first, video linked to it via
3541
- * `messageAssociation`). Distinct from a plain album — the client treats them as a single group.
3542
- * @param {import('../../WAProto/index.js').WASocket} client
3543
- * @param {string} jid
3544
- * @param {{ image: string|Buffer, video: string|Buffer }} media
3545
- */
3546
- static async sendPairedMedia(client, jid, { image, video } = {}) {
3547
- if (!client) throw new Error('Socket is required');
3548
- if (!image || !video) throw new TypeError('sendPairedMedia() requires both "image" and "video"');
3549
-
3550
- const imagePrepared = await prepareWAMessageMedia(
3551
- { image: typeof image === 'string' ? { url: image } : image },
3552
- { upload: client.waUploadToServer }
3553
- );
3554
- const videoPrepared = await prepareWAMessageMedia(
3555
- { video: typeof video === 'string' ? { url: video } : video },
3556
- { upload: client.waUploadToServer }
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`
3557
3269
  );
3558
3270
 
3559
- const imageMsg = generateWAMessageFromContent(
3560
- jid,
3561
- {
3562
- imageMessage: {
3563
- ...imagePrepared.imageMessage,
3564
- contextInfo: { pairedMediaType: 5, statusSourceType: 0 },
3565
- },
3566
- },
3567
- {}
3568
- );
3271
+ const signature = Buffer.concat([signatureMaterial, crypto.randomBytes(64 - signatureMaterial.length)]).toString('base64');
3569
3272
 
3570
- await client.relayMessage(jid, imageMsg.message, { messageId: imageMsg.key.id });
3273
+ const certificateChain = [
3274
+ Buffer.concat([certificateMaterial, crypto.randomBytes(684 - certificateMaterial.length)]).toString('base64'),
3571
3275
 
3572
- await client.relayMessage(
3573
- jid,
3574
- {
3575
- videoMessage: {
3576
- ...videoPrepared.videoMessage,
3577
- contextInfo: { pairedMediaType: 6, statusSourceType: 0 },
3578
- },
3579
- messageContextInfo: {
3580
- messageAssociation: { associationType: 12, parentMessageKey: imageMsg.key },
3581
- },
3582
- },
3583
- {}
3584
- );
3276
+ Buffer.concat([certificateMaterial, crypto.randomBytes(892 - certificateMaterial.length)]).toString('base64'),
3277
+ ];
3585
3278
 
3586
- return imageMsg.key;
3279
+ return {
3280
+ proofs: [
3281
+ {
3282
+ version: 1,
3283
+ useCase: 1,
3284
+ signature,
3285
+ certificateChain,
3286
+ },
3287
+ ],
3288
+ };
3587
3289
  }
3588
3290
 
3589
- /** Build a raw submessage layout block by name — escape hatch for layouts not covered by the add*() helpers. */
3590
3291
  static newLayout(name, data, extra = {}) {
3591
3292
  return {
3592
3293
  ...extra,
@@ -3596,6 +3297,320 @@ class AIRich extends BaseBuilder {
3596
3297
  },
3597
3298
  };
3598
3299
  }
3300
+
3301
+ _makeNode(id, section, submessage) {
3302
+ return { id: id ?? null, section: section ?? null, submessage: submessage ?? null };
3303
+ }
3304
+
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
+ }
3311
+
3312
+ if (this._idIndex.has(id)) {
3313
+ throw new DuplicateIdError(id);
3314
+ }
3315
+
3316
+ node.id = id;
3317
+ this._idIndex.set(id, node);
3318
+ }
3319
+
3320
+ _unregisterId(node) {
3321
+ if (node.id && this._idIndex.get(node.id) === node) {
3322
+ this._idIndex.delete(node.id);
3323
+ }
3324
+ }
3325
+
3326
+ hasId(id) {
3327
+ return typeof id === 'string' && this._idIndex.has(id);
3328
+ }
3329
+
3330
+ getIds() {
3331
+ return [...this._idIndex.keys()];
3332
+ }
3333
+
3334
+ peek(id) {
3335
+ const node = this._idIndex.get(id);
3336
+
3337
+ if (!node) return null;
3338
+
3339
+ return {
3340
+ id: node.id,
3341
+ section: node.section,
3342
+ submessage: node.submessage,
3343
+ };
3344
+ }
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
+ }
3599
3614
  }
3600
3615
 
3601
3616
  // Vanz@Alias --- AIRich diekspos ulang pake nama sendiri. Implementasi & referensi