@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
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
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
|
+
[](https://nodejs.org/api/esm.html)
|
|
9
|
+
[](#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>
|