@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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +629 -0
  3. package/examples/ai-rich.js +84 -0
  4. package/examples/button.js +57 -0
  5. package/examples/carousel.js +51 -0
  6. package/examples/interactive.js +102 -0
  7. package/examples/media.js +66 -0
  8. package/examples/simple-bot.js +56 -0
  9. package/package.json +86 -0
  10. package/src/builders/ai-rich.js +644 -0
  11. package/src/builders/base.js +109 -0
  12. package/src/builders/button-v2.js +159 -0
  13. package/src/builders/button.js +398 -0
  14. package/src/builders/card.js +168 -0
  15. package/src/builders/carousel.js +122 -0
  16. package/src/builders/index.d.ts +1 -0
  17. package/src/builders/index.js +13 -0
  18. package/src/client/bot.js +192 -0
  19. package/src/client/connection.js +180 -0
  20. package/src/errors.js +88 -0
  21. package/src/index.d.ts +458 -0
  22. package/src/index.js +152 -0
  23. package/src/media/fetch.js +67 -0
  24. package/src/media/image.js +86 -0
  25. package/src/media/index.d.ts +1 -0
  26. package/src/media/index.js +12 -0
  27. package/src/media/resolver.js +115 -0
  28. package/src/media/uploader.js +65 -0
  29. package/src/media/video.js +195 -0
  30. package/src/parsers/code-tokenizer-keywords.js +128 -0
  31. package/src/parsers/code-tokenizer.js +191 -0
  32. package/src/parsers/index.d.ts +1 -0
  33. package/src/parsers/index.js +11 -0
  34. package/src/parsers/inline-entity.js +231 -0
  35. package/src/parsers/table-metadata.js +69 -0
  36. package/src/proto/enums.js +170 -0
  37. package/src/proto/index.d.ts +1 -0
  38. package/src/proto/index.js +13 -0
  39. package/src/proto/layouts.js +89 -0
  40. package/src/proto/primitives.js +181 -0
  41. package/src/proto/relay-nodes.js +55 -0
  42. package/src/proto/rich-response.js +144 -0
  43. package/src/proto/updater.js +318 -0
  44. package/src/services/index.d.ts +1 -0
  45. package/src/services/index.js +10 -0
  46. package/src/services/media-service.js +184 -0
  47. package/src/services/message-service.js +288 -0
  48. package/src/services/proto-service.js +90 -0
  49. package/src/utils/id.js +42 -0
  50. package/src/utils/logger.js +65 -0
  51. package/src/utils/mime.js +104 -0
  52. package/src/utils/promise.js +52 -0
  53. package/src/utils/validator.js +129 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KyyInfinite
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,629 @@
1
+ # @kyyinfinite/lumina
2
+
3
+ > Modern WhatsApp framework built on top of [Baileys](https://github.com/WhiskeySockets/Baileys).
4
+ > Hide the protocol complexity, expose a fluent API.
5
+
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![Node: >=20](https://img.shields.io/badge/Node-%3E%3D20-brightgreen.svg)](https://nodejs.org/)
8
+ [![Module: ESM](https://img.shields.io/badge/Module-ESM-blue.svg)](https://nodejs.org/api/esm.html)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-typed-blue.svg)](#typescript)
10
+
11
+ Lumina is **not** a helper. Lumina is **not** a thin wrapper.
12
+
13
+ Lumina is an abstraction layer that absorbs **all** the complexity of WhatsApp
14
+ Web's binary protocol — `WAProto`, `generateWAMessageFromContent`,
15
+ `relayMessage`, `prepareWAMessageMedia`, Native Flow, Interactive Message,
16
+ Rich Response, ContextInfo, media upload, and proto generation — so that
17
+ developers can focus on building bots.
18
+
19
+ ```js
20
+ import { Bot } from '@kyyinfinite/lumina'
21
+
22
+ const bot = new Bot(socket)
23
+
24
+ await bot.text(jid, 'Halo')
25
+
26
+ await bot.button()
27
+ .title('Lumina')
28
+ .body('Pilih menu')
29
+ .reply('Menu', 'menu_cmd')
30
+ .url('Website', 'https://github.com/kyyinfinite/lumina')
31
+ .send(jid)
32
+
33
+ await bot.ai()
34
+ .text('Cek [dokumentasi](https://github.com/kyyinfinite/lumina)')
35
+ .code('javascript', 'console.log("hi")')
36
+ .image('https://picsum.photos/600/300')
37
+ .table([['Name', 'Score'], ['A', '90'], ['B', '85']])
38
+ .suggest(['Lanjut', 'Ulang'])
39
+ .send(jid)
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Table of Contents
45
+
46
+ - [Install](#install)
47
+ - [Features](#features)
48
+ - [Quick Start](#quick-start)
49
+ - [Basic Usage](#basic-usage)
50
+ - [Builder Usage](#builder-usage)
51
+ - [Button](#button)
52
+ - [Carousel](#carousel)
53
+ - [AI Rich](#ai-rich)
54
+ - [Media](#media)
55
+ - [Proto Update](#proto-update)
56
+ - [TypeScript](#typescript)
57
+ - [API Reference](#api-reference)
58
+ - [Migration](#migration)
59
+ - [Best Practice](#best-practice)
60
+ - [FAQ](#faq)
61
+
62
+ ---
63
+
64
+ ## Install
65
+
66
+ ```bash
67
+ # npm
68
+ npm install @kyyinfinite/lumina @whiskeysockets/baileys
69
+
70
+ # pnpm
71
+ pnpm add @kyyinfinite/lumina @whiskeysockets/baileys
72
+
73
+ # yarn
74
+ yarn add @kyyinfinite/lumina @whiskeysockets/baileys
75
+ ```
76
+
77
+ Optional peer dependencies (lazy-loaded only when needed):
78
+
79
+ ```bash
80
+ # For image resizing (PNG/JPEG/WebP):
81
+ npm install sharp
82
+
83
+ # For video thumbnail extraction:
84
+ npm install fluent-ffmpeg
85
+ # Plus the ffmpeg binary itself: https://ffmpeg.org/download.html
86
+ ```
87
+
88
+ Requirements:
89
+ - Node.js ≥ 20
90
+ - WhatsApp Web socket via `@whiskeysockets/baileys` ≥ 6.7
91
+
92
+ ---
93
+
94
+ ## Features
95
+
96
+ - **Fluent, verb-first API** — `bot.text()`, `button.title().body().reply().send()`.
97
+ - **5 interactive builders** — Button (native flow), ButtonV2 (legacy), Carousel, Card, AI Rich.
98
+ - **AI Rich Response** with 11 primitives: text, code, table, image, video, source, reels, product, post, tip, suggest.
99
+ - **Media toolkit** — fetch, resize, thumbnail, MP4 duration, ffmpeg frame extractor, WA upload, base64/URL/Buffer strategies.
100
+ - **Inline entity parser** — auto-extract `[text](url)`, `[](url)`, `[text]<url|w|h|fh|p>` to interactive citations/hyperlinks/latex.
101
+ - **Code tokenizer** — syntax highlighting for 13 languages (JS/TS/Python/Java/Go/C/C++/PHP/Rust/HTML/CSS/Bash/Markdown).
102
+ - **ProtoUpdater** — automatic WAProto maintenance (CJS→ESM transform, HistorySyncType fix, backup/restore/rollback).
103
+ - **Catalog-driven** — every magic number, `__typename`, and version string lives in one file (`proto/enums.js`).
104
+ - **TypeScript-first** — manual `.d.ts` with full typing for every public surface.
105
+ - **Tree-shakable** — subpath exports for `parsers`, `media`, `proto`, `services`, `builders`.
106
+ - **Error hierarchy** — `LuminaError` → `ValidationError | MediaError | ProtoError | ConnectionError | ProtocolError`.
107
+ - **Optional logger** — inject pino/winston subset, or use the built-in no-op default.
108
+ - **Eager async** — every `add*()` is awaited immediately; no race conditions on stored Promises.
109
+ - **Zero personal leftovers** — every hardcoded value is parameterised or removed.
110
+
111
+ ---
112
+
113
+ ## Quick Start
114
+
115
+ ```js
116
+ import makeWASocket from '@whiskeysockets/baileys'
117
+ import { useMultiFileAuthState } from '@whiskeysockets/baileys'
118
+ import { Bot } from '@kyyinfinite/lumina'
119
+
120
+ const { state, saveCreds } = await useMultiFileAuthState('./.auth')
121
+ const socket = makeWASocket.default({ auth: state, printQRInTerminal: true })
122
+
123
+ const bot = new Bot(socket, {
124
+ uploadJid: '62831@s.whatsapp.net', // optional, defaults to a permissive bot JID
125
+ })
126
+
127
+ socket.ev.on('creds.update', saveCreds)
128
+
129
+ bot.on('messages.upsert', async ({ messages }) => {
130
+ const m = messages[0]
131
+ if (!m?.message || m.key.fromMe) return
132
+
133
+ const text = m.message.conversation ?? m.message.extendedTextMessage?.text ?? ''
134
+ if (text.toLowerCase() === 'ping') {
135
+ await bot.text(m.key.remoteJid, 'pong 🏓')
136
+ }
137
+ })
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Basic Usage
143
+
144
+ ```js
145
+ const jid = '62812xxxxxxx@s.whatsapp.net'
146
+
147
+ await bot.text(jid, 'Halo!')
148
+ await bot.image(jid, 'image.jpg', 'Caption opsional')
149
+ await bot.video(jid, 'video.mp4', 'Video caption')
150
+ await bot.audio(jid, 'audio.mp3')
151
+ await bot.document(jid, 'file.pdf')
152
+ await bot.sticker(jid, 'sticker.webp')
153
+
154
+ await bot.contact(jid, [{ name: 'Andi', number: '62812xxxxxxx' }])
155
+ await bot.location(jid, -6.2, 106.8, { name: 'Jakarta', address: 'Indonesia' })
156
+ await bot.poll(jid, 'Pilih menu', ['A', 'B', 'C'], { selectableCount: 1 })
157
+
158
+ await bot.reply(jid, 'Reply text', quotedMessage)
159
+ await bot.react(jid, msgKey, '👍')
160
+ await bot.delete(jid, msgKey)
161
+ await bot.forward(jid, originalMessage)
162
+ await bot.copy(jid, originalMessage)
163
+ await bot.edit(jid, msgKey, 'edited text')
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Builder Usage
169
+
170
+ ### Button
171
+
172
+ Native-flow interactive message with chainable API.
173
+
174
+ ```js
175
+ await bot.button()
176
+ .title('Lumina')
177
+ .subtitle('Demo')
178
+ .body('Pilih menu di bawah')
179
+ .footer('Powered by Lumina')
180
+ .image('https://picsum.photos/600/300')
181
+ .reply('Menu Utama', 'cmd_menu')
182
+ .reply('Owner', 'cmd_owner')
183
+ .url('Website', 'https://github.com/kyyinfinite/lumina')
184
+ .copy('Salin Token', 'LUMINA-TOKEN')
185
+ .call('Hubungi CS', '62812xxxxxxx')
186
+ .send(jid)
187
+ ```
188
+
189
+ Selection with sections & rows (callback-based, no mutable state):
190
+
191
+ ```js
192
+ await bot.button()
193
+ .body('Pilih kategori:')
194
+ .selection('Kategori', (sel) => sel
195
+ .section('Makanan', (s) => s
196
+ .row('Nasi Goreng', 'Nasi Goreng Special', 'Rp 25.000', 'food_nasgor')
197
+ .row('Mie Ayam', 'Mie Ayam Bakso', 'Rp 20.000', 'food_mieayam'))
198
+ .section('Minuman', (s) => s
199
+ .row('Es Teh', 'Es Teh Manis', 'Rp 5.000', 'drink_esteh')))
200
+ .send(jid)
201
+ ```
202
+
203
+ ### Carousel
204
+
205
+ Multi-card carousel with per-card buttons.
206
+
207
+ ```js
208
+ const carousel = bot.carousel().body('Pilih produk:')
209
+
210
+ for (const p of products) {
211
+ const card = await carousel
212
+ .newCard()
213
+ .title(p.name)
214
+ .body(p.price)
215
+ .image(p.image)
216
+ .reply('Beli', p.cmd)
217
+ .build()
218
+ carousel.card(card)
219
+ }
220
+
221
+ await carousel.send(jid)
222
+ ```
223
+
224
+ ### AI Rich
225
+
226
+ AI Rich Response — the most powerful builder. 11 primitives, fully chainable.
227
+
228
+ ```js
229
+ await bot.ai()
230
+ .title('AI Assistant')
231
+ .footer('Generated by Lumina')
232
+ .text('Halo! Cek [dokumentasi](https://github.com/kyyinfinite/lumina).')
233
+ .code('javascript', 'console.log("hi")')
234
+ .image('https://picsum.photos/600/300')
235
+ .table([
236
+ ['Fitur', 'Status'],
237
+ ['Text', '✓'],
238
+ ['Image', '✓'],
239
+ ])
240
+ .tip('Tip: pakai fluent API untuk chaining yang bersih.')
241
+ .suggest(['Lanjut', 'Ulang'])
242
+ .source([
243
+ ['https://favicon.io/favicon.png', 'https://github.com', 'GitHub'],
244
+ ])
245
+ .product({
246
+ title: 'Sepatu Lumina',
247
+ brand: 'Lumina',
248
+ price: 'Rp 250.000',
249
+ url: 'https://example.com/1',
250
+ image_url: 'https://picsum.photos/400/400',
251
+ })
252
+ .send(jid)
253
+ ```
254
+
255
+ ---
256
+
257
+ ## Media
258
+
259
+ The `MediaService` (`bot.media`) handles every media operation:
260
+
261
+ ```js
262
+ // Fetch a URL into a Buffer (with built-in LRU cache).
263
+ const buf = await bot.media.fetch('https://example.com/image.png')
264
+
265
+ // Resize an image.
266
+ const thumb = await bot.media.resize(buf, { width: 200, height: 200, format: 'jpeg' })
267
+
268
+ // Square thumbnail helper.
269
+ const sq = await bot.media.thumbnail(buf, 300, 'png')
270
+
271
+ // MP4 duration (no native deps — pure ISO-BMFF box parser).
272
+ const seconds = bot.media.duration(videoBuf)
273
+
274
+ // Extract a video frame via ffmpeg.
275
+ const frameB64 = await bot.media.videoThumbnail(videoBuf, { time: 0, result: 'base64' })
276
+
277
+ // Upload to WhatsApp CDN.
278
+ const cdnUrl = await bot.media.upload(buf, 'image')
279
+
280
+ // Resolve any source (URL | base64 | Buffer | array) per strategy.
281
+ const resolved = await bot.media.resolve(source, {
282
+ mediaType: 'image', // 'image' | 'video' | 'audio' | 'document'
283
+ strategy: 'auto', // 'auto' | 'url-only' | 'buffer' | 'base64' | 'upload'
284
+ resize: { width: 300, height: 300, fit: 'cover' },
285
+ })
286
+
287
+ // Sniff MIME from buffer magic bytes.
288
+ const mime = bot.media.sniff(buf, 'photo.jpg')
289
+ const category = bot.media.category(mime) // 'image' | 'video' | 'audio' | 'document'
290
+ ```
291
+
292
+ ---
293
+
294
+ ## Proto Update
295
+
296
+ Lumina ships `ProtoUpdater` for refreshing `@whiskeysockets/baileys/WAProto`
297
+ with safety net:
298
+
299
+ ```js
300
+ import { ProtoUpdater } from '@kyyinfinite/lumina'
301
+
302
+ const updater = new ProtoUpdater({
303
+ protoPath: './node_modules/@whiskeysockets/baileys/WAProto',
304
+ backupDir: './.wa-proto-backups',
305
+ })
306
+
307
+ // Dry-run: see what would change without writing.
308
+ const dryRun = await updater.update({ dryRun: true })
309
+ console.log(dryRun.appliedFixes)
310
+
311
+ // Real update with auto-rollback on validation failure.
312
+ const result = await updater.update({ autoRollback: true })
313
+ if (!result.success) {
314
+ console.error('update failed, rolled back:', result.errors)
315
+ }
316
+ ```
317
+
318
+ What it does:
319
+
320
+ 1. **Backup** the current WAProto tree (timestamped, SHA-256-hashed).
321
+ 2. **Transform** every `.js` file from CommonJS to ESM:
322
+ - `require('x')` → `import ... from 'x'`
323
+ - `module.exports = x` → `export default x`
324
+ - `exports.x = ...` → `export const x = ...`
325
+ 3. **Apply known fixes**:
326
+ - `HistorySyncType.INITIAL_BOOTSTRAP = 0` (some `pbjs` runs emit `= 1`)
327
+ - `RecentMessagesWeightInheritance.CHRONOLOGICAL = 1`
328
+ 4. **Validate** by `import()`-ing every transformed file.
329
+ 5. **Rollback** automatically if validation fails (unless `autoRollback: false`).
330
+
331
+ ---
332
+
333
+ ## TypeScript
334
+
335
+ Lumina ships a **manually-authored** `.d.ts` (no auto-generated noise) with
336
+ full typing for every public surface:
337
+
338
+ ```ts
339
+ import { Bot, AIRichBuilder, ButtonBuilder, LuminaError } from '@kyyinfinite/lumina'
340
+
341
+ const bot: Bot = new Bot(socket)
342
+
343
+ const builder: AIRichBuilder = bot.ai()
344
+ await builder.text('hi').code('javascript', 'console.log(1)').send(jid)
345
+
346
+ try {
347
+ await bot.text(jid, 'hello')
348
+ } catch (err) {
349
+ if (err instanceof LuminaError) {
350
+ console.error(err.code, err.module, err.message)
351
+ }
352
+ }
353
+ ```
354
+
355
+ Type-checked features:
356
+
357
+ - Every builder method returns `this` (chainable)
358
+ - `AIRichBuilder.add*()` methods return `Promise<this>` (eager async)
359
+ - Error classes carry `code: string` and `module: string`
360
+ - Enum-like catalogs (`MessageType`, `HeaderType`, `LayoutKind`, ...) are `Readonly`
361
+ - `Bot.raw` escape hatch typed as `WASocket`
362
+
363
+ ---
364
+
365
+ ## API Reference
366
+
367
+ ### `Bot`
368
+
369
+ | Method | Returns | Description |
370
+ |---|---|---|
371
+ | `text(jid, text, opts?)` | `Promise<WAMessage>` | Send a plain text message. |
372
+ | `image(jid, source, caption?, opts?)` | `Promise<WAMessage>` | Send an image. |
373
+ | `video(jid, source, caption?, opts?)` | `Promise<WAMessage>` | Send a video. |
374
+ | `audio(jid, source, opts?)` | `Promise<WAMessage>` | Send an audio file. |
375
+ | `document(jid, source, opts?)` | `Promise<WAMessage>` | Send a document. |
376
+ | `sticker(jid, source, opts?)` | `Promise<WAMessage>` | Send a sticker. |
377
+ | `contact(jid, contacts, opts?)` | `Promise<WAMessage>` | Send one or more contacts. |
378
+ | `location(jid, lat, lng, opts?)` | `Promise<WAMessage>` | Send a location pin. |
379
+ | `poll(jid, name, options, opts?)` | `Promise<WAMessage>` | Send a poll. |
380
+ | `reply(jid, text, quoted, opts?)` | `Promise<WAMessage>` | Reply to a quoted message. |
381
+ | `react(jid, key, emoji, opts?)` | `Promise<WAMessage>` | React with an emoji. |
382
+ | `delete(jid, key)` | `Promise<void>` | Delete (revoke) a message. |
383
+ | `forward(jid, message, opts?)` | `Promise<WAMessage>` | Forward a message. |
384
+ | `copy(jid, message, opts?)` | `Promise<WAMessage>` | Copy a message's content. |
385
+ | `edit(jid, key, newText, opts?)` | `Promise<WAMessage>` | Edit a previously sent message. |
386
+ | `button()` | `ButtonBuilder` | Create a native-flow button builder. |
387
+ | `buttonV2()` | `ButtonV2Builder` | Create a legacy buttonsMessage builder. |
388
+ | `carousel()` | `CarouselBuilder` | Create a carousel builder. |
389
+ | `ai()` | `AIRichBuilder` | Create an AI Rich Response builder. |
390
+ | `on(event, handler)` | `() => void` | Subscribe to a Baileys event. Returns unsubscriber. |
391
+ | `once(event, handler)` | `() => void` | Subscribe once. |
392
+ | `off(event, handler)` | `void` | Unsubscribe. |
393
+ | `raw` | `WASocket` | Escape hatch to the underlying Baileys socket. |
394
+
395
+ ### `ButtonBuilder`
396
+
397
+ Content methods: `title`, `subtitle`, `body`, `footer`, `contextInfo`, `payload`.
398
+
399
+ Media: `media(type, source, opts?)`, `image(src, opts?)`, `video(src, opts?)`, `document(src, opts?)`.
400
+
401
+ Buttons: `reply`, `call`, `reminder`, `cancelReminder`, `address`, `url`, `copy`, `location`, `selection`, `button` (generic), `params`, `clear`.
402
+
403
+ Lifecycle: `toCard()`, `build(jid, opts?)`, `send(jid, opts?)`.
404
+
405
+ ### `CarouselBuilder`
406
+
407
+ Content: `body`, `footer`, `contextInfo`, `payload`.
408
+
409
+ Cards: `newCard()` → `CardBuilder`, `card(card | card[])`, `cards(...cards)`.
410
+
411
+ Lifecycle: `build(jid, opts?)`, `send(jid, opts?)`.
412
+
413
+ ### `AIRichBuilder`
414
+
415
+ Envelope: `title`, `footer`, `contextInfo`, `payload`, `forwarded(v?)`, `notification(v)`, `quoted(q, participant?)`, `includesUnifiedResponse(v?)`, `includesSubmessages(v?)`, `submessage(msg)`.
416
+
417
+ Primitives (all `Promise<this>`): `text`, `code`, `table`, `image`, `video`, `source`, `reels`, `product`, `post`, `tip`, `suggest`.
418
+
419
+ Lifecycle: `build(opts?)` → `Promise<object>`, `send(jid, opts?)` → `Promise<string>`.
420
+
421
+ ### `MediaService`
422
+
423
+ `fetch`, `resize`, `thumbnail`, `duration`, `videoThumbnail`, `upload`, `resolve`, `sniff`, `category`.
424
+
425
+ ### `ProtoUpdater`
426
+
427
+ `backup()`, `restore(id)`, `rollback()`, `validate()`, `update(opts?)`.
428
+
429
+ ### Errors
430
+
431
+ `LuminaError` (base), `ValidationError`, `MediaError`, `ProtoError`, `ConnectionError`, `ProtocolError`.
432
+
433
+ Each carries `code: string`, `module: string`, and the ES2022 `cause` field.
434
+
435
+ ---
436
+
437
+ ## Migration
438
+
439
+ Migrating from the legacy `_build-m.js`?
440
+
441
+ | Legacy | Lumina |
442
+ |---|---|
443
+ | `new Button(socket)` | `bot.button()` |
444
+ | `setTitle(x)` / `setBody(x)` | `.title(x)` / `.body(x)` |
445
+ | `setVideo(path)` / `setImage(path)` / `setDocument(path)` | `.video(src)` / `.image(src)` / `.document(src)` (or unified `.media(type, src)`) |
446
+ | `addReply(text, id)` | `.reply(text, id)` |
447
+ | `addUrl(text, url, webview)` | `.url(text, url, { webview_interaction })` |
448
+ | `addSelection(title)` + `makeSection()` + `makeRow()` | `.selection(title, sel => sel.section('...', s => s.row(...)))` |
449
+ | `new Carousel(socket).addCard(card)` | `bot.carousel().card(await carousel.newCard()....build())` |
450
+ | `new AIRich(socket).addText(...)` | `bot.ai().text(...)` (await) |
451
+ | `Toolkit.resize(buf, w, h)` | `bot.media.resize(buf, { width: w, height: h })` |
452
+ | `Toolkit.fetchBuffer(url, {}, { silent: true })` | `bot.media.fetch(url, { silent: true })` |
453
+ | `Toolkit.resolveMedia(client, src, 'image')` | `bot.media.resolve(src, { mediaType: 'image' })` |
454
+ | `Toolkit.getMp4Duration(buf)` | `bot.media.duration(buf)` |
455
+ | `Toolkit.getMp4Preview(buf, { time: 0 })` | `bot.media.videoThumbnail(buf, { time: 0 })` |
456
+ | `Toolkit.toUrl(client, path, 'image')` | `bot.media.upload(path, 'image')` |
457
+ | `Toolkit.extractIE(text, opts)` | `bot.util.extractInlineEntities(text, opts)` (or `extractInlineEntities` from `@kyyinfinite/lumina/parsers`) |
458
+ | `AIRich.tokenizer(code, lang)` | `bot.util.tokenizeCode(code, lang)` |
459
+ | `AIRich.toTableMetadata(arr)` | `bot.util.toTableMetadata(arr)` |
460
+ | `AIRich.newLayout('Single', data)` | `singleLayout(data)` from `@kyyinfinite/lumina/proto` |
461
+
462
+ ### Behavioral changes
463
+
464
+ - **Eager async**: every `add*()` / `text()` / `image()` on `AIRichBuilder` is `async` and resolves media immediately. You must `await` each call. The legacy "store Promise, resolve at build()" pattern is gone.
465
+ - **No `NIXEL_` prefix**: inline entity keys are now `LUMINA_HYPERLINK_0`, `LUMINA_CITATION_1`, `LUMINA_LATEX_0`. Override via `extractInlineEntities(text, { prefix: 'CUSTOM' })`.
466
+ - **No `disclaimerText: '~ Ahmad tumbuh kembang'`**: that personal leftover is gone. Set your own via `ai.notification({ disclaimerText: 'Your disclaimer' })`.
467
+ - **`GenATableUXPrimitive` typo corrected** → `GenAITableUXPrimitive` (verified against current WhatsApp Web wire format).
468
+ - **`Carousel.send()`** now properly awaits `build()` (was missing in legacy).
469
+ - **`@newsletter` jid** for media upload is no longer hardcoded. Configure via `new Bot(socket, { uploadJid })` or per-call `bot.media.upload(src, type, { jid })`.
470
+ - **`Button.paramsList` dead code** removed.
471
+
472
+ ---
473
+
474
+ ## Best Practice
475
+
476
+ ### Do
477
+
478
+ - **Await every `add*()` on AIRichBuilder.** They're async by design — chaining without `await` will silently drop primitives.
479
+ ```js
480
+ // Correct
481
+ await bot.ai().text('a').text('b').send(jid)
482
+
483
+ // Wrong — text() returns a Promise, .text('b') is undefined
484
+ bot.ai().text('a').text('b').send(jid)
485
+ ```
486
+
487
+ - **Reuse `bot` across handlers.** Constructing a `Bot` per message wastes the LRU cache and re-runs the Baileys API surface check.
488
+
489
+ - **Inject a logger in development.**
490
+ ```js
491
+ const bot = new Bot(socket, {
492
+ logger: createLogger({ level: 'debug' }),
493
+ })
494
+ ```
495
+
496
+ - **Use subpath imports for utilities** to keep your bundle small:
497
+ ```js
498
+ import { extractInlineEntities } from '@kyyinfinite/lumina/parsers'
499
+ ```
500
+
501
+ - **Catch `LuminaError`** for graceful fallback:
502
+ ```js
503
+ try {
504
+ await bot.image(jid, 'https://broken-url.example/x.png')
505
+ } catch (err) {
506
+ if (err instanceof MediaError) {
507
+ await bot.text(jid, 'Image unavailable.')
508
+ }
509
+ }
510
+ ```
511
+
512
+ ### Don't
513
+
514
+ - **Don't touch `bot.raw`** unless absolutely necessary. It bypasses every Lumina abstraction and will break on Baileys upgrades.
515
+
516
+ - **Don't call `bot.button()`/`bot.ai()`/... inside a hot loop without `await`** — each call returns a fresh builder; not awaiting it will orphan primitives.
517
+
518
+ - **Don't pass non-WA URLs to `strategy: 'url-only'`** — that strategy only accepts `*.whatsapp.net` URLs. Use `'auto'` (default) for everything else.
519
+
520
+ - **Don't rely on `GenATableUXPrimitive`** (the typo). Use the catalog: `import { TYPENAME } from '@kyyinfinite/lumina/proto'`.
521
+
522
+ ---
523
+
524
+ ## FAQ
525
+
526
+ ### Is Lumina compatible with CJS (`require`)?
527
+
528
+ No. Lumina is **ESM-only**. Node 20+ supports ESM natively. If you're stuck on CJS, use dynamic `import()`:
529
+
530
+ ```js
531
+ async function main() {
532
+ const { Bot } = await import('@kyyinfinite/lumina')
533
+ // ...
534
+ }
535
+ ```
536
+
537
+ ### Do I need `sharp` and `ffmpeg`?
538
+
539
+ Only if you use the features that depend on them:
540
+
541
+ - `sharp` — required for `bot.media.resize()`, `bot.media.thumbnail()`, and any AI Rich primitive that resizes images. Lazy-loaded.
542
+ - `fluent-ffmpeg` + ffmpeg binary — required for `bot.media.videoThumbnail()`. Lazy-loaded.
543
+
544
+ If you never call those methods, neither dependency needs to be installed.
545
+
546
+ ### Why is `AIRichBuilder.text()` async?
547
+
548
+ Because it parses inline entities synchronously (cheap) **and** the parent
549
+ class supports primitives that resolve media asynchronously (`image`,
550
+ `video`, `product`, `post`, `reels`). Forcing every primitive through an
551
+ async signature keeps the API uniform and prevents the legacy anti-pattern
552
+ of storing Promise objects inside the builder's state.
553
+
554
+ ### How do I customise the `botJid` for forwarded AI messages?
555
+
556
+ You can't — WhatsApp requires `0@bot` for the AI bot metadata to render
557
+ correctly. If WhatsApp ever changes this, update `proto/enums.BOT_JID` and
558
+ the whole framework follows.
559
+
560
+ ### How do I add a new language to the code tokenizer?
561
+
562
+ Append a `Set` of keywords to `src/parsers/code-tokenizer-keywords.js`:
563
+
564
+ ```js
565
+ export const KEYWORDS = {
566
+ // ...
567
+ kotlin: new Set(['fun', 'val', 'var', 'class', 'object', 'when', /* ... */]),
568
+ }
569
+ ```
570
+
571
+ `tokenizeCode('kotlin', code)` will pick it up automatically.
572
+
573
+ ### How do I add a new native-flow button type?
574
+
575
+ For "simple" types (params = `{ display_text, id, ...opts }`):
576
+
577
+ ```js
578
+ // In src/builders/button.js
579
+ const SIMPLE_BUTTON_TYPES = {
580
+ // ...
581
+ myCustomButton: 'cta_my_custom',
582
+ }
583
+ ButtonBuilder.prototype.myCustomButton = function (displayText, id, opts) {
584
+ return this.simpleButton('myCustomButton', displayText, id, opts)
585
+ }
586
+ ```
587
+
588
+ For complex types (custom params), use the generic `button(name, params)`.
589
+
590
+ ### How do I update WAProto after a Baileys upgrade?
591
+
592
+ ```js
593
+ import { ProtoUpdater } from '@kyyinfinite/lumina'
594
+
595
+ const updater = new ProtoUpdater()
596
+ const result = await updater.update({ autoRollback: true })
597
+ if (result.success) {
598
+ console.log('updated:', result.appliedFixes)
599
+ } else {
600
+ console.error('rolled back:', result.errors)
601
+ }
602
+ ```
603
+
604
+ ### Does Lumina work with multi-device accounts?
605
+
606
+ Yes — Lumina is purely a builder/relay layer; it works with any Baileys
607
+ socket that supports `relayMessage`, `prepareWAMessageMedia`, and the
608
+ `ev` event bus.
609
+
610
+ ### Can I use Lumina without sending — just to build message objects?
611
+
612
+ Yes. Every builder has a `build()` method that returns the proto-ready
613
+ object without relaying:
614
+
615
+ ```js
616
+ const msg = await bot.button().title('T').body('B').reply('OK').build(jid)
617
+ // msg.message is the proto object — inspect, log, or relay manually.
618
+ ```
619
+
620
+ ---
621
+
622
+ ## License
623
+
624
+ MIT © [KyyInfinite](https://github.com/kyyinfinite)
625
+
626
+ ## Repository
627
+
628
+ - Source: <https://github.com/kyyinfinite/lumina>
629
+ - Issues: <https://github.com/kyyinfinite/lumina/issues>