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