@vanzxy/baileys 2.0.1 → 2.0.3

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.
@@ -1,8 +1,11 @@
1
- import { BaseBuilder, Toolkit, extractIE, waitAllPromises, getSharp, getFfmpeg, botMetadataSignature, botMetadataCertificate, crypto, generateWAMessageFromContent, prepareWAMessageMedia, generateMessageIDV2 } from './shared.js';
1
+ import { BaseBuilder, Toolkit, extractIE, waitAllPromises, crypto, generateWAMessageFromContent, generateMessageIDV2 } from './shared.js';
2
+
3
+ const VERSION = '5.0';
4
+
2
5
  class AIRich extends BaseBuilder {
3
6
  #client;
4
7
 
5
- constructor(client) {
8
+ constructor(client, { dynamic = true, unsupportedTypeAlert = true } = {}) {
6
9
  if (!client) {
7
10
  throw new Error('Socket is required');
8
11
  }
@@ -10,204 +13,125 @@ class AIRich extends BaseBuilder {
10
13
  super();
11
14
  this.#client = client;
12
15
  this._contextInfo = {};
13
- this._submessages = [];
14
- this._sections = [];
15
- this._richResponseSources = [];
16
- // Vanz@Fix (bug 42 / inline image fallback): WA rejects rendering AIRichResponseInlineImageMetadata
17
- // for third-party bots regardless of URL (confirmed empirically — Meta/WA CDN url with valid
18
- // mediaKey still doesn't render, so it's a trust-chain gate, not a domain/encoding issue).
19
- // Track every addInlineImage() call here so send() can fall back to a normal imageMessage.
20
- this._inlineImages = [];
21
-
22
- // Vanz@Add 24-08-26 --- ported from temen's MessageBuilderV4.7 (setResponseId/setBotResponseId
23
- // below), rewritten to fit this fork's conventions. build() used to always mint a fresh
24
- // crypto.randomUUID() for both unifiedResponse.response_id and botMetadata.botResponseId on
25
- // every call, with no way to reuse one — so a `sendEdit()`-style flow (rebuild the same
26
- // message with updated content, same response_id, so WA patches it in place instead of
27
- // showing a new message) was never actually possible despite being in the gist example.
28
- // null here means "not pinned yet" — build() falls back to a fresh randomUUID() same as before
29
- // when neither setResponseId() nor setBotResponseId() has been called.
30
- this._responseId = null;
31
- this._botResponseId = null;
32
-
33
- // Vanz@Add --- set by send()/sendEdit() after every relay so a follow-up sendEdit(), called
34
- // with no args, knows which jid/message id to patch in place (matches temen's v4.7 API).
16
+ this._nodes = [];
17
+ this._idIndex = new Map();
18
+ this._unsupportedTypeAlert = !!unsupportedTypeAlert;
19
+ this._dynamic = !!dynamic;
20
+ this._responseId = crypto.randomUUID();
21
+ this._botResponseId = crypto.randomUUID();
35
22
  this._lastMessageKey = null;
23
+ }
36
24
 
37
- // Vanz@Add (v4.9.1) --- { id, insertAt } support for every add*()/set*() call, without
38
- // touching each method's own body/signature. Every add*() call ends up pushing 0-N items
39
- // onto _submessages and 0-N onto _sections (some push to both, some to just one — e.g.
40
- // addSuggest only touches _submessages, addSection only touches _sections). A Proxy wraps
41
- // every add*/set* call: it snapshots array lengths before calling the real method, lets the
42
- // method push onto the tail as it always has, then — if the caller passed { insertAt } —
43
- // peels those freshly-pushed items back off the tail and re-splices them right after the
44
- // last item that belongs to the block named by insertAt. Blocks are tracked by *object
45
- // reference*, not saved numeric index, so earlier insertions shifting the array around never
46
- // invalidates a later insertAt lookup (indexOf on the reference always finds the live position).
47
- //
48
- // Vanz@Note (bug 71, behavior — not fixed, documented) --- insertAt always inserts right after
49
- // the ANCHOR's block, not after "whatever was most recently inserted there". Chaining (each new
50
- // item gets its own id, and the next call's insertAt points at THAT id — exactly what the
51
- // addText/addSuggest streaming-reveal example does) produces the expected order. But calling
52
- // insertAt at the SAME static anchor id repeatedly, without giving each new item its own id to
53
- // chain onto, inserts every one of them right after the original anchor — so the order comes out
54
- // reversed relative to call order (confirmed by test: id:'x' then 3x insertAt:'x' with no id of
55
- // their own on the new items produces [x, third, second, first], not [x, first, second, third]).
56
- // Left as-is rather than "fixed": making insertAt self-advance (re-pointing the anchor's block at
57
- // whatever was just inserted) would silently change what an id resolves to for any OTHER caller
58
- // still holding that id for a later replace()/delete()/insertAt() — a subtler, harder-to-diagnose
59
- // bug than the surprising-but-deterministic order this produces. Chain with fresh ids instead.
60
- this._blocks = new Map(); // id -> { subItems: object[], secItems: object[] }
61
- return new Proxy(this, {
62
- get(target, prop, receiver) {
63
- const orig = Reflect.get(target, prop, receiver);
64
- if (typeof orig !== 'function') return orig;
65
-
66
- // Vanz@Fix 23-08-26 (part 2) --- the add*/set* filter below only wrapped methods whose
67
- // name starts with "add"/"set". Everything else (send(), build(), ...) fell through to
68
- // `return orig` unwrapped, so calling e.g. `richInstance.send(...)` still invoked the
69
- // real method with `this` = the Proxy (`receiver`), hitting the exact same
70
- // "Cannot read private member #client..." brand-check error the add*/set* fix was for —
71
- // just one level up, in send()/build() themselves. Every function property now gets
72
- // bound to `target` (the real instance) at minimum; add*/set* additionally get the
73
- // insertAt/id bookkeeping below.
74
- if (!/^(add|set)/.test(String(prop))) {
75
- return (...args) => {
76
- const result = orig.apply(target, args);
77
- return result === target ? receiver : result;
78
- };
25
+ loadFrom(msg) {
26
+ if (!msg) throw new Error('AI Rich message needed');
27
+
28
+ const message = msg.message ?? msg;
29
+
30
+ let richResponseMessage = message?.botForwardedMessage?.message?.richResponseMessage;
31
+
32
+ if (!richResponseMessage) {
33
+ richResponseMessage = message?.botForwardedMessage?.richResponseMessage;
34
+ }
35
+
36
+ if (!richResponseMessage) {
37
+ richResponseMessage = message?.richResponseMessage;
38
+ }
39
+
40
+ if (!richResponseMessage) {
41
+ throw new Error('richResponseMessage not found');
42
+ }
43
+
44
+ const messageContextInfo = message?.messageContextInfo ?? {};
45
+ const botMetadata = messageContextInfo?.botMetadata ?? {};
46
+
47
+ this._title = botMetadata?.messageDisclaimerText ?? '';
48
+
49
+ this._contextInfo = structuredClone(richResponseMessage?.contextInfo ?? {});
50
+
51
+ const loadedSubmessages = Array.isArray(richResponseMessage?.submessages) ? structuredClone(richResponseMessage.submessages) : [];
52
+
53
+ let loadedSections = [];
54
+
55
+ const unifiedData = richResponseMessage?.unifiedResponse?.data;
56
+
57
+ if (unifiedData) {
58
+ try {
59
+ const decoded = Buffer.from(unifiedData, 'base64').toString('utf8');
60
+ const unifiedResponse = JSON.parse(decoded);
61
+
62
+ if (Array.isArray(unifiedResponse?.sections)) {
63
+ loadedSections = structuredClone(unifiedResponse.sections);
79
64
  }
65
+ } catch {}
66
+ }
80
67
 
81
- return (...args) => {
82
- const opts = args.find((a) => a && typeof a === 'object' && !Array.isArray(a) && !Buffer.isBuffer(a) && ('id' in a || 'insertAt' in a || 'replace' in a));
83
- const id = opts?.id;
84
- const insertAt = opts?.insertAt;
85
- const replace = opts?.replace;
86
-
87
- // Vanz@Fix (bug 70) --- `id` reuse across two different add*/set* calls was silently
88
- // accepted: target._blocks.set(id, ...) below just clobbers the previous registration,
89
- // so the FIRST block with that id becomes an untracked ghost — still in _sections/
90
- // _submessages (still renders), but no longer reachable via hasId/peek/delete/replace/
91
- // insertAt (the id now only resolves to the second block). Confirmed by direct test:
92
- // addText('first',{id:'dup'}); addText('second',{id:'dup'}) left both in the message
93
- // but getIds() only ever had one 'dup', pointing at 'second'. Fail fast instead — same
94
- // as re-registering the same id you're actively `replace`-ing (that's a legitimate
95
- // "update this block, keep its id" call, not a collision).
96
- if (id && target._blocks.has(id) && replace !== id) {
97
- throw new Error(`add*/set*: id "${id}" is already registered — each id must be unique (pass { replace: "${id}" } to update that block instead, or use a different id)`);
98
- }
68
+ this._nodes = [];
69
+ this._idIndex = new Map();
99
70
 
100
- const subBefore = target._submessages.length;
101
- const secBefore = target._sections.length;
102
-
103
- // Vanz@Fix 23-08-26 --- was orig.apply(receiver, args): calling the real method bound to
104
- // the Proxy itself (`receiver`) makes any `this.#client` access inside throw
105
- // "Cannot read private member #client from an object whose class did not declare it",
106
- // because a Proxy is never the branded instance a private field was declared on —
107
- // this hit every add*() that touches #client via Toolkit.resolveMedia(this.#client, ...)
108
- // (addProduct/addPost/addReels/addSource, and would eventually hit addImage/addVideo
109
- // too once JIT/engine specifics changed). Binding to `target` (the real instance) instead
110
- // fixes it for good; `target._submessages`/`target._sections` below are unaffected since
111
- // they're plain properties, and `result === target ? receiver : result` still converts a
112
- // `this`-return back to the Proxy so chaining (`.addX().addY()`) keeps working.
113
- const result = orig.apply(target, args);
114
-
115
-
116
- const subItems = target._submessages.splice(subBefore);
117
- const secItems = target._sections.splice(secBefore);
118
-
119
- if (insertAt) {
120
- const anchor = target._blocks.get(insertAt);
121
- if (!anchor) throw new Error(`insertAt: no block registered with id "${insertAt}" (register it by passing { id: "${insertAt}" } on an earlier add*() call)`);
122
-
123
- const lastSub = anchor.subItems[anchor.subItems.length - 1];
124
- const subIdx = lastSub ? target._submessages.indexOf(lastSub) + 1 : target._submessages.length;
125
- target._submessages.splice(subIdx, 0, ...subItems);
126
-
127
- const lastSec = anchor.secItems[anchor.secItems.length - 1];
128
- const secIdx = lastSec ? target._sections.indexOf(lastSec) + 1 : target._sections.length;
129
- target._sections.splice(secIdx, 0, ...secItems);
130
- } else if (replace) {
131
- // replace: delete the old block's items at their current positions,
132
- // then insert new items at the same positions
133
- const old = target._blocks.get(replace);
134
- if (!old) throw new Error(`replace: no block registered with id "${replace}" (register it first with { id: "${replace}" })`);
135
-
136
- let subIdx = old.subItems.length > 0 ? target._submessages.indexOf(old.subItems[0]) : target._submessages.length;
137
- if (subIdx === -1) subIdx = target._submessages.length;
138
- for (const item of old.subItems) {
139
- const i = target._submessages.indexOf(item);
140
- if (i !== -1) target._submessages.splice(i, 1);
141
- }
142
- target._submessages.splice(subIdx, 0, ...subItems);
143
-
144
- let secIdx = old.secItems.length > 0 ? target._sections.indexOf(old.secItems[0]) : target._sections.length;
145
- if (secIdx === -1) secIdx = target._sections.length;
146
- for (const item of old.secItems) {
147
- const i = target._sections.indexOf(item);
148
- if (i !== -1) target._sections.splice(i, 1);
149
- }
150
- target._sections.splice(secIdx, 0, ...secItems);
151
-
152
- target._blocks.delete(replace);
153
- if (id) target._blocks.set(id, { subItems, secItems });
154
- else target._blocks.set(replace, { subItems, secItems });
155
- } else {
156
- target._submessages.push(...subItems);
157
- target._sections.push(...secItems);
158
- }
71
+ const maxLength = Math.max(loadedSections.length, loadedSubmessages.length);
72
+
73
+ for (let i = 0; i < maxLength; i++) {
74
+ this._nodes.push({
75
+ id: null,
76
+ section: loadedSections[i] ?? null,
77
+ submessage: loadedSubmessages[i] ?? null,
78
+ });
79
+ }
159
80
 
160
- if (id) target._blocks.set(id, { subItems, secItems });
81
+ this._extraPayload = {};
161
82
 
162
- return result === target ? receiver : result;
163
- };
164
- },
165
- });
83
+ for (const [key, value] of Object.entries(message)) {
84
+ if (key !== 'messageContextInfo' && key !== 'botForwardedMessage' && key !== 'richResponseMessage') {
85
+ this._extraPayload[key] = structuredClone(value);
86
+ }
87
+ }
88
+
89
+ return this;
166
90
  }
167
91
 
168
- /** Flatten every primitive pushed into `_sections` so far into one array — lets you build a
169
- * card set in one AIRich instance and re-embed it into another via addSection(AIRich.newLayout(...)). */
170
- get items() {
171
- return this._sections.flatMap((s) => {
172
- const vm = s?.view_model;
173
- if (!vm) return [];
174
- return vm.primitives ?? (vm.primitive !== undefined ? [vm.primitive] : []);
175
- });
92
+ setResponseId(id) {
93
+ if (typeof id !== 'string') {
94
+ throw new TypeError('ID must be a string');
95
+ }
96
+ this._responseId = id;
97
+
98
+ return this;
176
99
  }
177
100
 
178
- /** Push a raw pre-built submessage block (escape hatch for shapes not covered by the add*() helpers). */
179
- addSubmessage(submessage) {
180
- const items = Array.isArray(submessage) ? submessage : [submessage];
101
+ refreshResponseId() {
102
+ this._responseId = crypto.randomUUID();
181
103
 
182
- for (const item of items) {
183
- if (typeof item !== 'object' || item === null || Array.isArray(item)) {
184
- throw new TypeError('Submessage must be a plain object or array of plain objects');
185
- }
104
+ return this;
105
+ }
186
106
 
187
- this._submessages.push(item);
107
+ setBotResponseId(id) {
108
+ if (typeof id !== 'string') {
109
+ throw new TypeError('ID must be a string');
188
110
  }
111
+ this._botResponseId = id;
189
112
 
190
113
  return this;
191
114
  }
192
115
 
193
- /** Push a raw pre-built section wrapper around one or more submessages. */
194
- addSection(section) {
195
- const items = Array.isArray(section) ? section : [section];
116
+ refreshBotResponseId() {
117
+ this._botResponseId = crypto.randomUUID();
196
118
 
197
- for (const item of items) {
198
- if (typeof item !== 'object' || item === null || Array.isArray(item)) {
199
- throw new TypeError('Section must be a plain object or array of plain objects');
200
- }
119
+ return this;
120
+ }
201
121
 
202
- this._sections.push(item);
122
+ createAlert(type) {
123
+ if (this._unsupportedTypeAlert) {
124
+ return {
125
+ messageType: 2,
126
+ messageText: `[ UNSUPPORTED_TYPE - ${type}]`,
127
+ };
203
128
  }
204
129
 
205
- return this;
130
+ return undefined;
206
131
  }
207
132
 
208
- /** 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. */
209
- addText(text, { hyperlink = true, citation = true, latex = true } = {}) {
210
- if (typeof text != 'string') {
133
+ addText(text, { hyperlink = true, citation = true, latex = true, id, replace, insertAt } = {}) {
134
+ if (typeof text !== 'string') {
211
135
  throw new TypeError('Text must be a string');
212
136
  }
213
137
 
@@ -217,195 +141,185 @@ class AIRich extends BaseBuilder {
217
141
  latex,
218
142
  });
219
143
 
220
- this._submessages.push({
221
- messageType: 2,
222
- messageText: extractedText,
144
+ const section = AIRich.newLayout('Single', {
145
+ text: extractedText,
146
+ ...(inline_entities.length && { inline_entities }),
147
+ __typename: 'GenAIMarkdownTextUXPrimitive',
223
148
  });
224
149
 
225
- this._sections.push(
226
- AIRich.newLayout('Single', {
227
- text: extractedText,
228
- ...(inline_entities.length && {
229
- inline_entities,
230
- }),
231
- __typename: 'GenAIMarkdownTextUXPrimitive',
232
- })
233
- );
150
+ const submessages = [
151
+ {
152
+ messageType: 2,
153
+ messageText: text,
154
+ },
155
+ ].filter(Boolean);
234
156
 
235
- return this;
157
+ return this._addContent(section, submessages, {
158
+ id,
159
+ replace,
160
+ insertAt,
161
+ });
162
+ }
163
+
164
+ /** Alias — .d.ts documents this as "addHeading" (FOATextPrimitive), source only ever exposed addFOAText(). */
165
+ addHeading(text, options = {}) {
166
+ return this.addFOAText(text, options);
167
+ }
168
+
169
+ addFOAText(text, { id, replace, insertAt } = {}) {
170
+ if (typeof text !== 'string') {
171
+ throw new TypeError('Text must be a string');
172
+ }
173
+
174
+ const section = AIRich.newLayout('Single', {
175
+ text,
176
+ __typename: 'FOATextPrimitive',
177
+ });
178
+
179
+ const submessages = [
180
+ {
181
+ messageType: 2,
182
+ messageText: text,
183
+ },
184
+ ];
185
+
186
+ return this._addContent(section, submessages, {
187
+ id,
188
+ replace,
189
+ insertAt,
190
+ });
236
191
  }
237
192
 
238
- /** Add a syntax-highlighted code block. @param {string} language e.g. 'javascript', 'python'. */
239
- addCode(language, code) {
193
+ addCode(language, code, { id, replace, insertAt } = {}) {
240
194
  if (typeof language !== 'string' || typeof code !== 'string') {
241
195
  throw new TypeError('Language and code must be a string');
242
196
  }
243
197
 
244
198
  const meta = AIRich.tokenizer(code, language);
245
199
 
246
- this._submessages.push({
247
- messageType: 5,
248
- codeMetadata: {
249
- codeLanguage: language,
250
- codeBlocks: meta.codeBlock,
251
- },
200
+ const section = AIRich.newLayout('Single', {
201
+ language,
202
+ code_blocks: meta.unified_codeBlock,
203
+ __typename: 'GenAICodeUXPrimitive',
252
204
  });
253
205
 
254
- this._sections.push(
255
- AIRich.newLayout('Single', {
256
- language,
257
- code_blocks: meta.unified_codeBlock,
258
- __typename: 'GenAICodeUXPrimitive',
259
- })
260
- );
206
+ const submessages = [
207
+ {
208
+ messageType: 5,
209
+ codeMetadata: {
210
+ codeLanguage: language,
211
+ codeBlocks: meta.codeBlock,
212
+ },
213
+ },
214
+ ];
261
215
 
262
- return this;
216
+ return this._addContent(section, submessages, {
217
+ id,
218
+ replace,
219
+ insertAt,
220
+ });
263
221
  }
264
222
 
265
- /** Add a table. @param {string[][]} table Row-major grid, first row treated as the header. */
266
- addTable(table, { hyperlink = true, citation = true, latex = true } = {}) {
223
+ addTable(table, { hyperlink = true, citation = true, latex = true, id, replace, insertAt } = {}) {
267
224
  if (!Array.isArray(table)) {
268
225
  throw new TypeError('Table must be an array');
269
226
  }
270
227
 
271
- const meta = AIRich.toTableMetadata(table, { hyperlink, citation, latex });
272
-
273
- this._submessages.push({
274
- messageType: 4,
275
- tableMetadata: {
276
- title: meta.title,
277
- rows: meta.rows,
278
- },
228
+ const meta = AIRich.toTableMetadata(table, {
229
+ hyperlink,
230
+ citation,
231
+ latex,
279
232
  });
280
233
 
281
- this._sections.push(
282
- AIRich.newLayout('Single', {
283
- rows: meta.unified_rows,
284
- __typename: 'GenATableUXPrimitive',
285
- })
286
- );
287
-
288
- return this;
289
- }
234
+ const section = AIRich.newLayout('Single', {
235
+ rows: meta.unified_rows,
236
+ __typename: 'GenATableUXPrimitive',
237
+ });
290
238
 
291
- /** Add a "Sources" strip. @param {string[]|string[][]} sources Flat list of urls, or `[title, url]` pairs. */
292
-
293
- /** Build rich-response citation/link submessages using the same shape as Baileys' `links` content shortcut. */
294
- addLinks(links = []) {
295
- if (!Array.isArray(links)) throw new TypeError('links must be an array');
296
- links.forEach((linkField, index) => {
297
- if (!linkField || typeof linkField !== 'object') throw new TypeError('Each link must be an object');
298
- const prefix = 'SS_' + index;
299
- const url = linkField.url || '';
300
- const text = String(linkField.text ?? '');
301
- const sources = Array.isArray(linkField.sources) ? linkField.sources.map((sourceField) => ({
302
- source_type: 'THIRD_PARTY',
303
- source_display_name: sourceField?.displayName || sourceField?.title || 'Source',
304
- source_subtitle: sourceField?.subtitle || '',
305
- source_url: sourceField?.url || url,
306
- })) : [];
307
- const entity = {
308
- key: prefix,
309
- metadata: {
310
- reference_id: index + 1,
311
- reference_url: url,
312
- reference_title: linkField.title || 'Source',
313
- reference_display_name: linkField.displayName || linkField.title || 'Source',
314
- sources,
315
- __typename: 'GenAISearchCitationItem',
239
+ const submessages = [
240
+ {
241
+ messageType: 4,
242
+ tableMetadata: {
243
+ title: meta.title,
244
+ rows: meta.rows,
316
245
  },
317
- };
318
- const section = AIRich.newLayout('Single', {
319
- text: `${text} {{${prefix}}}${url}{{/${prefix}}}`,
320
- inline_entities: [entity],
321
- __typename: 'GenAIMarkdownTextUXPrimitive',
322
- });
323
- this._sections.push(section);
324
- this._submessages.push({
325
- messageType: 2,
326
- messageText: `${text} {{${prefix}}}¹{{/${prefix}}} `,
327
- inlineEntities: [entity],
328
- });
329
- });
330
- return this;
331
- }
246
+ },
247
+ ];
332
248
 
333
- /** Add a raw rich-response content-items carousel, matching Baileys' `items` field. */
334
- addContentItems(items = []) {
335
- if (!Array.isArray(items)) throw new TypeError('items must be an array');
336
- this._submessages.push({
337
- messageType: 9,
338
- contentItemsMetadata: { itemsMetadata: items, contentType: 1 },
249
+ return this._addContent(section, submessages, {
250
+ id,
251
+ replace,
252
+ insertAt,
339
253
  });
340
- this._sections.push(AIRich.newLayout('Single', {
341
- items,
342
- content_type: 1,
343
- __typename: 'GenAIContentItemsUXPrimitive',
344
- }));
345
- return this;
346
254
  }
347
255
 
348
- /** Add Baileys-compatible inline-video marker. WhatsApp's current rich-response helper carries this as a text marker. */
349
- addInlineVideo() {
350
- this._submessages.push({ messageType: 2, messageText: 'INLINE_VIDEO' });
351
- this._sections.push(AIRich.newLayout('Single', {
352
- text: 'INLINE_VIDEO',
353
- __typename: 'GenAIMarkdownTextUXPrimitive',
354
- }));
355
- return this;
356
- }
256
+ addSource(sources = [], { id, replace, insertAt } = {}) {
257
+ if (!Array.isArray(sources)) {
258
+ throw new TypeError('Sources must be an array of strings, arrays, or objects');
259
+ }
260
+
261
+ const isStringArray = sources.every((item) => typeof item === 'string');
262
+
263
+ const isArrayFormat = sources.every((item) => Array.isArray(item) && item.every((value) => typeof value === 'string'));
264
+
265
+ const isObjectFormat = sources.every((item) => item && typeof item === 'object' && !Array.isArray(item));
266
+
267
+ if (!isStringArray && !isArrayFormat && !isObjectFormat) {
268
+ throw new TypeError('Sources must be a string array, array of string arrays, or array of objects');
269
+ }
270
+
271
+ if (isStringArray) {
272
+ sources = [sources];
273
+ }
274
+
275
+ const normalizedSources = sources.map((source) => {
276
+ if (Array.isArray(source)) {
277
+ const [icon, url, title, subtitle] = source;
278
+
279
+ return {
280
+ icon,
281
+ url,
282
+ title,
283
+ subtitle,
284
+ };
285
+ }
286
+
287
+ return {
288
+ icon: source.favicon ?? source.icon ?? '',
289
+ url: source.url ?? '',
290
+ title: source.title ?? '',
291
+ subtitle: source.subtitle ?? '',
292
+ };
293
+ });
357
294
 
358
- addSource(sources = [], { resolveUrl = false } = {}) {
359
- // Accept 3 formats:
360
- // 1. Array of objects: [{ icon, url, title, subtitle }] — from v4.7 example
361
- // 2. Array of string arrays: [['iconUrl', 'url', 'text']]
362
- // 3. Single string array (shorthand for format 2): ['iconUrl', 'url', 'text']
363
- const isObjArray = Array.isArray(sources) && sources.every((item) => item && typeof item === 'object' && !Array.isArray(item));
364
- const isStrArrayArray = Array.isArray(sources) && sources.every((item) => Array.isArray(item) && item.every((v) => typeof v === 'string'));
365
- const isFlatStrArray = Array.isArray(sources) && sources.every((item) => typeof item === 'string');
366
-
367
- if (!isObjArray && !isStrArrayArray && !isFlatStrArray) {
368
- throw new TypeError('addSource(): pass an array of objects { icon, url, title, subtitle } or string arrays [iconUrl, url, text]');
369
- }
370
-
371
- let normalized;
372
- if (isObjArray) {
373
- normalized = sources.map((item) => ({
374
- icon: item.icon ?? item.iconUrl ?? item.favicon ?? '',
375
- url: item.url ?? '',
376
- text: item.title ?? item.displayName ?? item.text ?? '',
377
- subtitle: item.subtitle ?? 'AI',
378
- }));
379
- } else {
380
- const arr = isFlatStrArray ? [sources] : sources;
381
- normalized = arr.map(([icon = '', url = '', text = '']) => ({ icon, url, text, subtitle: 'AI' }));
382
- }
383
-
384
- const source = normalized.map(({ icon, url, text, subtitle }) => ({
295
+ const source = normalizedSources.map(({ icon, url, title, subtitle }) => ({
385
296
  source_type: 'THIRD_PARTY',
386
- source_display_name: text,
297
+ source_display_name: title,
387
298
  source_subtitle: subtitle,
388
299
  source_url: url,
389
300
  favicon: {
390
- url: Toolkit.resolveMedia(this.#client, icon, 'image', { resolveUrl }),
301
+ url: Toolkit.resolveMedia(this.#client, icon, 'image'),
391
302
  mime_type: 'image/jpeg',
392
303
  width: 16,
393
304
  height: 16,
394
305
  },
395
306
  }));
396
307
 
397
- this._sections.push(
398
- AIRich.newLayout('Single', {
399
- sources: source,
400
- __typename: 'GenAISearchResultPrimitive',
401
- })
402
- );
308
+ const submessage = this.createAlert('GenAISearchResultPrimitive');
403
309
 
404
- return this;
310
+ const section = AIRich.newLayout('Single', {
311
+ sources: source,
312
+ __typename: 'GenAISearchResultPrimitive',
313
+ });
314
+
315
+ return this._addContent(section, submessage, {
316
+ id,
317
+ replace,
318
+ insertAt,
319
+ });
405
320
  }
406
321
 
407
- /** Add a horizontally-scrollable reel of image/video items. */
408
- addReels(reelsItems = [], { resolveUrl = false } = {}) {
322
+ addReels(reelsItems = [], { id, replace, insertAt } = {}) {
409
323
  if (
410
324
  !(
411
325
  (reelsItems && typeof reelsItems === 'object' && !Array.isArray(reelsItems)) ||
@@ -415,88 +329,64 @@ class AIRich extends BaseBuilder {
415
329
  throw new TypeError('Reels items must be an object or an array of objects');
416
330
  }
417
331
 
418
- if (!Array.isArray(reelsItems)) {
419
- reelsItems = [reelsItems];
420
- }
332
+ const items = Array.isArray(reelsItems) ? reelsItems : [reelsItems];
421
333
 
422
- const reels = reelsItems.map((item) => ({
334
+ const reels = items.map((item) => ({
423
335
  ...item,
424
- _avatar: Toolkit.resolveMedia(this.#client, item.profileIconUrl ?? item.profile_url ?? item.profile ?? '', 'image', { resolveUrl }),
425
- _thumbnail: Toolkit.resolveMedia(this.#client, item.thumbnailUrl ?? item.thumbnail ?? '', 'image', { resolveUrl }),
336
+ _avatar: Toolkit.resolveMedia(this.#client, item.profileIconUrl ?? item.profile_url ?? item.profile ?? '', 'image'),
337
+ _thumbnail: Toolkit.resolveMedia(this.#client, item.thumbnailUrl ?? item.thumbnail ?? '', 'image'),
426
338
  }));
427
339
 
428
- this._submessages.push({
429
- messageType: 9,
430
- contentItemsMetadata: {
431
- contentType: 1,
432
- itemsMetadata: reels.map((item) => ({
433
- reelItem: {
434
- title: item.username ?? '',
435
- profileIconUrl: item._avatar,
436
- thumbnailUrl: item._thumbnail,
437
- videoUrl: item.videoUrl ?? item.url ?? '',
438
- },
439
- })),
340
+ const section = AIRich.newLayout(
341
+ 'HScroll',
342
+ reels.map((item) => ({
343
+ reels_url: item.videoUrl ?? item.url ?? '',
344
+ thumbnail_url: item._thumbnail,
345
+ creator: item.username ?? item.title ?? '',
346
+ avatar_url: item._avatar,
347
+ reels_title: item.reels_title ?? item.title ?? '',
348
+ likes_count: item.likes_count ?? item.like ?? 0,
349
+ shares_count: item.shares_count ?? item.share ?? 0,
350
+ view_count: item.view_count ?? item.view ?? 0,
351
+ reel_source: item.reel_source ?? item.source ?? 'IG',
352
+ is_verified: !!(item.is_verified || item.verified),
353
+ __typename: 'GenAIReelPrimitive',
354
+ }))
355
+ );
356
+
357
+ const submessages = [
358
+ {
359
+ messageType: 9,
360
+ contentItemsMetadata: {
361
+ contentType: 1,
362
+ itemsMetadata: reels.map((item) => ({
363
+ reelItem: {
364
+ title: item.username ?? '',
365
+ profileIconUrl: item._avatar,
366
+ thumbnailUrl: item._thumbnail,
367
+ videoUrl: item.videoUrl ?? item.url ?? '',
368
+ },
369
+ })),
370
+ },
440
371
  },
441
- });
372
+ ];
442
373
 
443
- reels.forEach((item, idx) => {
444
- this._richResponseSources.push({
445
- provider: 'Evernight AI',
446
- thumbnailCDNURL: item._thumbnail,
447
- sourceProviderURL: item.videoUrl ?? item.url ?? '',
448
- sourceQuery: '',
449
- faviconCDNURL: item._avatar,
450
- citationNumber: idx + 1,
451
- sourceTitle: item.username ?? '',
452
- });
374
+ return this._addContent(section, submessages, {
375
+ id,
376
+ replace,
377
+ insertAt,
453
378
  });
454
-
455
- this._sections.push(
456
- AIRich.newLayout(
457
- 'HScroll',
458
- reels.map((item) => ({
459
- reels_url: item.videoUrl ?? item.url ?? '',
460
- thumbnail_url: item._thumbnail,
461
- creator: item.username ?? item.title ?? '',
462
- avatar_url: item._avatar,
463
- reels_title: item.reels_title ?? item.title ?? '',
464
- likes_count: item.likes_count ?? item.like ?? 0,
465
- shares_count: item.shares_count ?? item.share ?? 0,
466
- view_count: item.view_count ?? item.view ?? 0,
467
- reel_source: item.reel_source ?? item.source ?? 'IG',
468
- is_verified: !!(item.is_verified || item.verified),
469
- __typename: 'GenAIReelPrimitive',
470
- }))
471
- )
472
- );
473
-
474
- return this;
475
379
  }
476
380
 
477
- /** Add a full-width image (or grid of images if `imageUrl` is an array). */
478
- /**
479
- * @param {{ resolveUrl?: boolean, instant?: boolean|'only' }} [options]
480
- * `instant: true` — sends BOTH: the GRID_IMAGE card (still shows WA's "can't verify"
481
- * forwarded-download prompt, unavoidable per-design of botForwardedMessage) AND a plain
482
- * (non-forwarded) imageMessage via send()'s inline-image fallback queue (`_inlineImages`,
483
- * shared with addInlineImage()) that renders instantly with no prompt. Two images, by design.
484
- * `instant: 'only'` — Vanz@Add (v4.9.2): skips building the GRID_IMAGE card entirely (no
485
- * submessage, no GenAIImaginePrimitive section) and queues ONLY the plain imageMessage.
486
- * One image, no prompt, nothing to download — use this when you don't need the rich card,
487
- * just the picture to show up immediately.
488
- */
489
- addImage(imageUrl, { resolveUrl = false, instant = false } = {}) {
381
+ addImage(imageUrl, { width, height, status = 'READY', update_text, resolveUrl = false, id, replace, insertAt } = {}) {
490
382
  if (!(typeof imageUrl === 'string' || Buffer.isBuffer(imageUrl) || (Array.isArray(imageUrl) && imageUrl.every((v) => typeof v === 'string' || Buffer.isBuffer(v))))) {
491
383
  throw new TypeError('imageUrl must be string | buffer | array of string/buffer');
492
384
  }
493
- if (instant !== false && instant !== true && instant !== 'only') {
494
- throw new TypeError(`instant must be false, true, or 'only' — got ${JSON.stringify(instant)}`);
495
- }
496
385
 
497
386
  const list = Array.isArray(imageUrl)
498
387
  ? imageUrl.map((v) => {
499
388
  const url = Toolkit.resolveMedia(this.#client, v, 'image', { resolveUrl });
389
+
500
390
  return {
501
391
  imagePreviewUrl: url,
502
392
  imageHighResUrl: url,
@@ -505,6 +395,7 @@ class AIRich extends BaseBuilder {
505
395
  })
506
396
  : (() => {
507
397
  const url = Toolkit.resolveMedia(this.#client, imageUrl, 'image', { resolveUrl });
398
+
508
399
  return [
509
400
  {
510
401
  imagePreviewUrl: url,
@@ -514,115 +405,46 @@ class AIRich extends BaseBuilder {
514
405
  ];
515
406
  })();
516
407
 
517
- const buildCard = instant !== 'only';
518
-
519
- if (buildCard) {
520
- this._submessages.push({
521
- messageType: 1,
522
- gridImageMetadata: {
523
- gridImageUrl: {
524
- imagePreviewUrl: list[0]?.imagePreviewUrl,
525
- },
526
- imageUrls: list,
527
- },
528
- });
529
- }
530
-
531
- list.forEach(({ imagePreviewUrl }) => {
532
- if (buildCard) {
533
- this._sections.push(
534
- AIRich.newLayout('Single', {
535
- media: {
536
- url: imagePreviewUrl,
537
- mime_type: 'image/png',
538
- },
539
- imagine_type: 'IMAGE',
540
- status: { status: 'READY' },
541
- __typename: 'GenAIImaginePrimitive',
542
- })
543
- );
544
- }
545
-
546
- if (instant) {
547
- this._inlineImages.push({ url: imagePreviewUrl, caption: undefined });
548
- }
549
- });
550
-
551
- return this;
552
- }
553
-
554
- // Vanz@Fix 15-08-26 (bug 41) --- addImage() only builds GRID_IMAGE (messageType 1).
555
- // There was no helper for standalone INLINE_IMAGE (messageType 3): callers were manually
556
- // pushing addSubmessage() (correct proto shape) + addSection() (WRONG shape — reused the
557
- // GRID_IMAGE/GenAIImaginePrimitive section schema instead of GenAIInlineImageUXPrimitive),
558
- // which broke client-side unifiedResponse rendering even though the submessage itself was fine.
559
- // Mirrors RichSubMessageType.INLINE_IMAGE handling in rich-message-utils.js's toUnified().
560
- /** 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). */
561
- addInlineImage(imageUrl, { text = '', alignment = 'center', tapLinkUrl = '', resolveUrl = false } = {}) {
562
- if (!(typeof imageUrl === 'string' || Buffer.isBuffer(imageUrl) || (imageUrl && typeof imageUrl === 'object'))) {
563
- throw new TypeError('imageUrl must be string | buffer | { imagePreviewUrl, imageHighResUrl, sourceUrl }');
564
- }
565
-
566
- const ALIGNMENT_ENUM = { leading: 0, trailing: 1, center: 2 };
567
- const ALIGNMENT_NAME = ['AI_RICH_RESPONSE_IMAGE_LAYOUT_LEADING_ALIGNED', 'AI_RICH_RESPONSE_IMAGE_LAYOUT_TRAILING_ALIGNED', 'AI_RICH_RESPONSE_IMAGE_LAYOUT_CENTER_ALIGNED'];
568
- const alignmentNum = typeof alignment === 'number' ? alignment : (ALIGNMENT_ENUM[String(alignment).toLowerCase()] ?? ALIGNMENT_ENUM.center);
569
-
570
- const url =
571
- imageUrl && typeof imageUrl === 'object'
572
- ? {
573
- imagePreviewUrl: imageUrl.imagePreviewUrl || imageUrl.url,
574
- imageHighResUrl: imageUrl.imageHighResUrl || imageUrl.url,
575
- sourceUrl: imageUrl.sourceUrl || imageUrl.url,
576
- }
577
- : (() => {
578
- const resolved = Toolkit.resolveMedia(this.#client, imageUrl, 'image', { resolveUrl });
579
- return { imagePreviewUrl: resolved, imageHighResUrl: resolved, sourceUrl: resolved };
580
- })();
581
-
582
- this._submessages.push({
583
- messageType: 3,
584
- imageMetadata: {
585
- imageUrl: url,
586
- imageText: text,
587
- alignment: alignmentNum,
588
- tapLinkUrl,
589
- },
590
- });
591
-
592
- this._sections.push(
408
+ const sections = list.map(({ imagePreviewUrl }) =>
593
409
  AIRich.newLayout('Single', {
594
- image_url: {
595
- image_preview_url: url.imagePreviewUrl || '',
596
- image_high_res_url: url.imageHighResUrl || '',
597
- source_url: url.sourceUrl || '',
410
+ media: {
411
+ url: imagePreviewUrl,
412
+ mime_type: 'image/png',
413
+ width,
414
+ height,
415
+ },
416
+ imagine_type: 'IMAGE',
417
+ status: {
418
+ status,
419
+ update_text,
598
420
  },
599
- image_text: text,
600
- alignment: ALIGNMENT_NAME[alignmentNum],
601
- tap_link_url: tapLinkUrl,
602
- __typename: 'GenAIInlineImageUXPrimitive',
421
+ __typename: 'GenAIImaginePrimitive',
603
422
  })
604
423
  );
605
424
 
606
- // Vanz@Fix (bug 42): stash for the imageMessage fallback in send()
607
- this._inlineImages.push({
608
- url: url.sourceUrl || url.imageHighResUrl || url.imagePreviewUrl,
609
- caption: text || undefined,
610
- });
425
+ const submessage = {
426
+ messageType: 1,
427
+ gridImageMetadata: {
428
+ gridImageUrl: {
429
+ imagePreviewUrl: list[0]?.imagePreviewUrl,
430
+ },
431
+ imageUrls: list,
432
+ },
433
+ };
611
434
 
612
- return this;
435
+ if (id && sections.length !== 1) {
436
+ throw new Error('Cannot assign one id to multiple image sections');
437
+ }
438
+
439
+ return this._addContent(sections, submessage, {
440
+ id,
441
+ replace,
442
+ insertAt,
443
+ });
613
444
  }
614
445
 
615
- // Vanz@Perf 15-08-26 --- autoFill defaults to false (arslan-baileys behavior): skips the
616
- // fetch-full-video + ffmpeg-frame-extraction + duration-parse round trip per video, which
617
- // was the main source of blurose's slower response time. Pass { autoFill: true } to opt
618
- // back into the complete/slow path (real thumbnail + duration + file_length).
619
- // Vanz@Fix 23-08-26 --- addVideo() had no resolveUrl option at all (unlike addImage()), so the
620
- // video url always stayed a raw external link, which stock WA clients show a "download" state
621
- // for before rendering. Mirrors addImage()'s { resolveUrl } — when true, the url is uploaded to
622
- // WA's own media server first via Toolkit.toUrl() so it renders instantly like WA-native media.
623
- /** Add a video block. */
624
- addVideo(videoUrl, { autoFill = false, resolveUrl = false } = {}) {
625
- const isObjectVideo = (v) => v && typeof v === 'object' && v.url;
446
+ addVideo(videoUrl, { autoFill = true, status = 'READY', estimatedTime, id, replace, insertAt } = {}) {
447
+ const isObjectVideo = (v) => v && typeof v === 'object' && !Array.isArray(v) && v.url;
626
448
 
627
449
  const isValidPrimitive =
628
450
  typeof videoUrl === 'string' ||
@@ -636,17 +458,15 @@ class AIRich extends BaseBuilder {
636
458
 
637
459
  const items = Array.isArray(videoUrl) ? videoUrl : [videoUrl];
638
460
 
639
- this._submessages.push({
640
- messageType: 2,
641
- messageText: '[ Video tidak dapat dimuat ]',
642
- });
461
+ const alert = this.createAlert('GenAIImaginePrimitive (ANIMATE)');
462
+
463
+ const sections = [];
464
+ const submessages = [];
643
465
 
644
- items.forEach((item) => {
466
+ for (const item of items) {
645
467
  const isObject = isObjectVideo(item);
646
468
 
647
- const url = isObject
648
- ? Toolkit.resolveMedia(this.#client, item.url ?? '', 'video', { resolveUrl })
649
- : Toolkit.resolveMedia(this.#client, item, 'video', { resolveUrl });
469
+ const url = isObject ? Toolkit.resolveMedia(this.#client, item.url ?? '', 'video') : Toolkit.resolveMedia(this.#client, item, 'video');
650
470
 
651
471
  const bufferPromise = autoFill ? Promise.resolve(url).then((u) => Toolkit.fetchBuffer(u)) : null;
652
472
 
@@ -672,17 +492,15 @@ class AIRich extends BaseBuilder {
672
492
  height: 300,
673
493
  })
674
494
  : autoFill
675
- ? bufferPromise
676
- ? bufferPromise.then((b) =>
677
- Toolkit.getMp4Preview(b, {
678
- time: 0,
679
- result: 'base64',
680
- })
681
- )
682
- : null
495
+ ? bufferPromise?.then((b) =>
496
+ Toolkit.getMp4Preview(b, {
497
+ time: 0,
498
+ result: 'base64',
499
+ })
500
+ )
683
501
  : null;
684
502
 
685
- this._sections.push(
503
+ sections.push(
686
504
  AIRich.newLayout('Single', {
687
505
  media: {
688
506
  url,
@@ -691,34 +509,37 @@ class AIRich extends BaseBuilder {
691
509
  duration,
692
510
  },
693
511
  imagine_type: 'ANIMATE',
694
- status: { status: 'READY' },
512
+ status: {
513
+ status,
514
+ estimated_completion_time: estimatedTime != null ? Math.floor((Date.now() + estimatedTime) / 1000) : undefined,
515
+ },
695
516
  thumbnail: {
696
517
  raw_media: thumbnail,
697
518
  },
698
519
  __typename: 'GenAIImaginePrimitive',
699
520
  })
700
521
  );
701
- });
702
-
703
- return this;
704
- }
522
+ }
705
523
 
706
- /** Add an inline product card (or array of cards). Each item needs at least a `title`. */
707
- addProduct(data = {}, { resolveUrl = false } = {}) {
708
- if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
709
- throw new TypeError('Product items must be an object or an array of objects');
524
+ if (alert !== undefined) {
525
+ submessages.push(alert);
710
526
  }
711
527
 
712
- const itemsToCheck = Array.isArray(data) ? data : [data];
713
- const missingTitleAt = itemsToCheck.findIndex((item) => !item.title);
714
- if (missingTitleAt !== -1) {
715
- throw new TypeError(`addProduct() item[${missingTitleAt}] is missing a required "title"`);
528
+ if (submessages.length > 1) {
529
+ throw new Error('Video content can only have one submessage');
716
530
  }
717
531
 
718
- this._submessages.push({
719
- messageType: 2,
720
- messageText: '[ Produk tidak dapat dimuat ]',
532
+ return this._addContent(sections, submessages[0], {
533
+ id,
534
+ replace,
535
+ insertAt,
721
536
  });
537
+ }
538
+
539
+ addProduct(data = {}, { id, replace, insertAt } = {}) {
540
+ if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
541
+ throw new TypeError('Product items must be an object or an array of objects');
542
+ }
722
543
 
723
544
  const items = Array.isArray(data) ? data : [data];
724
545
 
@@ -729,41 +550,41 @@ class AIRich extends BaseBuilder {
729
550
  sale_price: item.sale_price,
730
551
  product_url: item.product_url ?? item.url,
731
552
  image: {
732
- url: Toolkit.resolveMedia(this.#client, item.image_url ?? item.image, 'image', { resolveUrl }),
553
+ url: Toolkit.resolveMedia(this.#client, item.image_url ?? item.image, 'image'),
733
554
  },
734
555
  additional_images: [
735
556
  {
736
- url: Toolkit.resolveMedia(this.#client, item.icon_url ?? item.icon, 'image', { resolveUrl }),
557
+ url: Toolkit.resolveMedia(this.#client, item.icon_url ?? item.icon, 'image'),
737
558
  },
738
559
  ],
739
560
  __typename: 'GenAIProductItemCardPrimitive',
740
561
  }));
741
562
 
742
- this._sections.push(AIRich.newLayout(Array.isArray(data) ? 'HScroll' : 'Single', Array.isArray(data) ? product : product[0]));
563
+ const section = AIRich.newLayout(Array.isArray(data) ? 'HScroll' : 'Single', Array.isArray(data) ? product : product[0]);
743
564
 
744
- return this;
565
+ const submessage = this.createAlert('GenAIProductItemCardPrimitive');
566
+
567
+ return this._addContent(section, submessage, {
568
+ id,
569
+ replace,
570
+ insertAt,
571
+ });
745
572
  }
746
573
 
747
- /** Add an inline social-post style card (or array of cards). */
748
- addPost(data = {}, { resolveUrl = false } = {}) {
574
+ addPost(data = {}, { id, replace, insertAt } = {}) {
749
575
  if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
750
576
  throw new TypeError('Post items must be an object or an array of objects');
751
577
  }
752
578
 
753
579
  const posts = Array.isArray(data) ? data : [data];
754
580
 
755
- this._submessages.push({
756
- messageType: 2,
757
- messageText: '[ Postingan tidak dapat dimuat ]',
758
- });
759
-
760
581
  const primitives = posts.map((p) => ({
761
582
  title: p.title ?? '',
762
583
  subtitle: p.subtitle ?? '',
763
584
  username: p.username ?? '',
764
- profile_picture_url: Toolkit.resolveMedia(this.#client, p.profile_picture_url ?? p.profile_url ?? p.profile ?? '', 'image', { resolveUrl }),
585
+ profile_picture_url: Toolkit.resolveMedia(this.#client, p.profile_picture_url ?? p.profile_url ?? p.profile ?? '', 'image'),
765
586
  is_verified: !!(p.is_verified || p.verified),
766
- thumbnail_url: Toolkit.resolveMedia(this.#client, p.thumbnail_url ?? p.thumbnail ?? '', 'image', { resolveUrl }),
587
+ thumbnail_url: Toolkit.resolveMedia(this.#client, p.thumbnail_url ?? p.thumbnail ?? '', 'image'),
767
588
  post_caption: p.post_caption ?? p.caption ?? '',
768
589
  likes_count: p.likes_count ?? p.like ?? 0,
769
590
  comments_count: p.comments_count ?? p.comment ?? 0,
@@ -772,456 +593,462 @@ class AIRich extends BaseBuilder {
772
593
  post_deeplink: p.post_deeplink ?? p.deeplink ?? '',
773
594
  source_app: p.source_app || p.source || 'INSTAGRAM',
774
595
  footer_label: p.footer_label ?? p.footer ?? '',
775
- footer_icon: Toolkit.resolveMedia(this.#client, p.footer_icon ?? p.icon ?? '', 'image', { resolveUrl }),
596
+ footer_icon: Toolkit.resolveMedia(this.#client, p.footer_icon ?? p.icon ?? '', 'image'),
776
597
  is_carousel: posts.length > 1,
777
598
  orientation: p.orientation ?? 'LANDSCAPE',
778
599
  post_type: p.post_type ?? 'VIDEO',
779
600
  __typename: 'GenAIPostPrimitive',
780
601
  }));
781
602
 
782
- this._sections.push(AIRich.newLayout('HScroll', primitives));
603
+ const section = AIRich.newLayout('HScroll', primitives);
783
604
 
784
- return this;
605
+ const submessage = this.createAlert('GenAIPostPrimitive');
606
+
607
+ return this._addContent(section, submessage, {
608
+ id,
609
+ replace,
610
+ insertAt,
611
+ });
785
612
  }
786
613
 
787
- // Vanz@Add 24-08-26 --- ported from temen's MessageBuilderV4.7 (setResponseId/setBotResponseId/
788
- // refreshResponseId/refreshBotResponseId), rewritten for this fork. Pins the two ids build()
789
- // generates (see constructor comment) so a rebuilt message can reuse the same response_id/
790
- // botResponseId — needed for editing an already-sent AIRich message in place.
614
+ addMetadata(text, { id, replace, insertAt } = {}) {
615
+ if (typeof text !== 'string') {
616
+ throw new TypeError('Text must be a string');
617
+ }
791
618
 
792
- /** 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. */
793
- setResponseId(id) {
794
- if (typeof id !== 'string' || !id) throw new TypeError('setResponseId(id) requires a non-empty string');
795
- this._responseId = id;
796
- return this;
797
- }
619
+ const section = AIRich.newLayout('Single', {
620
+ text,
621
+ __typename: 'GenAIMetadataTextPrimitive',
622
+ });
798
623
 
799
- /** Un-pin `unifiedResponse.response_id`, generating a fresh crypto.randomUUID() immediately (not deferred to the next build()). */
800
- refreshResponseId() {
801
- this._responseId = crypto.randomUUID();
802
- return this;
803
- }
624
+ const submessage = {
625
+ messageType: 2,
626
+ messageText: text,
627
+ };
804
628
 
805
- /** Pin `botMetadata.botResponseId` to a specific value instead of a fresh random one each build(). */
806
- setBotResponseId(id) {
807
- if (typeof id !== 'string' || !id) throw new TypeError('setBotResponseId(id) requires a non-empty string');
808
- this._botResponseId = id;
809
- return this;
629
+ return this._addContent(section, submessage, {
630
+ id,
631
+ replace,
632
+ insertAt,
633
+ });
810
634
  }
811
635
 
812
- /** Un-pin `botMetadata.botResponseId`, generating a fresh crypto.randomUUID() immediately. */
813
- refreshBotResponseId() {
814
- this._botResponseId = crypto.randomUUID();
815
- return this;
816
- }
636
+ addTip(text, { id, replace, insertAt } = {}) {
637
+ if (typeof text !== 'string') {
638
+ throw new TypeError('Text must be a string');
639
+ }
817
640
 
818
- // Vanz@Add 25-08-26 --- ported from temen's MessageBuilderV4.7 (hasId/getIds/peek/delete).
819
- // That fork tracked every block in a unified `_nodes` array so query/delete-by-id was free;
820
- // this fork instead tracks blocks in the `_blocks` Map (id -> {subItems, secItems}, populated
821
- // by the constructor's Proxy on every add*/set* call that passes {id}) but never exposed a way
822
- // to query or undo one after the fact — you could insertAt an id but never inspect, check, or
823
- // remove it. These 4 read/delete that same Map, so no changes to the Proxy itself were needed.
641
+ const section = AIRich.newLayout('Single', {
642
+ text: 'ⓘ ' + text,
643
+ __typename: 'GenAIMetadataTextPrimitive',
644
+ });
824
645
 
825
- /** Check whether a block id was registered by an earlier `add*()`/`set*()` call passing `{id}`. */
826
- hasId(id) {
827
- return typeof id === 'string' && this._blocks.has(id);
828
- }
646
+ const submessage = {
647
+ messageType: 2,
648
+ messageText: text,
649
+ };
829
650
 
830
- /** List every block id registered so far, in no particular order. */
831
- getIds() {
832
- return [...this._blocks.keys()];
651
+ return this._addContent(section, submessage, {
652
+ id,
653
+ replace,
654
+ insertAt,
655
+ });
833
656
  }
834
657
 
835
- /** Inspect a registered block without modifying it. Returns `null` if `id` isn't registered. */
836
- peek(id) {
837
- const block = this._blocks.get(id);
838
- if (!block) return null;
658
+ addWidget(data, { layout, id, replace, insertAt, ...options } = {}) {
659
+ if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
660
+ throw new TypeError('Widget must be an object or an array of objects');
661
+ }
839
662
 
840
- return { id, sections: [...block.secItems], submessages: [...block.subItems] };
841
- }
663
+ const isArray = Array.isArray(data);
842
664
 
843
- /** Remove a previously-added block (by the `id` passed to its `add*()`/`set*()` call) from the message. Throws if `id` isn't registered. */
844
- delete(id) {
845
- const block = this._blocks.get(id);
846
- if (!block) throw new Error(`delete(id): no block registered with id "${id}"`);
665
+ const items = isArray ? data : [data];
847
666
 
848
- for (const item of block.subItems) {
849
- const idx = this._submessages.indexOf(item);
850
- if (idx !== -1) this._submessages.splice(idx, 1);
851
- }
852
- for (const item of block.secItems) {
853
- const idx = this._sections.indexOf(item);
854
- if (idx !== -1) this._sections.splice(idx, 1);
855
- }
667
+ const widgets = items.map((item) => ({
668
+ __typename: 'GenAI3PExtWidgetPrimitive',
856
669
 
857
- this._blocks.delete(id);
858
- return this;
859
- }
670
+ header: {
671
+ __typename: 'GenAI3PExtWidgetStandardHeader',
672
+ title: item.title ?? '',
673
+ ...(item.header ?? {}),
674
+ },
860
675
 
861
- /** 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. */
862
- addMetadata(text) {
863
- if (typeof text !== 'string' || !text) throw new TypeError('addMetadata(text) requires a non-empty string');
676
+ body: {
677
+ __typename: 'GenAI3PExtCalendarEventList',
678
+ sections: item.sections ?? [],
679
+
680
+ ctas: (item.actions ?? []).map((action) => ({
681
+ __typename: 'GenAI3PExtWidgetCTA',
682
+ label: action.label ?? '',
683
+ state: action.state ?? 'PENDING',
684
+ kind: action.kind ?? 'OTHER',
685
+ tool_call_id: action.tool_call_id ?? action.id ?? '',
686
+
687
+ ...(action.toast && {
688
+ toast: {
689
+ __typename: 'GenAI3PExtWidgetToast',
690
+ label: action.toast.label ?? action.label ?? '',
691
+ },
692
+ }),
693
+ })),
864
694
 
865
- this._submessages.push({
866
- messageType: 2,
867
- messageText: text,
868
- });
695
+ ...(item.body ?? {}),
696
+ },
697
+ }));
869
698
 
870
- this._sections.push(
871
- AIRich.newLayout('Single', {
872
- text,
873
- __typename: 'GenAIMetadataTextPrimitive',
874
- })
875
- );
699
+ const section = AIRich.newLayout(layout ?? (isArray ? 'HScroll' : 'Single'), isArray ? widgets : widgets[0], options);
876
700
 
877
- return this;
701
+ const submessage = this.createAlert('GenAI3PExtWidgetStandardHeader');
702
+
703
+ return this._addContent(section, submessage, {
704
+ id,
705
+ replace,
706
+ insertAt,
707
+ });
878
708
  }
879
709
 
880
- /** Add a small "tip" callout banner. @param {string} text */
881
- addTip(text) {
882
- if (typeof text !== 'string' || !text) {
883
- throw new TypeError('addTip(text) requires a non-empty string');
710
+ addFooterAction(data, { layout, id, replace, insertAt, ...options } = {}) {
711
+ if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
712
+ throw new TypeError('Footer action must be an object or an array of objects');
884
713
  }
885
714
 
886
- this._submessages.push({
887
- messageType: 2,
888
- messageText: text,
889
- });
715
+ const isArray = Array.isArray(data);
890
716
 
891
- this._sections.push(
892
- AIRich.newLayout('Single', {
893
- text,
894
- __typename: 'GenAIMetadataTextPrimitive',
895
- })
896
- );
717
+ const items = isArray ? data : [data];
897
718
 
898
- return this;
899
- }
719
+ const actions = items.map((item) => ({
720
+ __typename: 'GenAIFooterActionPrimitive',
900
721
 
901
- // Vanz@Add 22-08-26 (v4.7) --- addHeading/addWidget/addFooterAction: 3 primitives
902
- // reverse-engineered from captured Meta-AI-in-WhatsApp traffic that this project's own crm/snip
903
- // tooling (see rich-message-utils.js) dumps for study. Not in any public Baileys schema, so
904
- // unknown enum values (kind/state on addWidget's ctas) are passed through as observed rather
905
- // than guessed at, and documented as experimental below.
906
- // Vanz@Fix 25-08-26 --- removed addImageCard(): its GenAIImagePrimitive/preview_image+full_image
907
- // shape was mis-reverse-engineered (not a real WA schema) and crashed the client renderer on
908
- // arrival. addImage() already covers static image cards correctly — use that instead.
722
+ cta_text: item.text ?? item.cta_text ?? '',
909
723
 
910
- /** Add a large heading-style text block (`FOATextPrimitive`) — visually distinct from `addText()`'s regular paragraph text. */
911
- addHeading(text) {
912
- if (typeof text !== 'string' || !text) {
913
- throw new TypeError('addHeading(text) requires a non-empty string');
914
- }
724
+ cta_type: item.type ?? item.cta_type ?? 'OPEN_URL',
915
725
 
916
- this._submessages.push({
917
- messageType: 2,
918
- messageText: text,
919
- });
726
+ cta_url: item.url ?? item.cta_url ?? '',
727
+ }));
920
728
 
921
- this._sections.push(
922
- AIRich.newLayout('Single', {
923
- text,
924
- __typename: 'FOATextPrimitive',
925
- })
926
- );
729
+ const section = AIRich.newLayout(layout ?? (isArray ? 'HScroll' : 'Single'), isArray ? actions : actions[0], options);
927
730
 
928
- return this;
731
+ const submessage = this.createAlert('GenAIFooterActionPrimitive');
732
+
733
+ return this._addContent(section, submessage, {
734
+ id,
735
+ replace,
736
+ insertAt,
737
+ });
929
738
  }
930
739
 
931
- /**
932
- * Add a "3P extension" widget card (`GenAI3PExtWidgetPrimitive`) a small panel with a title and
933
- * a row of tappable CTA chips. Per captured traffic these CTAs call back into a tool (`tool_call_id`)
934
- * rather than opening a url; `kind`/`state` semantics beyond the observed `'OTHER'`/`'PENDING'`
935
- * defaults aren't publicly documented, so treat this as experimental.
936
- *
937
- * Vanz@Add (v4.8) --- accepts an `{ layout }` override so consecutive `addWidget()` calls can
938
- * pick different renderings (e.g. one `HScroll` row, one `ActionRow` stack) instead of always
939
- * inferring HScroll-for-array/Single-for-object from the shape of `data`. Also accepts either
940
- * `ctas` (original key, matches the wire field) or `actions` (alias) on each item — whichever
941
- * is present is used; `ctas` wins if both are somehow given.
942
- * @param {Record<string, any>|Record<string, any>[]} data `{ title, ctas|actions: [{ label, tool_call_id?, kind?, state?, toast? }] }` (single or array).
943
- * @param {{layout?: 'Single'|'HScroll'|'ActionRow'|string}} [options] `layout` overrides the default single/array inference.
944
- */
945
- addWidget(data = {}, { layout } = {}) {
946
- const items = Array.isArray(data) ? data : [data];
740
+ addTask(data, { id, replace, insertAt } = {}) {
741
+ if (Array.isArray(data) && data.length === 0) {
742
+ throw new TypeError('Task array must not be empty');
743
+ }
744
+
745
+ const isValidSingle = data && typeof data === 'object' && !Array.isArray(data);
746
+ const isValidArray = Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item));
947
747
 
948
- // Vanz@Fix (bug 44) --- layout: 'Single' forces `widgets[0]` below (a "Single" layout's
949
- // view_model can only ever hold one `primitive`, never a `primitives` array see
950
- // newLayout()). Previously an explicit { layout: 'Single' } combined with a multi-item
951
- // array silently dropped every item past the first with no error. Fail loud instead.
952
- if (layout === 'Single' && items.length > 1) {
953
- throw new TypeError(`addWidget(): layout "Single" can only hold one widget (got ${items.length}) — use "HScroll"/"ActionRow" (or omit layout) for multiple`);
748
+ if (!isValidSingle && !isValidArray) {
749
+ throw new TypeError('Task must be an object or a non-empty array of objects');
954
750
  }
955
751
 
956
- items.forEach((item, i) => {
957
- // header.title or top-level title required
958
- const hasTitle = item?.title || item?.header?.title;
959
- if (!hasTitle) {
960
- throw new TypeError(`addWidget() item[${i}] is missing a required "title" (or "header.title")`);
961
- }
962
- const ctas = item.ctas ?? item.actions;
963
- if (!Array.isArray(ctas) || !ctas.length) {
964
- throw new TypeError(`addWidget() item[${i}] requires a non-empty "ctas" (or "actions") array`);
965
- }
966
- });
752
+ const isArray = Array.isArray(data);
753
+ const items = isArray ? data : [data];
967
754
 
968
- this._submessages.push({
969
- messageType: 2,
970
- messageText: items.map((item) => item.header?.title ?? item.title).join(', '),
971
- });
755
+ const tasks = items.map((item) => {
756
+ if (typeof item.title !== 'string' || !item.title) {
757
+ throw new TypeError('addTask() requires a "title"');
758
+ }
972
759
 
973
- // Vanz@Fix (bug 45) --- auto tool_call_id used to be `idx` scoped per-item (ctas.map's own
974
- // index), resetting to 0 for every widget item. Two items (or two separate addWidget()
975
- // calls) that both omit tool_call_id/id ended up minting the identical auto id ("00"),
976
- // so a CTA tap could route to the wrong widget's tool call. Track the counter on the
977
- // instance instead so every auto-generated id is unique for this AIRich's lifetime.
978
- this._widgetCtaCounter ??= 0;
979
-
980
- const widgets = items.map((item) => {
981
- const ctas = item.ctas ?? item.actions;
982
- // header accepts either a string title (legacy) or an object { title, subtitle }
983
- const headerTitle = item.header?.title ?? item.title;
984
- const headerSubtitle = item.header?.subtitle ?? item.subtitle ?? undefined;
985
760
  return {
986
- header: {
987
- title: headerTitle,
988
- ...(headerSubtitle !== undefined && { subtitle: headerSubtitle }),
989
- __typename: 'GenAI3PExtWidgetStandardHeader',
990
- },
991
- body: {
992
- sections: item.sections ?? [],
993
- ctas: ctas.map((cta) => ({
994
- label: cta.label ?? '',
995
- state: cta.state ?? 'PENDING',
996
- kind: cta.kind ?? 'OTHER',
997
- tool_call_id: cta.tool_call_id ?? cta.id ?? String(this._widgetCtaCounter++).padStart(2, '0'),
998
- ...(cta.toast !== false && {
999
- toast: { label: typeof cta.toast === 'string' ? cta.toast : headerTitle, __typename: 'GenAI3PExtWidgetToast' },
1000
- }),
1001
- __typename: 'GenAI3PExtWidgetCTA',
1002
- })),
1003
- __typename: item.body_typename ?? 'GenAI3PExtCalendarEventList',
1004
- },
1005
- __typename: 'GenAI3PExtWidgetPrimitive',
761
+ task_id: item.task_id ?? '',
762
+ title: item.title,
763
+ subtitle: item.subtitle ?? '',
764
+ status: item.status ?? 'PENDING',
765
+ __typename: 'GenAITaskPrimitive',
1006
766
  };
1007
767
  });
1008
768
 
1009
- const resolvedLayout = layout ?? (Array.isArray(data) ? 'HScroll' : 'Single');
1010
- const asArray = resolvedLayout !== 'Single';
769
+ const section = AIRich.newLayout(isArray ? 'HScroll' : 'Single', isArray ? tasks : tasks[0]);
1011
770
 
1012
- this._sections.push(AIRich.newLayout(resolvedLayout, asArray ? widgets : widgets[0]));
771
+ const submessage = this.createAlert('GenAITaskPrimitive');
1013
772
 
1014
- return this;
773
+ return this._addContent(section, submessage, {
774
+ id,
775
+ replace,
776
+ insertAt,
777
+ });
1015
778
  }
1016
779
 
1017
- /**
1018
- * Add footer action link(s) (`GenAIFooterActionPrimitive`) e.g. "Join our WhatsApp Group/Channel"
1019
- * chips shown below the response, separate from `setFooter()`'s plain text footer.
1020
- * @param {{text: string, url: string, type?: string}|{text: string, url: string, type?: string}[]} actions
1021
- */
1022
- addFooterAction(actions) {
1023
- const items = Array.isArray(actions) ? actions : [actions];
780
+ addQuotaUpsell(data, { id, replace, insertAt } = {}) {
781
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
782
+ throw new TypeError('Quota upsell must be an object');
783
+ }
1024
784
 
1025
- items.forEach((item, i) => {
1026
- if (!item?.text || !item?.url) {
1027
- throw new TypeError(`addFooterAction() item[${i}] requires both "text" and "url"`);
1028
- }
1029
- });
785
+ if (typeof data.title !== 'string' || !data.title) {
786
+ throw new TypeError('addQuotaUpsell() requires a "title"');
787
+ }
1030
788
 
1031
- const primitives = items.map((item) => ({
1032
- cta_text: item.text,
1033
- cta_type: item.type ?? 'OPEN_URL',
1034
- cta_url: item.url,
1035
- __typename: 'GenAIFooterActionPrimitive',
1036
- }));
789
+ const buttons = Array.isArray(data.buttons) ? data.buttons : [];
1037
790
 
1038
- this._sections.push(AIRich.newLayout('HScroll', primitives));
791
+ const primitive = {
792
+ __typename: 'GenAIMetaSubsQuotaUpsellPrimitive',
793
+ title: data.title,
794
+ body_line1: data.body_line1 ?? data.body ?? '',
795
+ body_line2: data.body_line2 ?? '',
796
+
797
+ buttons: buttons.map((btn) => ({
798
+ __typename: 'GenAIMetaSubsQuotaUpsellButton',
799
+ label: btn.label ?? '',
800
+ action: btn.action ?? 'OPEN_DEEPLINK',
801
+ deeplink: btn.deeplink ?? '',
802
+ })),
803
+ };
1039
804
 
1040
- return this;
805
+ const section = AIRich.newLayout('Single', primitive);
806
+
807
+ const submessage = this.createAlert('GenAIMetaSubsQuotaUpsellPrimitive');
808
+
809
+ return this._addContent(section, submessage, {
810
+ id,
811
+ replace,
812
+ insertAt,
813
+ });
1041
814
  }
1042
815
 
1043
- // Vanz@Add (v4.8) --- 8 primitives from the 20-item reference test script that had no
1044
- // add*() helper yet (Divider/Spacer/Task/ProgressStatus/ThinkingStatus/QuotaUpsell/FOABloks
1045
- // have no dedicated AIRichResponseSubMessageType — WA carries them purely in the
1046
- // unifiedResponse view-model JSON, so their submessage falls back to plain AI_RICH_RESPONSE_TEXT
1047
- // like addTip/addHeading already do. Latex is the one exception: it has a real proto type
1048
- // (AI_RICH_RESPONSE_LATEX = 8, confirmed in WAProto) with its own latexMetadata, so that one
1049
- // gets a proper submessage instead of the text fallback.
1050
-
1051
- /** Add a plain horizontal divider line (`GenAIDividerPrimitive`, no content). */
1052
- addDivider() {
1053
- this._submessages.push({ messageType: 2, messageText: '---' });
1054
- this._sections.push(AIRich.newLayout('Single', { __typename: 'GenAIDividerPrimitive' }));
1055
- return this;
816
+ /** GenAIDividerPrimitive plain horizontal line, no content. */
817
+ addDivider({ id, replace, insertAt } = {}) {
818
+ const section = AIRich.newLayout('Single', {
819
+ __typename: 'GenAIDividerPrimitive',
820
+ });
821
+
822
+ return this._addContent(section, undefined, {
823
+ id,
824
+ replace,
825
+ insertAt,
826
+ });
1056
827
  }
1057
828
 
1058
- /** Add blank vertical spacing (`GenAISpacerPrimitive`). @param {number} [spacing=1] Spacing unit, per observed traffic. */
1059
- addSpacer(spacing = 1) {
1060
- if (typeof spacing !== 'number' || spacing < 0) {
1061
- throw new TypeError('addSpacer(spacing) requires a non-negative number');
1062
- }
1063
- this._submessages.push({ messageType: 2, messageText: `spasi ${spacing}` });
1064
- this._sections.push(AIRich.newLayout('Single', { spacing, __typename: 'GenAISpacerPrimitive' }));
1065
- return this;
829
+ /** GenAISpacerPrimitive blank vertical spacing. */
830
+ addSpacer(spacing, { id, replace, insertAt } = {}) {
831
+ const section = AIRich.newLayout('Single', {
832
+ spacing,
833
+ __typename: 'GenAISpacerPrimitive',
834
+ });
835
+
836
+ return this._addContent(section, undefined, {
837
+ id,
838
+ replace,
839
+ insertAt,
840
+ });
1066
841
  }
1067
842
 
1068
843
  /**
1069
- * Add a rendered LaTeX expression (`GenAILatexUXPrimitive`), with a real `AI_RICH_RESPONSE_LATEX`
1070
- * submessage (unlike most primitives in this block, this one has a proper proto type).
1071
- * @param {string} expression LaTeX source, e.g. `'$$E = mc^2$$'`.
844
+ * GenAILatexItem corrected after cross-checking an older, actually-shipped build
845
+ * (MessageBuilder v4.7, pre-dates this primitive-card guess). LaTeX in this ecosystem
846
+ * is NOT a standalone card — the real, working mechanism is an inline entity inside
847
+ * addText()'s own extractIE() parser (`[expr|width|height|font_height|padding]<imageUrl>`
848
+ * syntax), and it requires a PRE-RENDERED image of the equation — WhatsApp does not
849
+ * render raw LaTeX text client-side. The previous addLatex() (standalone
850
+ * GenAILatexUXPrimitive card, text-only) was confirmed via eval to render blank because
851
+ * that primitive/approach never existed in the first place. This version is a thin
852
+ * wrapper around the real, already-working addText() inline syntax.
1072
853
  */
1073
- addLatex(expression) {
854
+ addLatex(expression, { url, width, height, font_height, padding, id, replace, insertAt } = {}) {
1074
855
  if (typeof expression !== 'string' || !expression) {
1075
856
  throw new TypeError('addLatex(expression) requires a non-empty string');
1076
857
  }
1077
- this._submessages.push({
1078
- messageType: 8,
1079
- latexMetadata: { text: expression, expressions: [{ latexExpression: expression }] },
858
+
859
+ if (!url) {
860
+ throw new TypeError('addLatex() requires a pre-rendered equation image "url" — WhatsApp renders LaTeX as an inline image, not raw text');
861
+ }
862
+
863
+ const extras = [width, height, font_height, padding];
864
+
865
+ while (extras.length && extras[extras.length - 1] == null) {
866
+ extras.pop();
867
+ }
868
+
869
+ const raw = [expression, ...extras].join('|');
870
+
871
+ return this.addText(`[${raw}]<${url}>`, {
872
+ id,
873
+ replace,
874
+ insertAt,
1080
875
  });
1081
- this._sections.push(AIRich.newLayout('Single', { latex_expression: expression, __typename: 'GenAILatexUXPrimitive' }));
1082
- return this;
1083
876
  }
1084
877
 
1085
878
  /**
1086
- * Add a task/checklist card (`GenAITaskPrimitive`).
1087
- * @param {{task_id?: string, title: string, subtitle?: string, status?: string}} data
879
+ * GenAIInlineImageUXPrimitive messageType 3 (INLINE_IMAGE) submessage, field layout
880
+ * ported from rich-message-utils.js toUnified() INLINE_IMAGE case (same image-url-object
881
+ * fix as addImage()/bug 40 round 2), not guessed from the .d.ts alone.
1088
882
  */
1089
- addTask(data = {}) {
1090
- if (!data?.title) {
1091
- throw new TypeError('addTask() requires a "title"');
1092
- }
1093
- this._submessages.push({ messageType: 2, messageText: `Tugas: ${data.title}` });
1094
- this._sections.push(
1095
- AIRich.newLayout('Single', {
1096
- task_id: data.task_id ?? '',
1097
- title: data.title,
1098
- subtitle: data.subtitle ?? '',
1099
- status: data.status ?? 'IN_PROGRESS',
1100
- __typename: 'GenAITaskPrimitive',
1101
- })
1102
- );
1103
- // Safety net: GenAITaskPrimitive is a custom AI-only component the stock WA client
1104
- // doesn't render visibly. Append a plain text section so the task is still visible.
1105
- // Set data.textFallback = false to skip.
1106
- if (data.textFallback !== false) {
1107
- const fallbackText = data.subtitle ? `${data.title} — ${data.subtitle}` : data.title;
1108
- this._sections.push(AIRich.newLayout('Single', { text: `Tugas: ${fallbackText}`, __typename: 'FOATextPrimitive' }));
883
+ addInlineImage(imageUrl, { text = '', alignment = 'center', tapLinkUrl = '', resolveUrl = false, id, replace, insertAt } = {}) {
884
+ if (!(typeof imageUrl === 'string' || Buffer.isBuffer(imageUrl))) {
885
+ throw new TypeError('imageUrl must be string | buffer');
1109
886
  }
1110
- return this;
887
+
888
+ const url = Toolkit.resolveMedia(this.#client, imageUrl, 'image', { resolveUrl });
889
+
890
+ const ALIGNMENT_INDEX = { leading: 0, trailing: 1, center: 2 };
891
+ const ALIGNMENT_NAME = ['AI_RICH_RESPONSE_IMAGE_LAYOUT_LEADING_ALIGNED', 'AI_RICH_RESPONSE_IMAGE_LAYOUT_TRAILING_ALIGNED', 'AI_RICH_RESPONSE_IMAGE_LAYOUT_CENTER_ALIGNED'];
892
+ const alignmentIndex = ALIGNMENT_INDEX[String(alignment).toLowerCase()] ?? ALIGNMENT_INDEX.center;
893
+
894
+ const section = AIRich.newLayout('Single', {
895
+ image_url: {
896
+ image_preview_url: url,
897
+ image_high_res_url: url,
898
+ source_url: url,
899
+ },
900
+ image_text: text,
901
+ alignment: ALIGNMENT_NAME[alignmentIndex],
902
+ tap_link_url: tapLinkUrl,
903
+ __typename: 'GenAIInlineImageUXPrimitive',
904
+ });
905
+
906
+ const submessage = {
907
+ messageType: 3,
908
+ imageMetadata: {
909
+ imageUrl: { imagePreviewUrl: url, imageHighResUrl: url, sourceUrl: url },
910
+ imageText: text,
911
+ alignment: alignmentIndex,
912
+ tapLinkUrl,
913
+ },
914
+ };
915
+
916
+ return this._addContent(section, submessage, {
917
+ id,
918
+ replace,
919
+ insertAt,
920
+ });
1111
921
  }
1112
922
 
1113
923
  /**
1114
- * Add a "searching/working" progress banner (`GenAIBotProgressStatusPrimitive`) a one-shot
1115
- * status chip (unlike `addSuggest`, this isn't tappable). Distinct from `addThinkingStatus()`'s
1116
- * icon/typename.
1117
- * @param {string} title
1118
- * @param {{icon?: string, is_in_progress?: boolean}} [options]
924
+ * GenAIContentItemsUXPrimitive messageType 9 (CONTENT_ITEMS) submessage, ported from
925
+ * rich-message-utils.js toUnified() CONTENT_ITEMS case (bug 40: was returning {}).
1119
926
  */
1120
- addProgressStatus(title, { icon = 'SEARCH', is_in_progress = true, target_secondary_screen_id, target_secondary_screen_tab_id } = {}) {
927
+ addContentItems(items = [], { content_type = 1, id, replace, insertAt } = {}) {
928
+ if (!Array.isArray(items)) {
929
+ throw new TypeError('addContentItems() requires an array of items');
930
+ }
931
+
932
+ const section = AIRich.newLayout('Single', {
933
+ items,
934
+ content_type,
935
+ __typename: 'GenAIContentItemsUXPrimitive',
936
+ });
937
+
938
+ const submessage = {
939
+ messageType: 9,
940
+ contentItemsMetadata: {
941
+ itemsMetadata: items,
942
+ contentType: content_type,
943
+ },
944
+ };
945
+
946
+ return this._addContent(section, submessage, {
947
+ id,
948
+ replace,
949
+ insertAt,
950
+ });
951
+ }
952
+
953
+ /** GenAIBotProgressStatusPrimitive — one-shot "searching/working" status chip. */
954
+ addProgressStatus(title, { icon, is_in_progress = true, target_secondary_screen_id, target_secondary_screen_tab_id, id, replace, insertAt } = {}) {
1121
955
  if (typeof title !== 'string' || !title) {
1122
- throw new TypeError('addProgressStatus(title) requires a non-empty string');
956
+ throw new TypeError('addProgressStatus() requires a "title"');
1123
957
  }
1124
- this._submessages.push({ messageType: 2, messageText: title });
1125
- const primitive = {
958
+
959
+ const section = AIRich.newLayout('Single', {
1126
960
  title,
1127
961
  icon,
1128
962
  is_in_progress,
1129
- meta_search_apps: [],
963
+ target_secondary_screen_id,
964
+ target_secondary_screen_tab_id,
1130
965
  __typename: 'GenAIBotProgressStatusPrimitive',
1131
- };
1132
- // NOTE: these two fields must be OMITTED when unset, not sent as `null` —
1133
- // an explicit null here was reproducibly crashing the WA client renderer
1134
- // on group-open/media-download. Only include when the caller actually passes one.
1135
- if (target_secondary_screen_id != null) primitive.target_secondary_screen_id = target_secondary_screen_id;
1136
- if (target_secondary_screen_tab_id != null) primitive.target_secondary_screen_tab_id = target_secondary_screen_tab_id;
1137
- this._sections.push(AIRich.newLayout('Single', primitive));
1138
- return this;
966
+ });
967
+
968
+ const submessage = this.createAlert('GenAIBotProgressStatusPrimitive');
969
+
970
+ return this._addContent(section, submessage, {
971
+ id,
972
+ replace,
973
+ insertAt,
974
+ });
1139
975
  }
1140
976
 
1141
- /** Add a "thinking" status banner (`GenAIBotThinkingStatusPrimitive`). See `addProgressStatus()`. */
1142
- addThinkingStatus(title, { icon = 'THINKING', is_in_progress = true, target_secondary_screen_id, target_secondary_screen_tab_id, textFallback = true } = {}) {
977
+ /** GenAIBotThinkingStatusPrimitive one-shot "thinking" status chip. */
978
+ addThinkingStatus(title, { icon, is_in_progress = true, target_secondary_screen_id, target_secondary_screen_tab_id, textFallback = true, id, replace, insertAt } = {}) {
1143
979
  if (typeof title !== 'string' || !title) {
1144
- throw new TypeError('addThinkingStatus(title) requires a non-empty string');
980
+ throw new TypeError('addThinkingStatus() requires a "title"');
1145
981
  }
1146
- this._submessages.push({ messageType: 2, messageText: title });
1147
- const primitive = {
982
+
983
+ const section = AIRich.newLayout('Single', {
1148
984
  title,
1149
985
  icon,
1150
986
  is_in_progress,
1151
- meta_search_apps: [],
987
+ target_secondary_screen_id,
988
+ target_secondary_screen_tab_id,
1152
989
  __typename: 'GenAIBotThinkingStatusPrimitive',
1153
- };
1154
- // Same crash-avoidance rule as addProgressStatus(): omit, never null.
1155
- if (target_secondary_screen_id != null) primitive.target_secondary_screen_id = target_secondary_screen_id;
1156
- if (target_secondary_screen_tab_id != null) primitive.target_secondary_screen_tab_id = target_secondary_screen_tab_id;
1157
- this._sections.push(AIRich.newLayout('Single', primitive));
1158
- // Safety net: stock WA client doesn't render this primitive's own view (it's meant
1159
- // as a transient spinner in the official app), so the card shows blank when forwarded.
1160
- // Append a plain text section so the title is still visible. Set { textFallback: false } to skip.
1161
- if (textFallback) {
1162
- this._sections.push(AIRich.newLayout('Single', { text: title, __typename: 'FOATextPrimitive' }));
1163
- }
1164
- return this;
990
+ });
991
+
992
+ const submessage = textFallback ? this.createAlert('GenAIBotThinkingStatusPrimitive') : undefined;
993
+
994
+ return this._addContent(section, submessage, {
995
+ id,
996
+ replace,
997
+ insertAt,
998
+ });
1165
999
  }
1166
1000
 
1167
- /**
1168
- * Add a subscription-quota-limit upsell card (`GenAIMetaSubsQuotaUpsellPrimitive`).
1169
- * @param {{title: string, body?: string, body_line1?: string, body_line2?: string, buttons?: {label: string, action?: string, deeplink?: string}[]}} data
1170
- */
1171
- addQuotaUpsell(data = {}) {
1172
- if (!data?.title) {
1173
- throw new TypeError('addQuotaUpsell() requires a "title"');
1001
+ /** FOABloksPrimitive — raw Bloks payload; most experimental primitive, fields passed through as-is. */
1002
+ addBloks(data, { id, replace, insertAt } = {}) {
1003
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
1004
+ throw new TypeError('Bloks payload must be an object');
1174
1005
  }
1175
- this._submessages.push({ messageType: 2, messageText: data.title });
1176
- this._sections.push(
1177
- AIRich.newLayout('Single', {
1178
- title: data.title,
1179
- body: data.body ?? '',
1180
- body_line1: data.body_line1 ?? '',
1181
- body_line2: data.body_line2 ?? '',
1182
- buttons: (data.buttons ?? []).map((b) => ({
1183
- label: b.label ?? '',
1184
- action: b.action ?? 'OPEN_DEEPLINK',
1185
- deeplink: b.deeplink ?? '',
1186
- })),
1187
- __typename: 'GenAIMetaSubsQuotaUpsellPrimitive',
1188
- })
1189
- );
1190
- return this;
1191
- }
1192
1006
 
1193
- /**
1194
- * Add a raw Bloks payload (`FOABloksPrimitive`) — Meta's internal UI-description format.
1195
- * Escape hatch: field meaning beyond what's passed through is undocumented, so this is the
1196
- * most experimental primitive in this block; pass whatever your captured traffic shows.
1197
- * @param {{type: string, data: string, uuid?: string, initial_response?: any, versioning_id?: string}} data
1198
- */
1199
- addBloks(data = {}) {
1200
- if (!data?.type) {
1007
+ if (typeof data.type !== 'string' || !data.type) {
1201
1008
  throw new TypeError('addBloks() requires a "type"');
1202
1009
  }
1203
- this._submessages.push({ messageType: 2, messageText: 'Bloks' });
1204
- const primitive = {
1205
- type: data.type,
1206
- data: data.data ?? '{}',
1207
- uuid: data.uuid ?? '',
1208
- versioning_id: data.versioning_id ?? '',
1010
+
1011
+ const { textFallback = true, ...rest } = data;
1012
+
1013
+ const section = AIRich.newLayout('Single', {
1014
+ ...rest,
1209
1015
  __typename: 'FOABloksPrimitive',
1210
- };
1211
- // Omit initial_response entirely when unset — same null-field crash as addProgressStatus/addThinkingStatus.
1212
- if (data.initial_response != null) primitive.initial_response = data.initial_response;
1213
- this._sections.push(AIRich.newLayout('Single', primitive));
1214
- // Safety net: FOABloksPrimitive needs a real, client-registered Bloks screen to render
1215
- // anything — arbitrary/placeholder payloads show up blank. Append a plain text section
1216
- // so the card isn't empty. Set data.textFallback = false to skip.
1217
- if (data.textFallback !== false) {
1218
- this._sections.push(AIRich.newLayout('Single', { text: `Bloks: ${data.type}`, __typename: 'FOATextPrimitive' }));
1219
- }
1220
- return this;
1016
+ });
1017
+
1018
+ const submessage = textFallback ? this.createAlert('FOABloksPrimitive') : undefined;
1019
+
1020
+ return this._addContent(section, submessage, {
1021
+ id,
1022
+ replace,
1023
+ insertAt,
1024
+ });
1025
+ }
1026
+
1027
+ /**
1028
+ * GenAIImaginePrimitive with status GENERATING — pending-generation placeholder.
1029
+ * Mirrors addVideo()'s ANIMATE/GENERATING branch but with no media yet.
1030
+ */
1031
+ addGenerating({ imagine_type = 'IMAGE', estimated_completion_time, textFallback = true, id, replace, insertAt } = {}) {
1032
+ const section = AIRich.newLayout('Single', {
1033
+ imagine_type,
1034
+ status: {
1035
+ status: 'GENERATING',
1036
+ // Matches addVideo()'s convention: input is a ms-from-now duration, stored as a unix-seconds timestamp.
1037
+ estimated_completion_time: estimated_completion_time != null ? Math.floor((Date.now() + estimated_completion_time) / 1000) : undefined,
1038
+ },
1039
+ __typename: 'GenAIImaginePrimitive',
1040
+ });
1041
+
1042
+ const submessage = textFallback ? this.createAlert(`GenAIImaginePrimitive (${imagine_type === 'ANIMATE' ? 'ANIMATE' : 'IMAGE'} GENERATING)`) : undefined;
1043
+
1044
+ return this._addContent(section, submessage, {
1045
+ id,
1046
+ replace,
1047
+ insertAt,
1048
+ });
1221
1049
  }
1222
1050
 
1223
- /** Add tappable follow-up suggestion chips below the message. @param {string|string[]} suggestion */
1224
- addSuggest(suggestion, { scroll = true, layout } = {}) {
1051
+ addSuggest(suggestion, { scroll = true, layout, id, replace, insertAt } = {}) {
1225
1052
  if (!(typeof suggestion === 'string' || (Array.isArray(suggestion) && suggestion.every((v) => typeof v === 'string')))) {
1226
1053
  throw new TypeError('Suggestion must be a string or array of strings');
1227
1054
  }
@@ -1242,18 +1069,28 @@ class AIRich extends BaseBuilder {
1242
1069
 
1243
1070
  const type = layout ?? (suggest.length === 1 ? 'Single' : scroll ? 'HScroll' : 'ActionRow');
1244
1071
 
1245
- this._sections.push(AIRich.newLayout(type, type === 'Single' ? suggest[0] : suggest, { __typename: 'GenAIUnifiedResponseSection' }));
1072
+ const section = AIRich.newLayout(type, type === 'Single' ? suggest[0] : suggest, {
1073
+ __typename: 'GenAIUnifiedResponseSection',
1074
+ });
1075
+
1076
+ const submessage = this.createAlert('GenAIFollowUpSuggestionPillPrimitive');
1246
1077
 
1247
- return this;
1078
+ return this._addContent(section, submessage, {
1079
+ id,
1080
+ replace,
1081
+ insertAt,
1082
+ });
1248
1083
  }
1249
1084
 
1250
- /** @returns {Promise<Record<string, any>>} The generated AI-rich message content (without wrapping/sending it). */
1251
- async build({ forwarded = true, notification = false, includesUnifiedResponse = true, includesSubmessages = true, quoted, quotedParticipant, ...options } = {}) {
1085
+ async build(
1086
+ jid,
1087
+ { bypassDownload = true, forwarded = true, notification = false, includesUnifiedResponse = true, includesSubmessages = true, quoted, quotedParticipant, messageId, ...options } = {}
1088
+ ) {
1252
1089
  const forward = forwarded
1253
1090
  ? {
1254
1091
  forwardingScore: 1,
1255
1092
  isForwarded: true,
1256
- forwardedAiBotMessageInfo: { botJid: '0@bot' },
1093
+ forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' },
1257
1094
  forwardOrigin: 4,
1258
1095
  }
1259
1096
  : {};
@@ -1271,7 +1108,7 @@ class AIRich extends BaseBuilder {
1271
1108
  const qObj = quoted
1272
1109
  ? {
1273
1110
  stanzaId: quoted?.key?.id || quoted?.id,
1274
- participant: quotedParticipant || quoted?.key?.participant || quoted?.key?.remoteJid,
1111
+ participant: quotedParticipant || quoted?.key?.participant || quoted?.participant || quoted?.key?.remoteJid,
1275
1112
  quotedType: 0,
1276
1113
  quotedMessage: typeof quoted === 'object' && quoted !== null ? (quoted.message ?? quoted) : undefined,
1277
1114
  }
@@ -1287,140 +1124,91 @@ class AIRich extends BaseBuilder {
1287
1124
  ]
1288
1125
  : [...(await waitAllPromises(this._sections))];
1289
1126
 
1290
- // Vanz@Merge 15-08-26 --- Neither blurose nor arslan sign the bot metadata with
1291
- // verificationMetadata (proofs/certificateChain). Backported from this project's own
1292
- // rich-message-utils.js botMetadataSignature/botMetadataCertificate helpers, plus a
1293
- // botResponseId tying the signed metadata to unifiedResponse.response_id.
1294
- // Vanz@Fix 24-08-26 --- was `const responseId = crypto.randomUUID()` shared for BOTH
1295
- // unifiedResponse.response_id and botMetadata.botResponseId, generated fresh every build()
1296
- // with no override. Now each has its own id, pinned via setResponseId()/setBotResponseId()
1297
- // if the caller set one (for sendEdit()-style in-place message updates), otherwise still
1298
- // defaults to a fresh randomUUID() per build() exactly like before.
1299
- const responseId = this._responseId ?? crypto.randomUUID();
1300
- const botResponseId = this._botResponseId ?? crypto.randomUUID();
1127
+ if (this._dynamic) {
1128
+ this.refreshResponseId();
1129
+ this.refreshBotResponseId();
1130
+ }
1301
1131
 
1302
- return {
1303
- messageContextInfo: {
1304
- deviceListMetadata: {},
1305
- deviceListMetadataVersion: 2,
1306
- botMetadata: {
1307
- messageDisclaimerText: this._title,
1308
- richResponseSourcesMetadata: { sources: this._richResponseSources },
1309
- botResponseId: botResponseId,
1310
- verificationMetadata: {
1311
- proofs: [
1312
- {
1313
- certificateChain: [botMetadataCertificate(), botMetadataCertificate(892)],
1314
- version: 1,
1315
- useCase: 1,
1316
- signature: botMetadataSignature(),
1317
- },
1318
- ],
1132
+ return generateWAMessageFromContent(
1133
+ jid,
1134
+ {
1135
+ messageContextInfo: {
1136
+ deviceListMetadata: {},
1137
+ deviceListMetadataVersion: 2,
1138
+ botMetadata: {
1139
+ messageDisclaimerText: this._title,
1140
+ ...notif,
1141
+ verificationMetadata: AIRich.generateVerificationMetadata(),
1142
+ botResponseId: this._botResponseId,
1319
1143
  },
1320
- ...notif,
1321
1144
  },
1322
- },
1323
- ...this._extraPayload,
1324
- botForwardedMessage: {
1325
- message: {
1326
- richResponseMessage: {
1327
- messageType: 1,
1328
- submessages: includesSubmessages ? await waitAllPromises(this._submessages) : [],
1329
- unifiedResponse: {
1330
- data: includesUnifiedResponse ? Buffer.from(JSON.stringify({ response_id: responseId, sections })).toString('base64') : '',
1331
- },
1332
- contextInfo: {
1333
- ...forward,
1334
- ...qObj,
1335
- ...this._contextInfo,
1145
+ ...this._extraPayload,
1146
+ botForwardedMessage: {
1147
+ message: {
1148
+ richResponseMessage: {
1149
+ messageType: 1,
1150
+ submessages: includesSubmessages ? await waitAllPromises(this._submessages) : [],
1151
+ unifiedResponse: {
1152
+ data: includesUnifiedResponse ? Buffer.from(Toolkit.stringifyEscaped({ response_id: this._responseId, sections })).toString('base64') : '',
1153
+ },
1154
+ originalRecipientMetadata: {
1155
+ data: includesUnifiedResponse ? Buffer.from(Toolkit.stringifyEscaped({ response_id: this._responseId, sections })).toString('base64') : '',
1156
+ },
1157
+ contextInfo: {
1158
+ ...forward,
1159
+ ...qObj,
1160
+ ...this._contextInfo,
1161
+ },
1336
1162
  },
1337
1163
  },
1338
1164
  },
1339
1165
  },
1340
- };
1166
+ { messageId: messageId || generateMessageIDV2(), ...options }
1167
+ );
1341
1168
  }
1342
1169
 
1343
- // Vanz@Fix (bug 42 / inline image fallback) --- WA won't render AIRichResponseInlineImageMetadata
1344
- // for bot-sent messages (confirmed: even a valid WA-CDN url with mediaKey stays blank), so any
1345
- // image added via addInlineImage() is sent here as a normal imageMessage instead. Pass
1346
- // { skipImageFallback: true } to opt out and send only the (image-less-looking) rich card.
1347
- // Vanz@Fix: don't spread relayMessage-shaped `options` into sendMessage()'s options param —
1348
- // the two calls expect different option shapes, so the fallback now only forwards `quoted`
1349
- // (the one option that clearly applies to both) instead of blindly spreading everything.
1350
- /** 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. */
1351
- async send(jid, { forwarded, notification, includesUnifiedResponse, includesSubmessages, skipImageFallback = false, quoted, messageId, ...options } = {}) {
1352
- const msg = await this.build({ forwarded, notification, includesUnifiedResponse, includesSubmessages, quoted, ...options });
1353
-
1354
- if (!skipImageFallback && this._inlineImages.length) {
1355
- for (const { url, caption } of this._inlineImages) {
1356
- try {
1357
- await this.#client.sendMessage(jid, { image: { url }, caption }, quoted ? { quoted } : {});
1358
- } catch (err) {
1359
- // Vanz@Fix: don't let a fallback image failure block the actual rich card from sending
1360
- this.#client.logger?.warn?.({ err, url }, 'inline image fallback failed, continuing with rich card');
1361
- }
1362
- }
1170
+ async buildEdit(targetJid, targetId, { msg, messageId, ...options } = {}) {
1171
+ if (!msg) {
1172
+ msg = (await this.build(targetJid, options)).message;
1363
1173
  }
1364
1174
 
1365
- // Vanz@Add --- pin our own messageId (instead of letting relayMessage mint one internally)
1366
- // so we know exactly which id was sent, and stash it as _lastMessageKey. That's what lets
1367
- // sendEdit() be called with no args afterwards and still know which message to patch.
1368
- messageId = messageId || generateMessageIDV2();
1369
-
1370
- await this.#client.relayMessage(jid, msg, { messageId, ...options });
1371
-
1372
- this._lastMessageKey = { remoteJid: jid, fromMe: true, id: messageId };
1373
-
1374
- return { key: this._lastMessageKey, message: msg };
1375
- }
1376
-
1377
- /**
1378
- * Build a `protocolMessage` (type EDIT) that patches an already-sent AIRich message in place.
1379
- * @param {string} targetJid Chat the original message lives in.
1380
- * @param {string} targetId `key.id` of the original message (the id `send()`/`sendEdit()` returned).
1381
- * @param {object} [opts] Pass `{ msg }` to reuse an already-built content object instead of rebuilding via build().
1382
- */
1383
- async buildEdit(targetJid, targetId, { msg, messageId, ...options } = {}) {
1384
- const editedMessage = msg || (await this.build({ ...options }));
1175
+ const editedMessage = msg;
1385
1176
 
1386
1177
  if (!editedMessage) {
1387
- throw new Error('buildEdit: no message content to edit (build() returned nothing)');
1178
+ throw new Error('buildEdit: msg does not contain botForwardedMessage');
1388
1179
  }
1389
1180
 
1390
1181
  return generateWAMessageFromContent(
1391
1182
  targetJid,
1392
1183
  {
1393
- protocolMessage: {
1394
- key: {
1395
- remoteJid: targetJid,
1396
- fromMe: true,
1397
- id: targetId,
1184
+ botForwardedMessage: {
1185
+ message: {
1186
+ protocolMessage: {
1187
+ key: {
1188
+ remoteJid: targetJid,
1189
+ fromMe: true,
1190
+ id: targetId,
1191
+ },
1192
+ type: 14,
1193
+ editedMessage,
1194
+ },
1398
1195
  },
1399
- type: 14, // MESSAGE_EDIT
1400
- editedMessage,
1401
1196
  },
1402
1197
  },
1403
1198
  { messageId: messageId || generateMessageIDV2(), ...options }
1404
1199
  );
1405
1200
  }
1406
1201
 
1407
- /**
1408
- * Rebuild this AIRich message's current content and patch it into an already-sent message in place
1409
- * (WA edits the bubble instead of showing a new one). With no args, edits the message from the last
1410
- * send()/sendEdit() call — that's the flow `.addX(...); await rich.sendEdit();` relies on.
1411
- * @param {string} [jid] Defaults to the jid from the last send()/sendEdit().
1412
- * @param {string} [id] Defaults to the message id from the last send()/sendEdit().
1413
- */
1414
1202
  async sendEdit(jid, id, { msg, messageId, additionalNodes = [], ...options } = {}) {
1415
1203
  jid = jid ?? this._lastMessageKey?.remoteJid;
1416
1204
  id = id ?? this._lastMessageKey?.id;
1417
1205
 
1418
1206
  if (!jid) {
1419
- throw new Error('sendEdit: no jid — pass one explicitly, or call send() first');
1207
+ throw new Error('JID is required');
1420
1208
  }
1421
1209
 
1422
1210
  if (!id) {
1423
- throw new Error('sendEdit: no message id pass one explicitly, or call send() first');
1211
+ throw new Error('Message id is required');
1424
1212
  }
1425
1213
 
1426
1214
  const msgEdit = await this.buildEdit(jid, id, {
@@ -1434,13 +1222,36 @@ class AIRich extends BaseBuilder {
1434
1222
  additionalNodes,
1435
1223
  });
1436
1224
 
1437
- // Vanz@Note --- deliberately NOT overwriting _lastMessageKey with msgEdit.key here: the
1438
- // protocolMessage envelope has its own id, but the message the user actually sees (and the
1439
- // one future sendEdit() calls need to keep patching) is still `id`/`jid` above.
1440
1225
  return msgEdit;
1441
1226
  }
1442
1227
 
1443
- /** 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. */
1228
+ async send(jid, { bypassDownload = true, forwarded = true, notification = false, includesUnifiedResponse = true, includesSubmessages = true, messageId, additionalNodes = [], ...options } = {}) {
1229
+ const msg = await this.build(jid, {
1230
+ forwarded,
1231
+ notification,
1232
+ includesUnifiedResponse,
1233
+ includesSubmessages,
1234
+ messageId,
1235
+ ...options,
1236
+ });
1237
+
1238
+ await this.#client.relayMessage(msg.key.remoteJid, msg.message, {
1239
+ messageId: msg.key.id,
1240
+ additionalNodes,
1241
+ ...options,
1242
+ });
1243
+
1244
+ if (includesUnifiedResponse && bypassDownload) {
1245
+ await this.sendEdit(jid, msg.key.id, {
1246
+ msg: msg.message,
1247
+ });
1248
+ }
1249
+
1250
+ this._lastMessageKey = msg.key;
1251
+
1252
+ return msg;
1253
+ }
1254
+
1444
1255
  static tokenizer(code, lang = 'javascript') {
1445
1256
  const keywordsMap = {
1446
1257
  javascript: new Set([
@@ -2106,7 +1917,6 @@ class AIRich extends BaseBuilder {
2106
1917
  };
2107
1918
  }
2108
1919
 
2109
- /** Convert a raw `string[][]` grid into the table metadata shape addTable()/addText() produce internally. */
2110
1920
  static toTableMetadata(arr, { hyperlink = true, citation = true, latex = true } = {}) {
2111
1921
  if (!Array.isArray(arr) || !arr.every((row) => Array.isArray(row) && row.every((cell) => typeof cell === 'string'))) {
2112
1922
  throw new TypeError('Table must be a nested array of strings');
@@ -2155,120 +1965,35 @@ class AIRich extends BaseBuilder {
2155
1965
  };
2156
1966
  }
2157
1967
 
2158
- /**
2159
- * Add an "AI is generating..." placeholder card (`GenAIImaginePrimitive` with status
2160
- * GENERATING) — distinct from addImage()/addVideo() which always send status READY.
2161
- * Use this to show a pending-generation state before the real media is ready.
2162
- * @param {{ imagine_type?: 'IMAGE'|'ANIMATE', estimated_completion_time?: number }} [options]
2163
- */
2164
- addGenerating({ imagine_type = 'IMAGE', estimated_completion_time, textFallback = true } = {}) {
2165
- this._submessages.push({ messageType: 2, messageText: '[ Sedang diproses... ]' });
2166
- this._sections.push(
2167
- AIRich.newLayout('Single', {
2168
- media: { url: '', mime_type: imagine_type === 'ANIMATE' ? 'video/mp4' : 'image/png' },
2169
- imagine_type,
2170
- status: {
2171
- status: 'GENERATING',
2172
- estimated_completion_time: estimated_completion_time ?? Math.floor(Date.now() / 1000) + 30,
2173
- },
2174
- __typename: 'GenAIImaginePrimitive',
2175
- })
1968
+ static generateVerificationMetadata() {
1969
+ const signatureMaterial = Buffer.from(
1970
+ `\u004E\u0049\u0058\u0045\u004C\u002E\u004D\u0065\u0073\u0073\u0061\u0067\u0065\u0042\u0075\u0069\u006C\u0064\u0065\u0072\u0056${VERSION}\u002D\u0056\u0065\u0072\u0069\u0066\u0069\u0063\u0061\u0074\u0069\u006F\u006E\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065\u002E\u004D\u0065\u0074\u0061\u0064\u0061\u0074\u0061`
2176
1971
  );
2177
- // Vanz@Fix 23-08-26 (v4.8) --- media.url kosong + status GENERATING gak punya renderer
2178
- // visual instan di stock WA client; sebelumnya cuma diem sampe WA nge-timeout sendiri
2179
- // dan nampilin fallback bawaannya ("Saat ini, saya tidak bisa membuat gambar itu...").
2180
- // Same fix class kayak addTask/addBloks: append FOATextPrimitive biar ada fallback
2181
- // instan, gak perlu nunggu timeout WA. Set { textFallback: false } buat skip.
2182
- if (textFallback) {
2183
- this._sections.push(AIRich.newLayout('Single', { text: '[ Sedang diproses... ]', __typename: 'FOATextPrimitive' }));
2184
- }
2185
- return this;
2186
- }
2187
-
2188
- /**
2189
- * Send a support-ticket marker message (`messageContextInfo.supportPayload`) — a plain
2190
- * conversation message tagged as an AI/support-bot ticket, distinct from richResponseMessage.
2191
- * @param {import('../../WAProto/index.js').WASocket} client
2192
- * @param {string} jid
2193
- * @param {string} text
2194
- * @param {{ ticketId?: string, isAiMessage?: boolean, shouldShowSystemMessage?: boolean, version?: number }} [options]
2195
- */
2196
- static async sendSupportPayload(client, jid, text, { ticketId = crypto.randomUUID(), isAiMessage = true, shouldShowSystemMessage = true, version = 1 } = {}) {
2197
- if (!client) throw new Error('Socket is required');
2198
- if (typeof text !== 'string' || !text) throw new TypeError('sendSupportPayload(client, jid, text) requires a non-empty string text');
2199
-
2200
- const msg = {
2201
- conversation: text,
2202
- messageContextInfo: {
2203
- messageSecret: crypto.randomBytes(32),
2204
- supportPayload: JSON.stringify({
2205
- version,
2206
- is_ai_message: isAiMessage,
2207
- should_show_system_message: shouldShowSystemMessage,
2208
- ticket_id: ticketId,
2209
- }),
2210
- },
2211
- };
2212
-
2213
- return client.relayMessage(jid, msg, {
2214
- additionalNodes: [
2215
- { tag: 'bot', attrs: { biz_bot: '1' } },
2216
- { tag: 'biz', attrs: {} },
2217
- ],
2218
- });
2219
- }
2220
-
2221
- /**
2222
- * Send an image and video as one paired-media unit (image sent first, video linked to it via
2223
- * `messageAssociation`). Distinct from a plain album — the client treats them as a single group.
2224
- * @param {import('../../WAProto/index.js').WASocket} client
2225
- * @param {string} jid
2226
- * @param {{ image: string|Buffer, video: string|Buffer }} media
2227
- */
2228
- static async sendPairedMedia(client, jid, { image, video } = {}) {
2229
- if (!client) throw new Error('Socket is required');
2230
- if (!image || !video) throw new TypeError('sendPairedMedia() requires both "image" and "video"');
2231
1972
 
2232
- const imagePrepared = await prepareWAMessageMedia(
2233
- { image: typeof image === 'string' ? { url: image } : image },
2234
- { upload: client.waUploadToServer }
2235
- );
2236
- const videoPrepared = await prepareWAMessageMedia(
2237
- { video: typeof video === 'string' ? { url: video } : video },
2238
- { upload: client.waUploadToServer }
1973
+ const certificateMaterial = Buffer.from(
1974
+ `\u004E\u0049\u0058\u0045\u004C\u002E\u004D\u0065\u0073\u0073\u0061\u0067\u0065\u0042\u0075\u0069\u006C\u0064\u0065\u0072\u0056${VERSION}\u002D\u0043\u0065\u0072\u0074\u0069\u0066\u0069\u0063\u0061\u0074\u0065\u0043\u0068\u0061\u0069\u006E\u002E\u004D\u0065\u0074\u0061\u0064\u0061\u0074\u0061`
2239
1975
  );
2240
1976
 
2241
- const imageMsg = generateWAMessageFromContent(
2242
- jid,
2243
- {
2244
- imageMessage: {
2245
- ...imagePrepared.imageMessage,
2246
- contextInfo: { pairedMediaType: 5, statusSourceType: 0 },
2247
- },
2248
- },
2249
- {}
2250
- );
1977
+ const signature = Buffer.concat([signatureMaterial, crypto.randomBytes(64 - signatureMaterial.length)]).toString('base64');
2251
1978
 
2252
- await client.relayMessage(jid, imageMsg.message, { messageId: imageMsg.key.id });
1979
+ const certificateChain = [
1980
+ Buffer.concat([certificateMaterial, crypto.randomBytes(684 - certificateMaterial.length)]).toString('base64'),
2253
1981
 
2254
- await client.relayMessage(
2255
- jid,
2256
- {
2257
- videoMessage: {
2258
- ...videoPrepared.videoMessage,
2259
- contextInfo: { pairedMediaType: 6, statusSourceType: 0 },
2260
- },
2261
- messageContextInfo: {
2262
- messageAssociation: { associationType: 12, parentMessageKey: imageMsg.key },
2263
- },
2264
- },
2265
- {}
2266
- );
1982
+ Buffer.concat([certificateMaterial, crypto.randomBytes(892 - certificateMaterial.length)]).toString('base64'),
1983
+ ];
2267
1984
 
2268
- return imageMsg.key;
1985
+ return {
1986
+ proofs: [
1987
+ {
1988
+ version: 1,
1989
+ useCase: 1,
1990
+ signature,
1991
+ certificateChain,
1992
+ },
1993
+ ],
1994
+ };
2269
1995
  }
2270
1996
 
2271
- /** Build a raw submessage layout block by name — escape hatch for layouts not covered by the add*() helpers. */
2272
1997
  static newLayout(name, data, extra = {}) {
2273
1998
  return {
2274
1999
  ...extra,
@@ -2278,9 +2003,324 @@ class AIRich extends BaseBuilder {
2278
2003
  },
2279
2004
  };
2280
2005
  }
2006
+
2007
+ _makeNode(id, section, submessage) {
2008
+ return { id: id ?? null, section: section ?? null, submessage: submessage ?? null };
2009
+ }
2010
+
2011
+ _registerId(node, id) {
2012
+ if (id === undefined || id === null || id === '') return;
2013
+
2014
+ if (typeof id !== 'string') {
2015
+ throw new ContentValidationError('Item id must be a string', { id });
2016
+ }
2017
+
2018
+ if (this._idIndex.has(id)) {
2019
+ throw new DuplicateIdError(id);
2020
+ }
2021
+
2022
+ node.id = id;
2023
+ this._idIndex.set(id, node);
2024
+ }
2025
+
2026
+ _unregisterId(node) {
2027
+ if (node.id && this._idIndex.get(node.id) === node) {
2028
+ this._idIndex.delete(node.id);
2029
+ }
2030
+ }
2031
+
2032
+ hasId(id) {
2033
+ return typeof id === 'string' && this._idIndex.has(id);
2034
+ }
2035
+
2036
+ getIds() {
2037
+ return [...this._idIndex.keys()];
2038
+ }
2039
+
2040
+ peek(id) {
2041
+ const node = this._idIndex.get(id);
2042
+
2043
+ if (!node) return null;
2044
+
2045
+ return {
2046
+ id: node.id,
2047
+ section: node.section,
2048
+ submessage: node.submessage,
2049
+ };
2050
+ }
2051
+
2052
+ assignId(index, id) {
2053
+ if (!Number.isInteger(index) || index < 0 || index >= this._nodes.length) {
2054
+ throw new InvalidTargetError(`Node index ${index} is out of range (0-${this._nodes.length - 1})`, { index });
2055
+ }
2056
+
2057
+ const node = this._nodes[index];
2058
+
2059
+ if (node.id) {
2060
+ throw new AIRichError(`Node at index ${index} already has id "${node.id}"`, 'ALREADY_HAS_ID', { index, id: node.id });
2061
+ }
2062
+
2063
+ this._registerId(node, id);
2064
+
2065
+ return this;
2066
+ }
2067
+
2068
+ _getNode(id) {
2069
+ if (typeof id !== 'string' || !id) {
2070
+ throw new ContentValidationError('Item id must be a non-empty string', { id });
2071
+ }
2072
+
2073
+ const node = this._idIndex.get(id);
2074
+
2075
+ if (!node) {
2076
+ throw new ItemNotFoundError(id, this.getIds());
2077
+ }
2078
+
2079
+ return node;
2080
+ }
2081
+
2082
+ _resolveTarget(target) {
2083
+ if (Array.isArray(target)) {
2084
+ if (target.length < 1 || target.length > 2) {
2085
+ throw new ContentValidationError('Target must be id or [id, offset]', { target });
2086
+ }
2087
+
2088
+ const [id, offset = 0] = target;
2089
+
2090
+ if (typeof id !== 'string' || !id) {
2091
+ throw new ContentValidationError('Target id must be a non-empty string', { target });
2092
+ }
2093
+
2094
+ if (!Number.isInteger(offset)) {
2095
+ throw new ContentValidationError('Offset must be an integer', { target });
2096
+ }
2097
+
2098
+ return { id, offset };
2099
+ }
2100
+
2101
+ if (typeof target !== 'string' || !target) {
2102
+ throw new ContentValidationError('Target must be a non-empty id or [id, offset]', { target });
2103
+ }
2104
+
2105
+ return { id: target, offset: 0 };
2106
+ }
2107
+
2108
+ _resolveNodeIndex(target) {
2109
+ const { id, offset } = this._resolveTarget(target);
2110
+ const node = this._getNode(id);
2111
+ const baseIndex = this._nodes.indexOf(node);
2112
+
2113
+ if (baseIndex === -1) {
2114
+ throw new InvalidTargetError(`Item id "${id}" is registered but not present in the node list (internal desync)`, { id });
2115
+ }
2116
+
2117
+ const index = baseIndex + offset;
2118
+
2119
+ if (index < 0 || index >= this._nodes.length) {
2120
+ 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 });
2121
+ }
2122
+
2123
+ return { id, offset, baseIndex, index };
2124
+ }
2125
+
2126
+ _validateSections(section) {
2127
+ const items = Array.isArray(section) ? section : [section];
2128
+
2129
+ if (!items.length) {
2130
+ throw new ContentValidationError('At least one section is required');
2131
+ }
2132
+
2133
+ for (const item of items) {
2134
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
2135
+ throw new ContentValidationError('Sections must be plain objects');
2136
+ }
2137
+ }
2138
+
2139
+ return items;
2140
+ }
2141
+
2142
+ _validateSubmessages(submessage) {
2143
+ if (submessage === undefined || submessage === null) {
2144
+ return [];
2145
+ }
2146
+
2147
+ const items = Array.isArray(submessage) ? submessage : [submessage];
2148
+
2149
+ for (const item of items) {
2150
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
2151
+ throw new ContentValidationError('Submessages must be plain objects');
2152
+ }
2153
+ }
2154
+
2155
+ return items;
2156
+ }
2157
+
2158
+ _pairSubmessages(sections, submessages) {
2159
+ const n = sections.length;
2160
+ const m = submessages.length;
2161
+
2162
+ if (m === 0) return sections.map(() => null);
2163
+ if (m === 1) return sections.map((_, i) => (i === 0 ? submessages[0] : null));
2164
+ if (m === n) return submessages;
2165
+
2166
+ throw new ContentValidationError(`Cannot pair ${m} submessage(s) with ${n} section(s): expected 0, 1, or ${n}`, { sectionCount: n, submessageCount: m });
2167
+ }
2168
+
2169
+ _addContent(section, submessage, { id, replace, insertAt } = {}) {
2170
+ const hasReplace = replace !== undefined && replace !== null && replace !== '';
2171
+
2172
+ const hasInsertAt = insertAt !== undefined && insertAt !== null && insertAt !== '';
2173
+
2174
+ if (hasReplace && hasInsertAt) {
2175
+ throw new ContentValidationError('replace and insertAt cannot be used together');
2176
+ }
2177
+
2178
+ const sections = this._validateSections(section);
2179
+ const submessages = this._validateSubmessages(submessage);
2180
+
2181
+ if (!sections.length) {
2182
+ throw new ContentValidationError('At least one section is required');
2183
+ }
2184
+
2185
+ if (id !== undefined && id !== null && id !== '' && sections.length !== 1) {
2186
+ throw new ContentValidationError('One id can only be assigned to one node', {
2187
+ id,
2188
+ sectionCount: sections.length,
2189
+ });
2190
+ }
2191
+
2192
+ if (submessages.length && submessages.length !== sections.length && submessages.length !== 1) {
2193
+ throw new ContentValidationError('Section and submessage count must match');
2194
+ }
2195
+
2196
+ const pairedSubmessages = sections.map((_, index) => {
2197
+ if (!submessages.length) return undefined;
2198
+
2199
+ return submessages.length === 1 ? submessages[0] : submessages[index];
2200
+ });
2201
+
2202
+ if (id && this._idIndex.has(id) && !(hasReplace && this._resolveTarget(replace)?.id === id)) {
2203
+ throw new DuplicateIdError(id);
2204
+ }
2205
+
2206
+ const newNodes = sections.map((currentSection, index) => {
2207
+ return this._makeNode(index === 0 ? id : null, currentSection, pairedSubmessages[index]);
2208
+ });
2209
+
2210
+ if (hasReplace) {
2211
+ if (newNodes.length !== 1) {
2212
+ throw new ContentValidationError('replace only supports adding exactly one node');
2213
+ }
2214
+
2215
+ const target = this._resolveNodeIndex(replace);
2216
+
2217
+ if (!target) {
2218
+ throw new ContentValidationError('Target node could not be resolved');
2219
+ }
2220
+
2221
+ const oldNode = this._nodes[target.index];
2222
+ const newNode = newNodes[0];
2223
+
2224
+ if (!newNode.id && oldNode?.id) {
2225
+ newNode.id = oldNode.id;
2226
+ }
2227
+
2228
+ this._unregisterId(oldNode);
2229
+
2230
+ this._nodes.splice(target.index, 1, newNode);
2231
+
2232
+ if (newNode.id) {
2233
+ this._idIndex.set(newNode.id, newNode);
2234
+ }
2235
+
2236
+ return this;
2237
+ }
2238
+
2239
+ if (hasInsertAt) {
2240
+ const target = this._resolveNodeIndex(insertAt);
2241
+
2242
+ if (!target) {
2243
+ throw new ContentValidationError('Target node could not be resolved');
2244
+ }
2245
+
2246
+ const insertIndex = target.offset < 0 ? target.index : target.index + 1;
2247
+
2248
+ this._nodes.splice(insertIndex, 0, ...newNodes);
2249
+
2250
+ for (const node of newNodes) {
2251
+ if (node.id) {
2252
+ this._idIndex.set(node.id, node);
2253
+ }
2254
+ }
2255
+
2256
+ return this;
2257
+ }
2258
+
2259
+ this._nodes.push(...newNodes);
2260
+
2261
+ for (const node of newNodes) {
2262
+ if (node.id) {
2263
+ this._idIndex.set(node.id, node);
2264
+ }
2265
+ }
2266
+
2267
+ return this;
2268
+ }
2269
+
2270
+ addSection(section, options = {}) {
2271
+ return this._addContent(section, undefined, options);
2272
+ }
2273
+
2274
+ addSubmessage(submessage, options = {}) {
2275
+ const items = this._validateSubmessages(submessage);
2276
+
2277
+ if (!items.length) {
2278
+ throw new ContentValidationError('At least one submessage is required');
2279
+ }
2280
+
2281
+ return this._addContent(undefined, items, options);
2282
+ }
2283
+
2284
+ delete(target) {
2285
+ const { index } = this._resolveNodeIndex(target);
2286
+ const [oldNode] = this._nodes.splice(index, 1);
2287
+
2288
+ this._unregisterId(oldNode);
2289
+
2290
+ return this;
2291
+ }
2292
+
2293
+ get _sections() {
2294
+ return this._nodes.filter((n) => n.section !== null).map((n) => n.section);
2295
+ }
2296
+
2297
+ get _submessages() {
2298
+ return this._nodes.filter((n) => n.submessage !== null).map((n) => n.submessage);
2299
+ }
2300
+
2301
+ get sections() {
2302
+ return this._sections;
2303
+ }
2304
+
2305
+ get items() {
2306
+ return this._sections.flatMap((section) => {
2307
+ const vm = section?.view_model;
2308
+
2309
+ if (Array.isArray(vm?.primitives)) {
2310
+ return vm.primitives;
2311
+ }
2312
+
2313
+ if (vm?.primitive) {
2314
+ return [vm.primitive];
2315
+ }
2316
+
2317
+ return [];
2318
+ });
2319
+ }
2281
2320
  }
2282
2321
 
2283
- /** Thin no-op subclass of `AIRich` — kept for drop-in compatibility with code ported from ourin-baileys that references `ORich` by name. */
2322
+
2323
+ /** Compatibility alias. */
2284
2324
  class ORich extends AIRich {}
2285
2325
 
2286
2326
  export { AIRich, ORich };