@kyyinfinite/lumina 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +629 -0
- package/examples/ai-rich.js +84 -0
- package/examples/button.js +57 -0
- package/examples/carousel.js +51 -0
- package/examples/interactive.js +102 -0
- package/examples/media.js +66 -0
- package/examples/simple-bot.js +56 -0
- package/package.json +86 -0
- package/src/builders/ai-rich.js +644 -0
- package/src/builders/base.js +109 -0
- package/src/builders/button-v2.js +159 -0
- package/src/builders/button.js +398 -0
- package/src/builders/card.js +168 -0
- package/src/builders/carousel.js +122 -0
- package/src/builders/index.d.ts +1 -0
- package/src/builders/index.js +13 -0
- package/src/client/bot.js +192 -0
- package/src/client/connection.js +180 -0
- package/src/errors.js +88 -0
- package/src/index.d.ts +458 -0
- package/src/index.js +152 -0
- package/src/media/fetch.js +67 -0
- package/src/media/image.js +86 -0
- package/src/media/index.d.ts +1 -0
- package/src/media/index.js +12 -0
- package/src/media/resolver.js +115 -0
- package/src/media/uploader.js +65 -0
- package/src/media/video.js +195 -0
- package/src/parsers/code-tokenizer-keywords.js +128 -0
- package/src/parsers/code-tokenizer.js +191 -0
- package/src/parsers/index.d.ts +1 -0
- package/src/parsers/index.js +11 -0
- package/src/parsers/inline-entity.js +231 -0
- package/src/parsers/table-metadata.js +69 -0
- package/src/proto/enums.js +170 -0
- package/src/proto/index.d.ts +1 -0
- package/src/proto/index.js +13 -0
- package/src/proto/layouts.js +89 -0
- package/src/proto/primitives.js +181 -0
- package/src/proto/relay-nodes.js +55 -0
- package/src/proto/rich-response.js +144 -0
- package/src/proto/updater.js +318 -0
- package/src/services/index.d.ts +1 -0
- package/src/services/index.js +10 -0
- package/src/services/media-service.js +184 -0
- package/src/services/message-service.js +288 -0
- package/src/services/proto-service.js +90 -0
- package/src/utils/id.js +42 -0
- package/src/utils/logger.js +65 -0
- package/src/utils/mime.js +104 -0
- package/src/utils/promise.js +52 -0
- package/src/utils/validator.js +129 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file builders/ai-rich.js
|
|
3
|
+
* @module lumina/builders/ai-rich
|
|
4
|
+
*
|
|
5
|
+
* AIRichBuilder — modern, fully-chainable builder for WhatsApp's AI Rich
|
|
6
|
+
* Response feature.
|
|
7
|
+
*
|
|
8
|
+
* This is the productionised successor of the legacy `AIRich` class (which
|
|
9
|
+
* was 1,270 lines, 57% of `_build-m.js`). Every legacy issue flagged in
|
|
10
|
+
* Tahap-1 analysis is fixed here:
|
|
11
|
+
*
|
|
12
|
+
* 1. Promise-in-state anti-pattern → eager await on every `add*()`.
|
|
13
|
+
* 2. 12x hardcoded `__typename` strings → sourced from `proto/primitives`.
|
|
14
|
+
* 3. 5x `messageType` magic numbers → sourced from `proto/enums.MessageType`.
|
|
15
|
+
* 4. Hardcoded `botJid: '0@bot'` → from `proto/enums.BOT_JID`.
|
|
16
|
+
* 5. Hardcoded `forwardOrigin: 4` → from `proto/enums.ForOrigin`.
|
|
17
|
+
* 6. Personal leftover `disclaimerText: '~ Ahmad tumbuh kembang'` removed.
|
|
18
|
+
* 7. Typo `GenATableUXPrimitive` corrected to `GenAITableUXPrimitive`.
|
|
19
|
+
* 8. `tokenizer` + `toTableMetadata` + `newLayout` extracted to dedicated modules.
|
|
20
|
+
* 9. 5x duplicated validation helper → `utils/validator.ensureObjectOrArray`.
|
|
21
|
+
* 10. Footer handling consolidated (was inlined in `build()`).
|
|
22
|
+
*
|
|
23
|
+
* API (verb-first fluent):
|
|
24
|
+
* bot.ai().title(t).text(...).code(...).image(...).table(...).send(jid)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { applyContentFields } from './base.js'
|
|
28
|
+
import { ensureObjectOrArray, ensureString, ensureStringArray } from '../utils/validator.js'
|
|
29
|
+
import { ValidationError } from '../errors.js'
|
|
30
|
+
|
|
31
|
+
import { extractInlineEntities } from '../parsers/inline-entity.js'
|
|
32
|
+
import { tokenizeCode } from '../parsers/code-tokenizer.js'
|
|
33
|
+
import { toTableMetadata } from '../parsers/table-metadata.js'
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
markdownTextPrimitive,
|
|
37
|
+
codePrimitive,
|
|
38
|
+
tablePrimitive,
|
|
39
|
+
searchResultPrimitive,
|
|
40
|
+
reelPrimitive,
|
|
41
|
+
imaginePrimitive,
|
|
42
|
+
productCardPrimitive,
|
|
43
|
+
postPrimitive,
|
|
44
|
+
metadataTextPrimitive,
|
|
45
|
+
followUpSuggestionPillPrimitive,
|
|
46
|
+
shapeSourceEntry,
|
|
47
|
+
shapeReelEntry,
|
|
48
|
+
} from '../proto/primitives.js'
|
|
49
|
+
import { singleLayout, hscrollLayout, actionRowLayout } from '../proto/layouts.js'
|
|
50
|
+
import { assembleRichResponse } from '../proto/rich-response.js'
|
|
51
|
+
import { MessageType, ImagineType, LayoutKind } from '../proto/enums.js'
|
|
52
|
+
|
|
53
|
+
/** @typedef {import('../client/connection.js').Connection} Connection */
|
|
54
|
+
/** @typedef {import('../services/proto-service.js').ProtoService} ProtoService */
|
|
55
|
+
/** @typedef {import('../services/media-service.js').MediaService} MediaService */
|
|
56
|
+
|
|
57
|
+
export class AIRichBuilder {
|
|
58
|
+
/**
|
|
59
|
+
* @param {Connection} conn
|
|
60
|
+
* @param {ProtoService} proto
|
|
61
|
+
* @param {MediaService} media
|
|
62
|
+
*/
|
|
63
|
+
constructor(conn, proto, media) {
|
|
64
|
+
applyContentFields(this)
|
|
65
|
+
this.#conn = conn
|
|
66
|
+
this.#proto = proto
|
|
67
|
+
this.#media = media
|
|
68
|
+
|
|
69
|
+
/** @type {Array<object>} */
|
|
70
|
+
this._submessages = []
|
|
71
|
+
/** @type {Array<object>} */
|
|
72
|
+
this._sections = []
|
|
73
|
+
/** @type {Array<object>} */
|
|
74
|
+
this._richResponseSources = []
|
|
75
|
+
|
|
76
|
+
/** @type {boolean} */
|
|
77
|
+
this._forwarded = true
|
|
78
|
+
/** @type {boolean | object} */
|
|
79
|
+
this._notification = false
|
|
80
|
+
/** @type {boolean} */
|
|
81
|
+
this._includesUnifiedResponse = true
|
|
82
|
+
/** @type {boolean} */
|
|
83
|
+
this._includesSubmessages = true
|
|
84
|
+
/** @type {object | undefined} */
|
|
85
|
+
this._quoted
|
|
86
|
+
/** @type {string | undefined} */
|
|
87
|
+
this._quotedParticipant
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @type {Connection} */ #conn
|
|
91
|
+
/** @type {ProtoService} */ #proto
|
|
92
|
+
/** @type {MediaService} */ #media
|
|
93
|
+
|
|
94
|
+
// ─── Envelope options ───────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Toggle forwarding metadata (default: on).
|
|
98
|
+
*
|
|
99
|
+
* @param {boolean} [v=true]
|
|
100
|
+
*/
|
|
101
|
+
forwarded(v = true) {
|
|
102
|
+
this._forwarded = v
|
|
103
|
+
return this
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Attach session-transparency metadata.
|
|
108
|
+
*
|
|
109
|
+
* @param {boolean | object} v `true` for defaults, or `{ disclaimerText, hcaId, sessionTransparencyType }`.
|
|
110
|
+
*/
|
|
111
|
+
notification(v) {
|
|
112
|
+
this._notification = v
|
|
113
|
+
return this
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {object} quoted
|
|
118
|
+
* @param {string} [quotedParticipant]
|
|
119
|
+
*/
|
|
120
|
+
quoted(quoted, quotedParticipant) {
|
|
121
|
+
this._quoted = quoted
|
|
122
|
+
this._quotedParticipant = quotedParticipant
|
|
123
|
+
return this
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {boolean} [v=true]
|
|
128
|
+
*/
|
|
129
|
+
includesUnifiedResponse(v = true) {
|
|
130
|
+
this._includesUnifiedResponse = v
|
|
131
|
+
return this
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {boolean} [v=true]
|
|
136
|
+
*/
|
|
137
|
+
includesSubmessages(v = true) {
|
|
138
|
+
this._includesSubmessages = v
|
|
139
|
+
return this
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Push a raw submessage object (escape hatch).
|
|
144
|
+
*
|
|
145
|
+
* @param {object | object[]} msg
|
|
146
|
+
*/
|
|
147
|
+
submessage(msg) {
|
|
148
|
+
const items = Array.isArray(msg) ? msg : [msg]
|
|
149
|
+
items.forEach((m) => ensureObjectOrArray(m, 'submessage'))
|
|
150
|
+
this._submessages.push(...items)
|
|
151
|
+
return this
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Primitives ─────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Add a markdown-text primitive. Inline entities (`[text](url)`, `[](url)`,
|
|
158
|
+
* `[text]<url|w|h|fh|p>`) are auto-extracted.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} text
|
|
161
|
+
* @param {object} [opts] { hyperlink, citation, latex } — all default true.
|
|
162
|
+
* @returns {Promise<this>}
|
|
163
|
+
*/
|
|
164
|
+
async text(text, opts = {}) {
|
|
165
|
+
ensureString(text, 'text')
|
|
166
|
+
const { hyperlink = true, citation = true, latex = true } = opts
|
|
167
|
+
|
|
168
|
+
const { text: rewritten, metadata } = extractInlineEntities(text, {
|
|
169
|
+
hyperlink,
|
|
170
|
+
citation,
|
|
171
|
+
latex,
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
this._submessages.push({
|
|
175
|
+
messageType: MessageType.TEXT,
|
|
176
|
+
messageText: rewritten,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
this._sections.push(
|
|
180
|
+
singleLayout(markdownTextPrimitive(rewritten, metadata.length ? metadata : undefined)),
|
|
181
|
+
)
|
|
182
|
+
return this
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Add a code-block primitive. Code is auto-tokenized for syntax highlighting.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} language e.g. 'javascript', 'python', 'go'.
|
|
189
|
+
* @param {string} code
|
|
190
|
+
* @returns {Promise<this>}
|
|
191
|
+
*/
|
|
192
|
+
async code(language, code) {
|
|
193
|
+
ensureString(language, 'language')
|
|
194
|
+
ensureString(code, 'code')
|
|
195
|
+
|
|
196
|
+
const { codeBlock, unifiedBlocks } = tokenizeCode(code, language)
|
|
197
|
+
|
|
198
|
+
this._submessages.push({
|
|
199
|
+
messageType: MessageType.CODE,
|
|
200
|
+
codeMetadata: {
|
|
201
|
+
codeLanguage: language,
|
|
202
|
+
codeBlocks: codeBlock,
|
|
203
|
+
},
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
this._sections.push(singleLayout(codePrimitive(language, unifiedBlocks)))
|
|
207
|
+
return this
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Add a table primitive.
|
|
212
|
+
*
|
|
213
|
+
* @param {string[][]} table First row is the header.
|
|
214
|
+
* @param {object} [opts] { title, hyperlink, citation, latex }
|
|
215
|
+
* @returns {Promise<this>}
|
|
216
|
+
*/
|
|
217
|
+
async table(table, opts = {}) {
|
|
218
|
+
if (!Array.isArray(table)) {
|
|
219
|
+
throw new ValidationError('table must be a 2-D array', { code: 'LUMINA_VALIDATION_TABLE' })
|
|
220
|
+
}
|
|
221
|
+
const { title = '', hyperlink = true, citation = true, latex = true } = opts
|
|
222
|
+
const meta = toTableMetadata(table, { title, hyperlink, citation, latex })
|
|
223
|
+
|
|
224
|
+
this._submessages.push({
|
|
225
|
+
messageType: MessageType.TABLE,
|
|
226
|
+
tableMetadata: { title: meta.title, rows: meta.rows },
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
this._sections.push(singleLayout(tablePrimitive(meta.unifiedRows)))
|
|
230
|
+
return this
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Add image primitive(s). Auto-uploads to WhatsApp.
|
|
235
|
+
*
|
|
236
|
+
* @param {string | Buffer | Array<string | Buffer>} source
|
|
237
|
+
* @param {object} [opts] { resolveUrl } — passed to MediaService.resolve.
|
|
238
|
+
* @returns {Promise<this>}
|
|
239
|
+
*/
|
|
240
|
+
async image(source, opts = {}) {
|
|
241
|
+
const { resolveUrl = false } = opts
|
|
242
|
+
const list = Array.isArray(source) ? source : [source]
|
|
243
|
+
|
|
244
|
+
/** @type {Array<{ imagePreviewUrl: string, imageHighResUrl: string, sourceUrl: string }>} */
|
|
245
|
+
const resolved = await Promise.all(
|
|
246
|
+
list.map(async (s) => {
|
|
247
|
+
const url = await this.#media.resolve(s, {
|
|
248
|
+
mediaType: 'image',
|
|
249
|
+
strategy: resolveUrl ? 'auto' : 'auto',
|
|
250
|
+
})
|
|
251
|
+
return {
|
|
252
|
+
imagePreviewUrl: url,
|
|
253
|
+
imageHighResUrl: url,
|
|
254
|
+
sourceUrl: url,
|
|
255
|
+
}
|
|
256
|
+
}),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
this._submessages.push({
|
|
260
|
+
messageType: MessageType.RICH_RESPONSE,
|
|
261
|
+
gridImageMetadata: {
|
|
262
|
+
gridImageUrl: { imagePreviewUrl: resolved[0]?.imagePreviewUrl },
|
|
263
|
+
imageUrls: resolved,
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
for (const r of resolved) {
|
|
268
|
+
this._sections.push(
|
|
269
|
+
singleLayout(
|
|
270
|
+
imaginePrimitive(
|
|
271
|
+
{ url: r.imagePreviewUrl, mime_type: 'image/png' },
|
|
272
|
+
ImagineType.IMAGE,
|
|
273
|
+
),
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
return this
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Add video primitive(s). Auto-fills file_length, duration, and thumbnail
|
|
282
|
+
* by downloading & inspecting the buffer (configurable via `autoFill`).
|
|
283
|
+
*
|
|
284
|
+
* @param {string | Buffer | object | Array} source
|
|
285
|
+
* @param {object} [opts] { autoFill = true }
|
|
286
|
+
* @returns {Promise<this>}
|
|
287
|
+
*/
|
|
288
|
+
async video(source, opts = {}) {
|
|
289
|
+
const { autoFill = true } = opts
|
|
290
|
+
const isObjectVideo = (v) => v && typeof v === 'object' && v.url
|
|
291
|
+
const items = Array.isArray(source) ? source : [source]
|
|
292
|
+
|
|
293
|
+
// Placeholder submessage — matches the legacy '[ CANNOT_LOAD_VIDEO ]' pattern.
|
|
294
|
+
this._submessages.push({
|
|
295
|
+
messageType: MessageType.TEXT,
|
|
296
|
+
messageText: '[ CANNOT_LOAD_VIDEO ]',
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
await Promise.all(
|
|
300
|
+
items.map(async (item) => {
|
|
301
|
+
const isObj = isObjectVideo(item)
|
|
302
|
+
const url = await this.#media.resolve(
|
|
303
|
+
isObj ? item.url : item,
|
|
304
|
+
{ mediaType: 'video', strategy: 'auto' },
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
let fileLength = isObj && item.file_length != null ? item.file_length : 0
|
|
308
|
+
let duration = isObj && item.duration != null ? item.duration : 0
|
|
309
|
+
let thumbnailB64 = null
|
|
310
|
+
|
|
311
|
+
if (autoFill) {
|
|
312
|
+
try {
|
|
313
|
+
const buf = typeof url === 'string' ? await this.#media.fetch(url) : url
|
|
314
|
+
if (buf?.length) {
|
|
315
|
+
fileLength = buf.length
|
|
316
|
+
duration = this.#media.duration(buf)
|
|
317
|
+
thumbnailB64 = await this.#media.videoThumbnail(buf, {
|
|
318
|
+
time: 0,
|
|
319
|
+
result: 'base64',
|
|
320
|
+
resizeOutput: true,
|
|
321
|
+
width: 300,
|
|
322
|
+
height: 300,
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
// Swallow autofill failures — the video URL alone is still useful.
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const mimeType = isObj ? item.mime_type ?? 'video/mp4' : 'video/mp4'
|
|
331
|
+
|
|
332
|
+
this._sections.push(
|
|
333
|
+
singleLayout(
|
|
334
|
+
imaginePrimitive(
|
|
335
|
+
{ url, mime_type: mimeType, file_length: fileLength, duration },
|
|
336
|
+
ImagineType.ANIMATE,
|
|
337
|
+
'READY',
|
|
338
|
+
thumbnailB64 || undefined,
|
|
339
|
+
),
|
|
340
|
+
),
|
|
341
|
+
)
|
|
342
|
+
}),
|
|
343
|
+
)
|
|
344
|
+
return this
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Add source(s) — citation footnotes with favicon, URL, and display text.
|
|
349
|
+
*
|
|
350
|
+
* @param {Array<string> | Array<Array<string>>} sources
|
|
351
|
+
* Each entry is either `[iconUrl, url, text]` or (when only strings)
|
|
352
|
+
* an array of URLs that get empty favicons.
|
|
353
|
+
* @returns {Promise<this>}
|
|
354
|
+
*/
|
|
355
|
+
async source(sources = []) {
|
|
356
|
+
if (!Array.isArray(sources)) {
|
|
357
|
+
throw new ValidationError('sources must be an array', { code: 'LUMINA_VALIDATION_SOURCE' })
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const normalised =
|
|
361
|
+
sources.length && typeof sources[0] === 'string'
|
|
362
|
+
? [sources]
|
|
363
|
+
: sources
|
|
364
|
+
|
|
365
|
+
const entries = await Promise.all(
|
|
366
|
+
normalised.map(async (entry) => {
|
|
367
|
+
const [iconUrl, url, text] = entry
|
|
368
|
+
const resolvedIcon = iconUrl
|
|
369
|
+
? await this.#media.resolve(iconUrl, { mediaType: 'image', strategy: 'auto' })
|
|
370
|
+
: ''
|
|
371
|
+
return shapeSourceEntry({ iconUrl: resolvedIcon, url, text })
|
|
372
|
+
}),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
this._sections.push(singleLayout(searchResultPrimitive(entries)))
|
|
376
|
+
return this
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Add reel(s) — short-video cards with avatar, thumbnail, like/view counts.
|
|
381
|
+
*
|
|
382
|
+
* @param {object | object[]} items
|
|
383
|
+
* @returns {Promise<this>}
|
|
384
|
+
*/
|
|
385
|
+
async reels(items) {
|
|
386
|
+
ensureObjectOrArray(items, 'reels')
|
|
387
|
+
const list = Array.isArray(items) ? items : [items]
|
|
388
|
+
|
|
389
|
+
const resolved = await Promise.all(
|
|
390
|
+
list.map(async (item) => {
|
|
391
|
+
const [avatar, thumb] = await Promise.all([
|
|
392
|
+
item.profileIconUrl ?? item.profile_url ?? item.profile
|
|
393
|
+
? this.#media.resolve(item.profileIconUrl ?? item.profile_url ?? item.profile, {
|
|
394
|
+
mediaType: 'image',
|
|
395
|
+
strategy: 'auto',
|
|
396
|
+
})
|
|
397
|
+
: Promise.resolve(''),
|
|
398
|
+
item.thumbnailUrl ?? item.thumbnail
|
|
399
|
+
? this.#media.resolve(item.thumbnailUrl ?? item.thumbnail, {
|
|
400
|
+
mediaType: 'image',
|
|
401
|
+
strategy: 'auto',
|
|
402
|
+
})
|
|
403
|
+
: Promise.resolve(''),
|
|
404
|
+
])
|
|
405
|
+
return { ...item, avatar, thumbnail: thumb }
|
|
406
|
+
}),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
this._submessages.push({
|
|
410
|
+
messageType: MessageType.REELS,
|
|
411
|
+
contentItemsMetadata: {
|
|
412
|
+
contentType: 1,
|
|
413
|
+
itemsMetadata: resolved.map((item) => ({
|
|
414
|
+
reelItem: {
|
|
415
|
+
title: item.username ?? '',
|
|
416
|
+
profileIconUrl: item.avatar,
|
|
417
|
+
thumbnailUrl: item.thumbnail,
|
|
418
|
+
videoUrl: item.videoUrl ?? item.url ?? '',
|
|
419
|
+
},
|
|
420
|
+
})),
|
|
421
|
+
},
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
resolved.forEach((item, idx) => {
|
|
425
|
+
this._richResponseSources.push({
|
|
426
|
+
provider: 'LUMINA',
|
|
427
|
+
thumbnailCDNURL: item.thumbnail,
|
|
428
|
+
sourceProviderURL: item.videoUrl ?? item.url ?? '',
|
|
429
|
+
sourceQuery: '',
|
|
430
|
+
faviconCDNURL: item.avatar,
|
|
431
|
+
citationNumber: idx + 1,
|
|
432
|
+
sourceTitle: item.username ?? '',
|
|
433
|
+
})
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
this._sections.push(
|
|
437
|
+
hscrollLayout(resolved.map((item) => reelPrimitive(shapeReelEntry(item)))),
|
|
438
|
+
)
|
|
439
|
+
return this
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Add product card(s).
|
|
444
|
+
*
|
|
445
|
+
* @param {object | object[]} item
|
|
446
|
+
* @returns {Promise<this>}
|
|
447
|
+
*/
|
|
448
|
+
async product(item) {
|
|
449
|
+
ensureObjectOrArray(item, 'product')
|
|
450
|
+
const list = Array.isArray(item) ? item : [item]
|
|
451
|
+
|
|
452
|
+
this._submessages.push({
|
|
453
|
+
messageType: MessageType.TEXT,
|
|
454
|
+
messageText: '[ CANNOT_LOAD_PRODUCT ]',
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
const products = await Promise.all(
|
|
458
|
+
list.map(async (p) => {
|
|
459
|
+
const [imageUrl, iconUrl] = await Promise.all([
|
|
460
|
+
p.image_url ?? p.image
|
|
461
|
+
? this.#media.resolve(p.image_url ?? p.image, { mediaType: 'image', strategy: 'auto' })
|
|
462
|
+
: Promise.resolve(''),
|
|
463
|
+
p.icon_url ?? p.icon
|
|
464
|
+
? this.#media.resolve(p.icon_url ?? p.icon, { mediaType: 'image', strategy: 'auto' })
|
|
465
|
+
: Promise.resolve(''),
|
|
466
|
+
])
|
|
467
|
+
return {
|
|
468
|
+
title: p.title,
|
|
469
|
+
brand: p.brand,
|
|
470
|
+
price: p.price,
|
|
471
|
+
sale_price: p.sale_price,
|
|
472
|
+
product_url: p.product_url ?? p.url,
|
|
473
|
+
image: { url: imageUrl },
|
|
474
|
+
additional_images: [{ url: iconUrl }],
|
|
475
|
+
}
|
|
476
|
+
}),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
const primitives = products.map((p) => productCardPrimitive(p))
|
|
480
|
+
this._sections.push(
|
|
481
|
+
list.length === 1 ? singleLayout(primitives[0]) : hscrollLayout(primitives),
|
|
482
|
+
)
|
|
483
|
+
return this
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Add post(s).
|
|
488
|
+
*
|
|
489
|
+
* @param {object | object[]} item
|
|
490
|
+
* @returns {Promise<this>}
|
|
491
|
+
*/
|
|
492
|
+
async post(item) {
|
|
493
|
+
ensureObjectOrArray(item, 'post')
|
|
494
|
+
const list = Array.isArray(item) ? item : [item]
|
|
495
|
+
|
|
496
|
+
this._submessages.push({
|
|
497
|
+
messageType: MessageType.TEXT,
|
|
498
|
+
messageText: '[ CANNOT_LOAD_POST ]',
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
const isCarousel = list.length > 1
|
|
502
|
+
|
|
503
|
+
const primitives = await Promise.all(
|
|
504
|
+
list.map(async (p) => {
|
|
505
|
+
const [profileUrl, thumbUrl, footerIcon] = await Promise.all([
|
|
506
|
+
p.profile_picture_url ?? p.profile_url ?? p.profile
|
|
507
|
+
? this.#media.resolve(p.profile_picture_url ?? p.profile_url ?? p.profile, {
|
|
508
|
+
mediaType: 'image',
|
|
509
|
+
strategy: 'auto',
|
|
510
|
+
})
|
|
511
|
+
: Promise.resolve(''),
|
|
512
|
+
p.thumbnail_url ?? p.thumbnail
|
|
513
|
+
? this.#media.resolve(p.thumbnail_url ?? p.thumbnail, {
|
|
514
|
+
mediaType: 'image',
|
|
515
|
+
strategy: 'auto',
|
|
516
|
+
})
|
|
517
|
+
: Promise.resolve(''),
|
|
518
|
+
p.footer_icon ?? p.icon
|
|
519
|
+
? this.#media.resolve(p.footer_icon ?? p.icon, {
|
|
520
|
+
mediaType: 'image',
|
|
521
|
+
strategy: 'auto',
|
|
522
|
+
})
|
|
523
|
+
: Promise.resolve(''),
|
|
524
|
+
])
|
|
525
|
+
|
|
526
|
+
return postPrimitive(
|
|
527
|
+
{
|
|
528
|
+
title: p.title ?? '',
|
|
529
|
+
subtitle: p.subtitle ?? '',
|
|
530
|
+
username: p.username ?? '',
|
|
531
|
+
profile_picture_url: profileUrl,
|
|
532
|
+
is_verified: !!(p.is_verified ?? p.verified),
|
|
533
|
+
thumbnail_url: thumbUrl,
|
|
534
|
+
post_caption: p.post_caption ?? p.caption ?? '',
|
|
535
|
+
likes_count: p.likes_count ?? p.like ?? 0,
|
|
536
|
+
comments_count: p.comments_count ?? p.comment ?? 0,
|
|
537
|
+
shares_count: p.shares_count ?? p.share ?? 0,
|
|
538
|
+
post_url: p.post_url ?? p.url ?? '',
|
|
539
|
+
post_deeplink: p.post_deeplink ?? p.deeplink ?? '',
|
|
540
|
+
source_app: p.source_app ?? p.source ?? 'INSTAGRAM',
|
|
541
|
+
footer_label: p.footer_label ?? p.footer ?? '',
|
|
542
|
+
footer_icon: footerIcon,
|
|
543
|
+
orientation: p.orientation ?? 'LANDSCAPE',
|
|
544
|
+
post_type: p.post_type ?? 'VIDEO',
|
|
545
|
+
},
|
|
546
|
+
isCarousel,
|
|
547
|
+
)
|
|
548
|
+
}),
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
this._sections.push(hscrollLayout(primitives))
|
|
552
|
+
return this
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Add a tip / metadata-text primitive (small footer-style hint).
|
|
557
|
+
*
|
|
558
|
+
* @param {string} text
|
|
559
|
+
* @returns {Promise<this>}
|
|
560
|
+
*/
|
|
561
|
+
async tip(text) {
|
|
562
|
+
ensureString(text, 'tip text')
|
|
563
|
+
this._submessages.push({ messageType: MessageType.TEXT, messageText: text })
|
|
564
|
+
this._sections.push(singleLayout(metadataTextPrimitive(text)))
|
|
565
|
+
return this
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Add follow-up suggestion pill(s).
|
|
570
|
+
*
|
|
571
|
+
* @param {string | string[]} suggestion
|
|
572
|
+
* @param {object} [opts] { scroll = true, layout?: 'Single'|'HScroll'|'ActionRow' }
|
|
573
|
+
* @returns {Promise<this>}
|
|
574
|
+
*/
|
|
575
|
+
async suggest(suggestion, opts = {}) {
|
|
576
|
+
const { scroll = true, layout } = opts
|
|
577
|
+
|
|
578
|
+
if (typeof suggestion === 'string') {
|
|
579
|
+
suggestion = [suggestion]
|
|
580
|
+
} else {
|
|
581
|
+
ensureStringArray(suggestion, 'suggestion')
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const pills = suggestion.map((text) => followUpSuggestionPillPrimitive(text))
|
|
585
|
+
|
|
586
|
+
let kind
|
|
587
|
+
if (layout) {
|
|
588
|
+
kind = layout
|
|
589
|
+
} else if (pills.length === 1) {
|
|
590
|
+
kind = LayoutKind.SINGLE
|
|
591
|
+
} else {
|
|
592
|
+
kind = scroll ? LayoutKind.HSCROLL : LayoutKind.ACTION_ROW
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (kind === LayoutKind.SINGLE) {
|
|
596
|
+
this._sections.push(singleLayout(pills[0], { __typename: 'GenAIUnifiedResponseSection' }))
|
|
597
|
+
} else if (kind === LayoutKind.HSCROLL) {
|
|
598
|
+
this._sections.push(hscrollLayout(pills, { __typename: 'GenAIUnifiedResponseSection' }))
|
|
599
|
+
} else {
|
|
600
|
+
this._sections.push(actionRowLayout(pills, { __typename: 'GenAIUnifiedResponseSection' }))
|
|
601
|
+
}
|
|
602
|
+
return this
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ─── Lifecycle ──────────────────────────────────────────────────────
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Assemble the `botForwardedMessage` envelope (without relaying).
|
|
609
|
+
*
|
|
610
|
+
* @param {object} [opts] Overrides forwarded / notification / etc.
|
|
611
|
+
* @returns {Promise<object>}
|
|
612
|
+
*/
|
|
613
|
+
async build(opts = {}) {
|
|
614
|
+
return assembleRichResponse({
|
|
615
|
+
title: this._title,
|
|
616
|
+
sections: this._sections,
|
|
617
|
+
submessages: this._submessages,
|
|
618
|
+
richResponseSources: this._richResponseSources,
|
|
619
|
+
contextInfo: this._contextInfo,
|
|
620
|
+
footer: this._footer,
|
|
621
|
+
forwarded: opts.forwarded ?? this._forwarded,
|
|
622
|
+
notification: opts.notification ?? this._notification,
|
|
623
|
+
includesUnifiedResponse: opts.includesUnifiedResponse ?? this._includesUnifiedResponse,
|
|
624
|
+
includesSubmessages: opts.includesSubmessages ?? this._includesSubmessages,
|
|
625
|
+
quoted: opts.quoted ?? this._quoted,
|
|
626
|
+
quotedParticipant: opts.quotedParticipant ?? this._quotedParticipant,
|
|
627
|
+
extraPayload: this._extraPayload,
|
|
628
|
+
})
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Build + relay via the Baileys socket.
|
|
633
|
+
*
|
|
634
|
+
* @param {string} jid
|
|
635
|
+
* @param {object} [opts]
|
|
636
|
+
* @returns {Promise<string>} Message ID.
|
|
637
|
+
*/
|
|
638
|
+
async send(jid, opts = {}) {
|
|
639
|
+
const envelope = await this.build(opts)
|
|
640
|
+
return this.#conn.relayMessage(jid, envelope, opts)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export default AIRichBuilder
|