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