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