@kyyinfinite/lumina 1.0.0 → 1.0.2

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 (40) hide show
  1. package/package.json +1 -1
  2. package/src/builders/ai-rich.js +0 -165
  3. package/src/builders/base.js +4 -51
  4. package/src/builders/button-v2.js +13 -78
  5. package/src/builders/button.js +20 -234
  6. package/src/builders/card.js +9 -76
  7. package/src/builders/carousel.js +4 -61
  8. package/src/builders/index.js +1 -7
  9. package/src/builders/sticker.js +102 -0
  10. package/src/client/bot.js +28 -153
  11. package/src/client/connection.js +4 -111
  12. package/src/errors.js +0 -37
  13. package/src/index.d.ts +1 -28
  14. package/src/index.js +23 -121
  15. package/src/media/fetch.js +2 -33
  16. package/src/media/image.js +1 -41
  17. package/src/media/resolver.js +3 -55
  18. package/src/media/sticker.js +124 -0
  19. package/src/media/uploader.js +0 -30
  20. package/src/media/video.js +1 -39
  21. package/src/parsers/code-tokenizer-keywords.js +0 -12
  22. package/src/parsers/code-tokenizer.js +0 -42
  23. package/src/parsers/index.js +0 -7
  24. package/src/parsers/inline-entity.js +8 -117
  25. package/src/parsers/table-metadata.js +1 -35
  26. package/src/proto/enums.js +9 -65
  27. package/src/proto/layouts.js +3 -64
  28. package/src/proto/primitives.js +4 -91
  29. package/src/proto/relay-nodes.js +1 -32
  30. package/src/proto/rich-response.js +6 -57
  31. package/src/proto/updater.js +0 -85
  32. package/src/services/index.js +0 -7
  33. package/src/services/media-service.js +1 -102
  34. package/src/services/message-service.js +16 -158
  35. package/src/services/proto-service.js +3 -57
  36. package/src/utils/id.js +0 -25
  37. package/src/utils/logger.js +2 -39
  38. package/src/utils/mime.js +17 -73
  39. package/src/utils/promise.js +0 -26
  40. package/src/utils/validator.js +6 -71
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyyinfinite/lumina",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Lumina — Modern WhatsApp framework built on top of Baileys. Hide the protocol complexity, expose a fluent API.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -1,28 +1,3 @@
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
1
 
27
2
  import { applyContentFields } from './base.js'
28
3
  import { ensureObjectOrArray, ensureString, ensureStringArray } from '../utils/validator.js'
@@ -50,100 +25,54 @@ import { singleLayout, hscrollLayout, actionRowLayout } from '../proto/layouts.j
50
25
  import { assembleRichResponse } from '../proto/rich-response.js'
51
26
  import { MessageType, ImagineType, LayoutKind } from '../proto/enums.js'
52
27
 
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
28
 
57
29
  export class AIRichBuilder {
58
- /**
59
- * @param {Connection} conn
60
- * @param {ProtoService} proto
61
- * @param {MediaService} media
62
- */
63
30
  constructor(conn, proto, media) {
64
31
  applyContentFields(this)
65
32
  this.#conn = conn
66
33
  this.#proto = proto
67
34
  this.#media = media
68
35
 
69
- /** @type {Array<object>} */
70
36
  this._submessages = []
71
- /** @type {Array<object>} */
72
37
  this._sections = []
73
- /** @type {Array<object>} */
74
38
  this._richResponseSources = []
75
39
 
76
- /** @type {boolean} */
77
40
  this._forwarded = true
78
- /** @type {boolean | object} */
79
41
  this._notification = false
80
- /** @type {boolean} */
81
42
  this._includesUnifiedResponse = true
82
- /** @type {boolean} */
83
43
  this._includesSubmessages = true
84
- /** @type {object | undefined} */
85
44
  this._quoted
86
- /** @type {string | undefined} */
87
45
  this._quotedParticipant
88
46
  }
89
47
 
90
- /** @type {Connection} */ #conn
91
- /** @type {ProtoService} */ #proto
92
- /** @type {MediaService} */ #media
93
48
 
94
- // ─── Envelope options ───────────────────────────────────────────────
95
49
 
96
- /**
97
- * Toggle forwarding metadata (default: on).
98
- *
99
- * @param {boolean} [v=true]
100
- */
101
50
  forwarded(v = true) {
102
51
  this._forwarded = v
103
52
  return this
104
53
  }
105
54
 
106
- /**
107
- * Attach session-transparency metadata.
108
- *
109
- * @param {boolean | object} v `true` for defaults, or `{ disclaimerText, hcaId, sessionTransparencyType }`.
110
- */
111
55
  notification(v) {
112
56
  this._notification = v
113
57
  return this
114
58
  }
115
59
 
116
- /**
117
- * @param {object} quoted
118
- * @param {string} [quotedParticipant]
119
- */
120
60
  quoted(quoted, quotedParticipant) {
121
61
  this._quoted = quoted
122
62
  this._quotedParticipant = quotedParticipant
123
63
  return this
124
64
  }
125
65
 
126
- /**
127
- * @param {boolean} [v=true]
128
- */
129
66
  includesUnifiedResponse(v = true) {
130
67
  this._includesUnifiedResponse = v
131
68
  return this
132
69
  }
133
70
 
134
- /**
135
- * @param {boolean} [v=true]
136
- */
137
71
  includesSubmessages(v = true) {
138
72
  this._includesSubmessages = v
139
73
  return this
140
74
  }
141
75
 
142
- /**
143
- * Push a raw submessage object (escape hatch).
144
- *
145
- * @param {object | object[]} msg
146
- */
147
76
  submessage(msg) {
148
77
  const items = Array.isArray(msg) ? msg : [msg]
149
78
  items.forEach((m) => ensureObjectOrArray(m, 'submessage'))
@@ -151,16 +80,7 @@ export class AIRichBuilder {
151
80
  return this
152
81
  }
153
82
 
154
- // ─── Primitives ─────────────────────────────────────────────────────
155
83
 
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
84
  async text(text, opts = {}) {
165
85
  ensureString(text, 'text')
166
86
  const { hyperlink = true, citation = true, latex = true } = opts
@@ -182,13 +102,6 @@ export class AIRichBuilder {
182
102
  return this
183
103
  }
184
104
 
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
105
  async code(language, code) {
193
106
  ensureString(language, 'language')
194
107
  ensureString(code, 'code')
@@ -207,13 +120,6 @@ export class AIRichBuilder {
207
120
  return this
208
121
  }
209
122
 
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
123
  async table(table, opts = {}) {
218
124
  if (!Array.isArray(table)) {
219
125
  throw new ValidationError('table must be a 2-D array', { code: 'LUMINA_VALIDATION_TABLE' })
@@ -230,18 +136,10 @@ export class AIRichBuilder {
230
136
  return this
231
137
  }
232
138
 
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
139
  async image(source, opts = {}) {
241
140
  const { resolveUrl = false } = opts
242
141
  const list = Array.isArray(source) ? source : [source]
243
142
 
244
- /** @type {Array<{ imagePreviewUrl: string, imageHighResUrl: string, sourceUrl: string }>} */
245
143
  const resolved = await Promise.all(
246
144
  list.map(async (s) => {
247
145
  const url = await this.#media.resolve(s, {
@@ -277,20 +175,11 @@ export class AIRichBuilder {
277
175
  return this
278
176
  }
279
177
 
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
178
  async video(source, opts = {}) {
289
179
  const { autoFill = true } = opts
290
180
  const isObjectVideo = (v) => v && typeof v === 'object' && v.url
291
181
  const items = Array.isArray(source) ? source : [source]
292
182
 
293
- // Placeholder submessage — matches the legacy '[ CANNOT_LOAD_VIDEO ]' pattern.
294
183
  this._submessages.push({
295
184
  messageType: MessageType.TEXT,
296
185
  messageText: '[ CANNOT_LOAD_VIDEO ]',
@@ -323,7 +212,6 @@ export class AIRichBuilder {
323
212
  })
324
213
  }
325
214
  } catch {
326
- // Swallow autofill failures — the video URL alone is still useful.
327
215
  }
328
216
  }
329
217
 
@@ -344,14 +232,6 @@ export class AIRichBuilder {
344
232
  return this
345
233
  }
346
234
 
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
235
  async source(sources = []) {
356
236
  if (!Array.isArray(sources)) {
357
237
  throw new ValidationError('sources must be an array', { code: 'LUMINA_VALIDATION_SOURCE' })
@@ -376,12 +256,6 @@ export class AIRichBuilder {
376
256
  return this
377
257
  }
378
258
 
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
259
  async reels(items) {
386
260
  ensureObjectOrArray(items, 'reels')
387
261
  const list = Array.isArray(items) ? items : [items]
@@ -439,12 +313,6 @@ export class AIRichBuilder {
439
313
  return this
440
314
  }
441
315
 
442
- /**
443
- * Add product card(s).
444
- *
445
- * @param {object | object[]} item
446
- * @returns {Promise<this>}
447
- */
448
316
  async product(item) {
449
317
  ensureObjectOrArray(item, 'product')
450
318
  const list = Array.isArray(item) ? item : [item]
@@ -483,12 +351,6 @@ export class AIRichBuilder {
483
351
  return this
484
352
  }
485
353
 
486
- /**
487
- * Add post(s).
488
- *
489
- * @param {object | object[]} item
490
- * @returns {Promise<this>}
491
- */
492
354
  async post(item) {
493
355
  ensureObjectOrArray(item, 'post')
494
356
  const list = Array.isArray(item) ? item : [item]
@@ -552,12 +414,6 @@ export class AIRichBuilder {
552
414
  return this
553
415
  }
554
416
 
555
- /**
556
- * Add a tip / metadata-text primitive (small footer-style hint).
557
- *
558
- * @param {string} text
559
- * @returns {Promise<this>}
560
- */
561
417
  async tip(text) {
562
418
  ensureString(text, 'tip text')
563
419
  this._submessages.push({ messageType: MessageType.TEXT, messageText: text })
@@ -565,13 +421,6 @@ export class AIRichBuilder {
565
421
  return this
566
422
  }
567
423
 
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
424
  async suggest(suggestion, opts = {}) {
576
425
  const { scroll = true, layout } = opts
577
426
 
@@ -602,14 +451,7 @@ export class AIRichBuilder {
602
451
  return this
603
452
  }
604
453
 
605
- // ─── Lifecycle ──────────────────────────────────────────────────────
606
454
 
607
- /**
608
- * Assemble the `botForwardedMessage` envelope (without relaying).
609
- *
610
- * @param {object} [opts] Overrides forwarded / notification / etc.
611
- * @returns {Promise<object>}
612
- */
613
455
  async build(opts = {}) {
614
456
  return assembleRichResponse({
615
457
  title: this._title,
@@ -628,13 +470,6 @@ export class AIRichBuilder {
628
470
  })
629
471
  }
630
472
 
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
473
  async send(jid, opts = {}) {
639
474
  const envelope = await this.build(opts)
640
475
  return this.#conn.relayMessage(jid, envelope, opts)
@@ -1,39 +1,6 @@
1
- /**
2
- * @file builders/base.js
3
- * @module lumina/builders/base
4
- *
5
- * Builder-state composer. Lumina deliberately avoids a `BaseBuilder` parent
6
- * class — inheritance forces every builder to carry every state field
7
- * (`_title`, `_subtitle`, etc.) whether or not they need it, and locks the
8
- * framework into a single hierarchy.
9
- *
10
- * Instead, we expose `applyContentFields(target)` — a mixin that attaches
11
- * the fluent content methods (`title`, `body`, `footer`, `contextInfo`,
12
- * `payload`) to whatever builder invokes it. A builder composes only the
13
- * capabilities it needs.
14
- */
15
-
16
1
  import { ensurePlainObject } from '../utils/validator.js'
17
2
  import { ValidationError } from '../errors.js'
18
3
 
19
- /**
20
- * Apply the shared content-mixin to a builder instance.
21
- *
22
- * Attaches:
23
- * - title(t) → sets _title
24
- * - subtitle(t) → sets _subtitle
25
- * - body(t) → sets _body
26
- * - footer(t) → sets _footer
27
- * - contextInfo(o) → sets _contextInfo (validated plain object)
28
- * - payload(o) → merges into _extraPayload (validated plain object)
29
- *
30
- * Each method returns `this` for chaining.
31
- *
32
- * @template {object} T
33
- * @param {T} target The builder instance to mixin into.
34
- * @param {object} [initial] Optional initial state.
35
- * @returns {T & ContentFields}
36
- */
37
4
  export function applyContentFields(target, initial = {}) {
38
5
  target._title = initial.title ?? ''
39
6
  target._subtitle = initial.subtitle ?? ''
@@ -43,33 +10,25 @@ export function applyContentFields(target, initial = {}) {
43
10
  target._extraPayload = initial.extraPayload ?? {}
44
11
 
45
12
  target.title = function (t) {
46
- if (typeof t !== 'string') {
47
- throw new ValidationError('title must be a string', { code: 'LUMINA_VALIDATION_TITLE' })
48
- }
13
+ if (typeof t !== 'string') throw new ValidationError('title must be a string', { code: 'LUMINA_VALIDATION_TITLE' })
49
14
  target._title = t
50
15
  return target
51
16
  }
52
17
 
53
18
  target.subtitle = function (t) {
54
- if (typeof t !== 'string') {
55
- throw new ValidationError('subtitle must be a string', { code: 'LUMINA_VALIDATION_SUBTITLE' })
56
- }
19
+ if (typeof t !== 'string') throw new ValidationError('subtitle must be a string', { code: 'LUMINA_VALIDATION_SUBTITLE' })
57
20
  target._subtitle = t
58
21
  return target
59
22
  }
60
23
 
61
24
  target.body = function (t) {
62
- if (typeof t !== 'string') {
63
- throw new ValidationError('body must be a string', { code: 'LUMINA_VALIDATION_BODY' })
64
- }
25
+ if (typeof t !== 'string') throw new ValidationError('body must be a string', { code: 'LUMINA_VALIDATION_BODY' })
65
26
  target._body = t
66
27
  return target
67
28
  }
68
29
 
69
30
  target.footer = function (t) {
70
- if (typeof t !== 'string') {
71
- throw new ValidationError('footer must be a string', { code: 'LUMINA_VALIDATION_FOOTER' })
72
- }
31
+ if (typeof t !== 'string') throw new ValidationError('footer must be a string', { code: 'LUMINA_VALIDATION_FOOTER' })
73
32
  target._footer = t
74
33
  return target
75
34
  }
@@ -89,12 +48,6 @@ export function applyContentFields(target, initial = {}) {
89
48
  return target
90
49
  }
91
50
 
92
- /**
93
- * Helper: extract the standard content fields as an envelope fragment.
94
- *
95
- * @param {object} target A builder that has had applyContentFields called on it.
96
- * @returns {{ title: string, subtitle: string, body: string, footer: string, contextInfo: object, extraPayload: object }}
97
- */
98
51
  export function readContentFields(target) {
99
52
  return {
100
53
  title: target._title,
@@ -1,100 +1,45 @@
1
- /**
2
- * @file builders/button-v2.js
3
- * @module lumina/builders/button-v2
4
- *
5
- * ButtonV2Builder — legacy `buttonsMessage` format. Preserved for users who
6
- * target WhatsApp clients that do not yet support native flow.
7
- *
8
- * Refactored to use the same mixin-based content fields, registry-based
9
- * button factory, and centralised interactive relay nodes as ButtonBuilder.
10
- * The hardcoded `headerType: 6` (LOCATION_THUMBNAIL) magic number is now
11
- * sourced from `proto/enums.HeaderType`.
12
- */
13
-
14
1
  import { applyContentFields } from './base.js'
15
2
  import { coerceMediaSource, ensurePlainObject } from '../utils/validator.js'
16
3
  import { createInteractiveNodes } from '../proto/relay-nodes.js'
17
4
  import { HeaderType } from '../proto/enums.js'
18
5
  import { uuid } from '../utils/id.js'
19
-
20
6
  import { fetchBuffer } from '../media/fetch.js'
21
7
  import { resize } from '../media/image.js'
22
8
 
23
- /** @typedef {import('../client/connection.js').Connection} Connection */
24
-
25
9
  export class ButtonV2Builder {
26
- /** @param {Connection} conn */
27
10
  constructor(conn) {
28
11
  applyContentFields(this)
29
12
  this.#conn = conn
30
-
31
- /** @type {Array<object>} */
32
13
  this._buttons = []
33
- /** @type {string | Buffer | null} */
34
14
  this._thumbnailSource = null
35
- /** @type {object | null} */
36
15
  this._data = null
37
16
  }
38
17
 
39
- /** @type {Connection} */ #conn
18
+ #conn
40
19
 
41
- /**
42
- * Add a legacy button with a display text and (optional) explicit ID.
43
- * Auto-generates a UUID when `id` is omitted.
44
- *
45
- * @param {string} displayText
46
- * @param {string} [id]
47
- */
48
20
  button(displayText, id = uuid()) {
49
- this._buttons.push({
50
- buttonId: id,
51
- buttonText: { displayText },
52
- type: 1,
53
- })
21
+ this._buttons.push({ buttonId: id, buttonText: { displayText }, type: 1 })
54
22
  return this
55
23
  }
56
24
 
57
- /**
58
- * Push a raw, already-shaped button object.
59
- *
60
- * @param {object} obj
61
- */
62
25
  rawButton(obj) {
63
26
  ensurePlainObject(obj, 'button')
64
27
  this._buttons.push(obj)
65
28
  return this
66
29
  }
67
30
 
68
- /**
69
- * Set the JPEG thumbnail (used by the location-fallback path).
70
- *
71
- * @param {string | Buffer} source
72
- */
73
31
  thumbnail(source) {
74
32
  if (!source) throw new TypeError('thumbnail source is required')
75
33
  this._thumbnailSource = source
76
34
  return this
77
35
  }
78
36
 
79
- /**
80
- * Provide a fully-shaped media descriptor that overrides the
81
- * location-thumbnail fallback entirely.
82
- *
83
- * @param {object} obj
84
- */
85
37
  media(obj) {
86
38
  ensurePlainObject(obj, 'media')
87
39
  this._data = obj
88
40
  return this
89
41
  }
90
42
 
91
- /**
92
- * Build the proto message object (without relaying).
93
- *
94
- * @param {string} jid
95
- * @param {object} [opts]
96
- * @returns {Promise<object>}
97
- */
98
43
  async build(jid, opts = {}) {
99
44
  let jpegThumbnail = null
100
45
  if (this._thumbnailSource) {
@@ -106,18 +51,16 @@ export class ButtonV2Builder {
106
51
  }
107
52
  }
108
53
 
109
- const fallback =
110
- this._data ??
111
- {
112
- headerType: HeaderType.LOCATION_THUMBNAIL,
113
- locationMessage: {
114
- degreesLatitude: 0,
115
- degreesLongitude: 0,
116
- name: this._title,
117
- address: this._subtitle,
118
- jpegThumbnail,
119
- },
120
- }
54
+ const fallback = this._data ?? {
55
+ headerType: HeaderType.LOCATION_THUMBNAIL,
56
+ locationMessage: {
57
+ degreesLatitude: 0,
58
+ degreesLongitude: 0,
59
+ name: this._title,
60
+ address: this._subtitle,
61
+ jpegThumbnail,
62
+ },
63
+ }
121
64
 
122
65
  return this.#conn.generateMessage(
123
66
  jid,
@@ -136,16 +79,8 @@ export class ButtonV2Builder {
136
79
  )
137
80
  }
138
81
 
139
- /**
140
- * Build + relay.
141
- *
142
- * @param {string} jid
143
- * @param {object} [opts]
144
- */
145
82
  async send(jid, opts = {}) {
146
- if (this._buttons.length === 0) {
147
- throw new Error('ButtonV2 requires at least one button')
148
- }
83
+ if (this._buttons.length === 0) throw new Error('ButtonV2 requires at least one button')
149
84
  const msg = await this.build(jid, opts)
150
85
  await this.#conn.relayMessage(msg.key.remoteJid, msg.message, {
151
86
  messageId: msg.key.id,