@kyyinfinite/lumina 1.0.6 → 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyyinfinite/lumina",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
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",
@@ -12,13 +12,21 @@ import {
12
12
  import { singleLayout, hscrollLayout, actionRowLayout } from '../proto/layouts.js'
13
13
  import { assembleRichResponse } from '../proto/rich-response.js'
14
14
  import { MessageType, ImagineType, LayoutKind } from '../proto/enums.js'
15
+ import { isRawSocket, socketToServices } from '../utils/socket-to-conn.js'
15
16
 
16
17
  export class AIRichBuilder {
17
18
  #conn
18
19
  #proto
19
20
  #media
20
21
 
22
+ /**
23
+ * @param {object} conn Connection instance OR raw Baileys socket (legacy: new AIRich(socket))
24
+ */
21
25
  constructor(conn, proto, media) {
26
+ // ── Auto-detect raw socket (backward compat: new AIRich(socket)) ──────────
27
+ const services = isRawSocket(conn) ? socketToServices(conn) : null
28
+ if (services) { conn = services.conn; proto = services.proto; media = services.media }
29
+
22
30
  applyContentFields(this)
23
31
  this.#conn = conn
24
32
  this.#proto = proto
@@ -34,6 +42,8 @@ export class AIRichBuilder {
34
42
  this._quotedParticipant = undefined
35
43
  }
36
44
 
45
+ // ─── Flags ────────────────────────────────────────────────────────────────────
46
+
37
47
  forwarded(v = true) { this._forwarded = v; return this }
38
48
  notification(v) { this._notification = v; return this }
39
49
  includesUnifiedResponse(v = true) { this._includesUnifiedResponse = v; return this }
@@ -45,6 +55,8 @@ export class AIRichBuilder {
45
55
  return this
46
56
  }
47
57
 
58
+ // ─── Low-level section/submessage injection ───────────────────────────────────
59
+
48
60
  submessage(msg) {
49
61
  const items = Array.isArray(msg) ? msg : [msg]
50
62
  items.forEach((m) => ensureObjectOrArray(m, 'submessage'))
@@ -52,6 +64,30 @@ export class AIRichBuilder {
52
64
  return this
53
65
  }
54
66
 
67
+ /**
68
+ * Directly push a raw section object (or array of objects) into the sections list.
69
+ * Useful when you've built a section manually via proto helpers.
70
+ */
71
+ section(s) {
72
+ const items = Array.isArray(s) ? s : [s]
73
+ for (const item of items) {
74
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) {
75
+ throw new ValidationError('Section must be a plain object or array of plain objects', {
76
+ code: 'LUMINA_VALIDATION_SECTION',
77
+ })
78
+ }
79
+ this._sections.push(item)
80
+ }
81
+ return this
82
+ }
83
+
84
+ /** @deprecated use `.submessage()` */
85
+ addSubmessage(msg) { return this.submessage(msg) }
86
+ /** @deprecated use `.section()` */
87
+ addSection(s) { return this.section(s) }
88
+
89
+ // ─── Content builders ─────────────────────────────────────────────────────────
90
+
55
91
  async text(text, opts = {}) {
56
92
  ensureString(text, 'text')
57
93
  const { hyperlink = true, citation = true, latex = true } = opts
@@ -176,10 +212,10 @@ export class AIRichBuilder {
176
212
  const resolved = await Promise.all(
177
213
  list.map(async (item) => {
178
214
  const avatarSrc = item.profileIconUrl ?? item.profile_url ?? item.profile
179
- const thumbSrc = item.thumbnailUrl ?? item.thumbnail
215
+ const thumbSrc = item.thumbnailUrl ?? item.thumbnail
180
216
  const [avatar, thumbnail] = await Promise.all([
181
217
  avatarSrc ? this.#media.resolve(avatarSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
182
- thumbSrc ? this.#media.resolve(thumbSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
218
+ thumbSrc ? this.#media.resolve(thumbSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
183
219
  ])
184
220
  return { ...item, avatar, thumbnail }
185
221
  }),
@@ -219,10 +255,10 @@ export class AIRichBuilder {
219
255
  this._submessages.push({ messageType: MessageType.TEXT, messageText: '[ CANNOT_LOAD_PRODUCT ]' })
220
256
  const products = await Promise.all(
221
257
  list.map(async (p) => {
222
- const imgSrc = p.image_url ?? p.image
258
+ const imgSrc = p.image_url ?? p.image
223
259
  const iconSrc = p.icon_url ?? p.icon
224
260
  const [imageUrl, iconUrl] = await Promise.all([
225
- imgSrc ? this.#media.resolve(imgSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
261
+ imgSrc ? this.#media.resolve(imgSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
226
262
  iconSrc ? this.#media.resolve(iconSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
227
263
  ])
228
264
  return {
@@ -246,12 +282,12 @@ export class AIRichBuilder {
246
282
  const primitives = await Promise.all(
247
283
  list.map(async (p) => {
248
284
  const profileSrc = p.profile_picture_url ?? p.profile_url ?? p.profile
249
- const thumbSrc = p.thumbnail_url ?? p.thumbnail
250
- const footerSrc = p.footer_icon ?? p.icon
285
+ const thumbSrc = p.thumbnail_url ?? p.thumbnail
286
+ const footerSrc = p.footer_icon ?? p.icon
251
287
  const [profileUrl, thumbUrl, footerIcon] = await Promise.all([
252
288
  profileSrc ? this.#media.resolve(profileSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
253
- thumbSrc ? this.#media.resolve(thumbSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
254
- footerSrc ? this.#media.resolve(footerSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
289
+ thumbSrc ? this.#media.resolve(thumbSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
290
+ footerSrc ? this.#media.resolve(footerSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
255
291
  ])
256
292
  return postPrimitive({
257
293
  title: p.title ?? '', subtitle: p.subtitle ?? '', username: p.username ?? '',
@@ -285,13 +321,10 @@ export class AIRichBuilder {
285
321
  }
286
322
  const pills = suggestion.map((text) => followUpSuggestionPillPrimitive(text))
287
323
  let kind
288
- if (layout) {
289
- kind = layout
290
- } else if (pills.length === 1) {
291
- kind = LayoutKind.SINGLE
292
- } else {
293
- kind = scroll ? LayoutKind.HSCROLL : LayoutKind.ACTION_ROW
294
- }
324
+ if (layout) kind = layout
325
+ else if (pills.length === 1) kind = LayoutKind.SINGLE
326
+ else kind = scroll ? LayoutKind.HSCROLL : LayoutKind.ACTION_ROW
327
+
295
328
  if (kind === LayoutKind.SINGLE) {
296
329
  this._sections.push(singleLayout(pills[0], { __typename: 'GenAIUnifiedResponseSection' }))
297
330
  } else if (kind === LayoutKind.HSCROLL) {
@@ -302,6 +335,32 @@ export class AIRichBuilder {
302
335
  return this
303
336
  }
304
337
 
338
+ // ─── Legacy add* aliases (paste / AIRich API) ─────────────────────────────────
339
+ /** @deprecated use `.text()` */
340
+ async addText(text, opts) { return this.text(text, opts) }
341
+ /** @deprecated use `.code()` */
342
+ async addCode(language, code) { return this.code(language, code) }
343
+ /** @deprecated use `.table()` */
344
+ async addTable(table, opts) { return this.table(table, opts) }
345
+ /** @deprecated use `.image()` */
346
+ async addImage(source, opts) { return this.image(source, opts) }
347
+ /** @deprecated use `.video()` */
348
+ async addVideo(source, opts) { return this.video(source, opts) }
349
+ /** @deprecated use `.source()` */
350
+ async addSource(sources) { return this.source(sources) }
351
+ /** @deprecated use `.reels()` */
352
+ async addReels(items) { return this.reels(items) }
353
+ /** @deprecated use `.product()` */
354
+ async addProduct(item) { return this.product(item) }
355
+ /** @deprecated use `.post()` */
356
+ async addPost(item) { return this.post(item) }
357
+ /** @deprecated use `.tip()` */
358
+ async addTip(text) { return this.tip(text) }
359
+ /** @deprecated use `.suggest()` */
360
+ async addSuggest(suggestion, opts) { return this.suggest(suggestion, opts) }
361
+
362
+ // ─── Build & send ─────────────────────────────────────────────────────────────
363
+
305
364
  async build(opts = {}) {
306
365
  return assembleRichResponse({
307
366
  title: this._title,
@@ -45,15 +45,24 @@ export function applyContentFields(target, initial = {}) {
45
45
  return target
46
46
  }
47
47
 
48
+ // ─── Legacy aliases (paste / BaseBuilder API) ───────────────────────────────
49
+ target.setTitle = target.title
50
+ target.setSubtitle = target.subtitle
51
+ target.setBody = target.body
52
+ target.setFooter = target.footer
53
+ target.setContextInfo = target.contextInfo
54
+ /** @deprecated use `.payload()` */
55
+ target.addPayload = target.payload
56
+
48
57
  return target
49
58
  }
50
59
 
51
60
  export function readContentFields(target) {
52
61
  return {
53
- title: target._title,
54
- subtitle: target._subtitle,
55
- body: target._body,
56
- footer: target._footer,
62
+ title: target._title,
63
+ subtitle: target._subtitle,
64
+ body: target._body,
65
+ footer: target._footer,
57
66
  contextInfo: target._contextInfo,
58
67
  extraPayload: target._extraPayload,
59
68
  }
@@ -1,13 +1,23 @@
1
1
  import { applyContentFields } from './base.js'
2
- import { coerceMediaSource, ensurePlainObject } from '../utils/validator.js'
2
+ import { ensurePlainObject } from '../utils/validator.js'
3
3
  import { createInteractiveNodes } from '../proto/relay-nodes.js'
4
4
  import { HeaderType } from '../proto/enums.js'
5
5
  import { uuid } from '../utils/id.js'
6
6
  import { fetchBuffer } from '../media/fetch.js'
7
7
  import { resize } from '../media/image.js'
8
+ import { isRawSocket, socketToServices } from '../utils/socket-to-conn.js'
8
9
 
9
10
  export class ButtonV2Builder {
11
+ #conn
12
+
13
+ /**
14
+ * @param {object} conn Connection instance OR raw Baileys socket (legacy)
15
+ */
10
16
  constructor(conn) {
17
+ // ── Auto-detect raw socket (backward compat: new ButtonV2(socket)) ─────────
18
+ const services = isRawSocket(conn) ? socketToServices(conn) : null
19
+ if (services) conn = services.conn
20
+
11
21
  applyContentFields(this)
12
22
  this.#conn = conn
13
23
  this._buttons = []
@@ -15,8 +25,6 @@ export class ButtonV2Builder {
15
25
  this._data = null
16
26
  }
17
27
 
18
- #conn
19
-
20
28
  button(displayText, id = uuid()) {
21
29
  this._buttons.push({ buttonId: id, buttonText: { displayText }, type: 1 })
22
30
  return this
@@ -40,6 +48,16 @@ export class ButtonV2Builder {
40
48
  return this
41
49
  }
42
50
 
51
+ // ─── Legacy aliases (paste / ButtonV2 API) ───────────────────────────────────
52
+ /** @deprecated use `.button()` */
53
+ addButton(displayText, id = uuid()) { return this.button(displayText, id) }
54
+ /** @deprecated use `.rawButton()` */
55
+ addRawButton(obj) { return this.rawButton(obj) }
56
+ /** @deprecated use `.thumbnail()` */
57
+ setThumbnail(source) { return this.thumbnail(source) }
58
+ /** @deprecated use `.media()` */
59
+ setMedia(obj) { return this.media(obj) }
60
+
43
61
  async build(jid, opts = {}) {
44
62
  let jpegThumbnail = null
45
63
  if (this._thumbnailSource) {
@@ -2,6 +2,7 @@ import { applyContentFields } from './base.js'
2
2
  import { coerceMediaSource, ensurePlainObject } from '../utils/validator.js'
3
3
  import { createInteractiveNodes } from '../proto/relay-nodes.js'
4
4
  import { SimpleButtonType } from '../proto/enums.js'
5
+ import { isRawSocket, socketToServices } from '../utils/socket-to-conn.js'
5
6
 
6
7
  class SelectionBuilder {
7
8
  constructor() {
@@ -9,10 +10,7 @@ class SelectionBuilder {
9
10
  this._sections = []
10
11
  }
11
12
 
12
- title(t) {
13
- this._title = t
14
- return this
15
- }
13
+ title(t) { this._title = t; return this }
16
14
 
17
15
  section(title, builder) {
18
16
  const section = { title, highlight_label: '', rows: [] }
@@ -27,10 +25,7 @@ class SelectionBuilder {
27
25
  }
28
26
 
29
27
  class SectionBuilder {
30
- constructor(rowsRef) {
31
- this._rows = rowsRef
32
- }
33
-
28
+ constructor(rowsRef) { this._rows = rowsRef }
34
29
  row(header, title, description, id) {
35
30
  this._rows.push({ header, title, description, id })
36
31
  return this
@@ -38,7 +33,18 @@ class SectionBuilder {
38
33
  }
39
34
 
40
35
  export class ButtonBuilder {
36
+ #conn
37
+ #proto
38
+ #media
39
+
40
+ /**
41
+ * @param {object} conn Connection instance OR raw Baileys socket (legacy)
42
+ */
41
43
  constructor(conn, proto, media) {
44
+ // ── Auto-detect raw socket (backward compat: new Button(socket)) ──────────
45
+ const services = isRawSocket(conn) ? socketToServices(conn) : null
46
+ if (services) { conn = services.conn; proto = services.proto; media = services.media }
47
+
42
48
  applyContentFields(this)
43
49
  this.#conn = conn
44
50
  this.#proto = proto
@@ -46,11 +52,12 @@ export class ButtonBuilder {
46
52
  this._buttons = []
47
53
  this._data = null
48
54
  this._params = {}
55
+ // For legacy makeRow / makeSection support
56
+ this._currentSelectionIndex = -1
57
+ this._currentSectionIndex = -1
49
58
  }
50
59
 
51
- #conn
52
- #proto
53
- #media
60
+ // ─── Media setters ───────────────────────────────────────────────────────────
54
61
 
55
62
  media(type, source, opts = {}) {
56
63
  if (!['image', 'video', 'document'].includes(type)) {
@@ -61,10 +68,44 @@ export class ButtonBuilder {
61
68
  return this
62
69
  }
63
70
 
64
- image(source, opts) { return this.media('image', source, opts) }
65
- video(source, opts) { return this.media('video', source, opts) }
71
+ image(source, opts) { return this.media('image', source, opts) }
72
+ video(source, opts) { return this.media('video', source, opts) }
66
73
  document(source, opts) { return this.media('document', source, opts) }
67
74
 
75
+ /** @deprecated use `.media(obj)` – raw object form */
76
+ setMedia(obj) {
77
+ ensurePlainObject(obj, 'media')
78
+ this._data = obj
79
+ return this
80
+ }
81
+
82
+ // ── Legacy set* shims (image / video / document with url-or-buffer) ──────────
83
+ setImage(path, opts = {}) {
84
+ if (!path) throw new Error('Url or buffer needed')
85
+ this._data = Buffer.isBuffer(path) ? { image: path, ...opts } : { image: { url: path }, ...opts }
86
+ return this
87
+ }
88
+
89
+ setVideo(path, opts = {}) {
90
+ if (!path) throw new Error('Url or buffer needed')
91
+ this._data = Buffer.isBuffer(path) ? { video: path, ...opts } : { video: { url: path }, ...opts }
92
+ return this
93
+ }
94
+
95
+ setDocument(path, opts = {}) {
96
+ if (!path) throw new Error('Url or buffer needed')
97
+ this._data = Buffer.isBuffer(path) ? { document: path, ...opts } : { document: { url: path }, ...opts }
98
+ return this
99
+ }
100
+
101
+ // ─── Params / params ─────────────────────────────────────────────────────────
102
+
103
+ params(obj) { ensurePlainObject(obj, 'params'); this._params = obj; return this }
104
+ /** @deprecated use `.params()` */
105
+ setParams(obj) { return this.params(obj) }
106
+
107
+ // ─── Raw button ──────────────────────────────────────────────────────────────
108
+
68
109
  button(name, params, extra = {}) {
69
110
  this._buttons.push({
70
111
  name,
@@ -74,27 +115,34 @@ export class ButtonBuilder {
74
115
  return this
75
116
  }
76
117
 
77
- params(obj) {
78
- ensurePlainObject(obj, 'params')
79
- this._params = obj
80
- return this
81
- }
118
+ /** @deprecated use `.button()` */
119
+ addButton(name, params) { return this.button(name, params) }
120
+
121
+ // ─── Simple buttons ──────────────────────────────────────────────────────────
82
122
 
83
123
  simpleButton(kind, displayText, id, opts = {}) {
84
124
  const name = SimpleButtonType[kind]
85
125
  if (!name) throw new TypeError(`unknown simple button kind: ${kind}`)
86
- this._buttons.push({
87
- name,
88
- buttonParamsJson: JSON.stringify({ display_text: displayText, id, ...opts }),
89
- })
126
+ this._buttons.push({ name, buttonParamsJson: JSON.stringify({ display_text: displayText, id, ...opts }) })
90
127
  return this
91
128
  }
92
129
 
93
- reply(displayText, id, opts) { return this.simpleButton('reply', displayText, id, opts) }
94
- call(displayText, id, opts) { return this.simpleButton('call', displayText, id, opts) }
95
- reminder(displayText, id, opts) { return this.simpleButton('reminder', displayText, id, opts) }
96
- cancelReminder(displayText, id, opts) { return this.simpleButton('cancelReminder', displayText, id, opts) }
97
- address(displayText, id, opts) { return this.simpleButton('address', displayText, id, opts) }
130
+ reply(displayText, id, opts) { return this.simpleButton('reply', displayText, id, opts) }
131
+ call(displayText, id, opts) { return this.simpleButton('call', displayText, id, opts) }
132
+ reminder(displayText, id, opts) { return this.simpleButton('reminder', displayText, id, opts) }
133
+ cancelReminder(displayText, id, opts){ return this.simpleButton('cancelReminder', displayText, id, opts) }
134
+ address(displayText, id, opts) { return this.simpleButton('address', displayText, id, opts) }
135
+
136
+ /** @deprecated use `.reply()` */
137
+ addReply(displayText, id, opts) { return this.reply(displayText, id, opts) }
138
+ /** @deprecated use `.call()` */
139
+ addCall(displayText, id, opts) { return this.call(displayText, id, opts) }
140
+ /** @deprecated use `.reminder()` */
141
+ addReminder(displayText, id, opts) { return this.reminder(displayText, id, opts) }
142
+ /** @deprecated use `.cancelReminder()` */
143
+ addCancelReminder(displayText, id, opts) { return this.cancelReminder(displayText, id, opts) }
144
+ /** @deprecated use `.address()` */
145
+ addAddress(displayText, id, opts) { return this.address(displayText, id, opts) }
98
146
 
99
147
  url(displayText, url, opts = {}) {
100
148
  this._buttons.push({
@@ -122,6 +170,19 @@ export class ButtonBuilder {
122
170
  return this
123
171
  }
124
172
 
173
+ /** @deprecated use `.url()` */
174
+ addUrl(displayText, url, webview_interaction = false, opts = {}) {
175
+ return this.url(displayText, url, { webview_interaction, ...opts })
176
+ }
177
+
178
+ /** @deprecated use `.copy()` */
179
+ addCopy(displayText, copyCode, opts = {}) { return this.copy(displayText, copyCode, opts) }
180
+
181
+ /** @deprecated use `.location()` */
182
+ addLocation(opts = {}) { return this.location(opts) }
183
+
184
+ // ─── Selection (new fluent builder API) ──────────────────────────────────────
185
+
125
186
  selection(title, builder, extra = {}) {
126
187
  const sel = new SelectionBuilder()
127
188
  sel.title(title)
@@ -130,11 +191,57 @@ export class ButtonBuilder {
130
191
  return this
131
192
  }
132
193
 
133
- clear() {
134
- this._buttons = []
194
+ // ─── Legacy selection API (paste compatible) ──────────────────────────────────
195
+ // addSelection() / makeSection() / makeRow() use mutable cursor state
196
+
197
+ /**
198
+ * @deprecated Prefer `.selection(title, cb)` builder style.
199
+ * Adds a single_select button and sets cursor for makeSection/makeRow.
200
+ */
201
+ addSelection(title, options = {}) {
202
+ this._buttons.push({
203
+ ...options,
204
+ name: 'single_select',
205
+ buttonParamsJson: JSON.stringify({ title, sections: [] }),
206
+ })
207
+ this._currentSelectionIndex = this._buttons.length - 1
208
+ this._currentSectionIndex = -1
135
209
  return this
136
210
  }
137
211
 
212
+ /**
213
+ * @deprecated Append a section to the current selection (cursor must be set by addSelection).
214
+ */
215
+ makeSection(title = '', highlight_label = '') {
216
+ if (this._currentSelectionIndex === -1) throw new Error('You need to create a selection first')
217
+ const params = JSON.parse(this._buttons[this._currentSelectionIndex].buttonParamsJson)
218
+ params.sections.push({ title, highlight_label, rows: [] })
219
+ this._currentSectionIndex = params.sections.length - 1
220
+ this._buttons[this._currentSelectionIndex].buttonParamsJson = JSON.stringify(params)
221
+ return this
222
+ }
223
+
224
+ /**
225
+ * @deprecated Append a row to the current section.
226
+ */
227
+ makeRow(header = '', title = '', description = '', id = '') {
228
+ if (this._currentSelectionIndex === -1 || this._currentSectionIndex === -1) {
229
+ throw new Error('You need to create a selection and a section first')
230
+ }
231
+ const params = JSON.parse(this._buttons[this._currentSelectionIndex].buttonParamsJson)
232
+ params.sections[this._currentSectionIndex].rows.push({ header, title, description, id })
233
+ this._buttons[this._currentSelectionIndex].buttonParamsJson = JSON.stringify(params)
234
+ return this
235
+ }
236
+
237
+ // ─── Utility ─────────────────────────────────────────────────────────────────
238
+
239
+ clear() { this._buttons = []; return this }
240
+ /** @deprecated use `.clear()` */
241
+ clearButtons() { return this.clear() }
242
+
243
+ // ─── Build & send ─────────────────────────────────────────────────────────────
244
+
138
245
  async toCard() {
139
246
  let mediaPayload = {}
140
247
  if (this._data) {
@@ -1,9 +1,21 @@
1
1
  import { applyContentFields } from './base.js'
2
2
  import { createInteractiveNodes } from '../proto/relay-nodes.js'
3
3
  import { CardBuilder } from './card.js'
4
+ import { isRawSocket, socketToServices } from '../utils/socket-to-conn.js'
4
5
 
5
6
  export class CarouselBuilder {
7
+ #conn
8
+ #proto
9
+ #media
10
+
11
+ /**
12
+ * @param {object} conn Connection instance OR raw Baileys socket (legacy)
13
+ */
6
14
  constructor(conn, proto, media) {
15
+ // ── Auto-detect raw socket (backward compat: new Carousel(socket)) ─────────
16
+ const services = isRawSocket(conn) ? socketToServices(conn) : null
17
+ if (services) { conn = services.conn; proto = services.proto; media = services.media }
18
+
7
19
  applyContentFields(this, { body: '', footer: '' })
8
20
  this.#conn = conn
9
21
  this.#proto = proto
@@ -11,10 +23,6 @@ export class CarouselBuilder {
11
23
  this._cards = []
12
24
  }
13
25
 
14
- #conn
15
- #proto
16
- #media
17
-
18
26
  newCard() {
19
27
  return new CardBuilder(this.#conn)
20
28
  }
@@ -30,9 +38,10 @@ export class CarouselBuilder {
30
38
  return this
31
39
  }
32
40
 
33
- cards(...cards) {
34
- return this.card(cards.flat())
35
- }
41
+ cards(...cards) { return this.card(cards.flat()) }
42
+
43
+ /** @deprecated use `.card()` */
44
+ addCard(card) { return this.card(card) }
36
45
 
37
46
  async build(jid, opts = {}) {
38
47
  return this.#conn.generateMessage(
@@ -5,3 +5,9 @@ export { CardBuilder } from './card.js'
5
5
  export { AIRichBuilder } from './ai-rich.js'
6
6
  export { StickerBuilder } from './sticker.js'
7
7
  export { applyContentFields, readContentFields } from './base.js'
8
+
9
+ // Legacy aliases
10
+ export { ButtonBuilder as Button } from './button.js'
11
+ export { ButtonV2Builder as ButtonV2 } from './button-v2.js'
12
+ export { CarouselBuilder as Carousel } from './carousel.js'
13
+ export { AIRichBuilder as AIRich } from './ai-rich.js'
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Toolkit — backward-compatibility shim matching the original monolithic library's
3
+ * static helper class. All methods delegate to the modular Lumina internals.
4
+ *
5
+ * Usage (legacy):
6
+ * import { Toolkit } from '@kyyinfinite/lumina'
7
+ * const url = await Toolkit.toUrl(sock, buffer, 'image')
8
+ */
9
+ import { extractInlineEntities } from '../parsers/inline-entity.js'
10
+ import { resize as _resize } from '../media/image.js'
11
+ import { resolveDeep } from '../utils/promise.js'
12
+ import { fetchBuffer as _fetchBuffer } from '../media/fetch.js'
13
+ import { getMp4Duration as _getMp4Duration, extractThumbnail as _extractThumbnail } from '../media/video.js'
14
+ import { isRawSocket, socketToServices } from '../utils/socket-to-conn.js'
15
+ import { resolveMedia as _resolveMedia } from '../media/resolver.js'
16
+ import { uploadToWhatsApp } from '../media/uploader.js'
17
+
18
+ function getConn(client) {
19
+ if (isRawSocket(client)) return socketToServices(client).conn
20
+ return client // already a Connection
21
+ }
22
+
23
+ export class Toolkit {
24
+ // ─── Inline entity extraction ──────────────────────────────────────────────
25
+ /**
26
+ * Extract hyperlinks / citations / latex from markdown-like text.
27
+ * Returns the same shape as the original: { text, ie, inline_entities }
28
+ */
29
+ static extractIE(text, opts = {}) {
30
+ const { text: rewritten, entities, metadata } = extractInlineEntities(text, opts)
31
+ return { text: rewritten, ie: entities, inline_entities: metadata }
32
+ }
33
+
34
+ // ─── Image ────────────────────────────────────────────────────────────────
35
+ /**
36
+ * Resize a buffer to (x, y) using sharp.
37
+ * @param {Buffer} buffer
38
+ * @param {number} x width
39
+ * @param {number} y height
40
+ * @param {string} [fit='cover']
41
+ */
42
+ static async resize(buffer, x, y, fit = 'cover') {
43
+ return _resize(buffer, { width: x, height: y, fit, format: 'png' })
44
+ }
45
+
46
+ // ─── Promise utility ──────────────────────────────────────────────────────
47
+ /**
48
+ * Recursively resolve all Promises inside a nested object / array.
49
+ */
50
+ static async waitAllPromises(input) {
51
+ return resolveDeep(input)
52
+ }
53
+
54
+ // ─── Fetch ────────────────────────────────────────────────────────────────
55
+ /**
56
+ * Download a URL and return a Buffer. Returns Buffer.alloc(0) on error when silent=true.
57
+ * @param {string} url
58
+ * @param {object} [options] fetch options
59
+ * @param {object} [meta]
60
+ * @param {boolean} [meta.silent=true]
61
+ */
62
+ static async fetchBuffer(url, options = {}, { silent = true } = {}) {
63
+ return _fetchBuffer(url, { ...options, silent })
64
+ }
65
+
66
+ // ─── Media upload ─────────────────────────────────────────────────────────
67
+ /**
68
+ * Upload a buffer / URL to WhatsApp servers and return the CDN URL.
69
+ * @param {object} _client Raw Baileys socket or Connection
70
+ * @param {Buffer|string} path Buffer or local/remote URL
71
+ * @param {string} [mediaType='document']
72
+ */
73
+ static async toUrl(_client, path, mediaType = 'document') {
74
+ if (!path) throw new Error('Url or buffer needed')
75
+ const conn = getConn(_client)
76
+ const source = Buffer.isBuffer(path) ? path : { url: path }
77
+ return uploadToWhatsApp(conn, source, mediaType)
78
+ }
79
+
80
+ // ─── Media resolver ───────────────────────────────────────────────────────
81
+ /**
82
+ * Resolve media to a URL, Buffer, or base64 string.
83
+ * @param {object} _client Raw Baileys socket or Connection
84
+ * @param {Buffer|string|Array} media
85
+ * @param {string} [mediaType='image']
86
+ * @param {object} [opts]
87
+ * @param {boolean} [opts.resolveUrl=false]
88
+ * @param {boolean} [opts.resolveWAUrl=false]
89
+ * @param {'url'|'buffer'|'base64'} [opts.result='url']
90
+ * @param {boolean} [opts.resize=false]
91
+ * @param {number} [opts.width=300]
92
+ * @param {number} [opts.height=300]
93
+ */
94
+ static async resolveMedia(_client, media, mediaType = 'image', opts = {}) {
95
+ const { result = 'url', resize: doResize = false, width = 300, height = 300 } = opts
96
+ const conn = getConn(_client)
97
+
98
+ let strategy
99
+ if (result === 'buffer') strategy = 'buffer'
100
+ else if (result === 'base64') strategy = 'base64'
101
+ else strategy = 'auto'
102
+
103
+ const resizeOpts = doResize ? { width, height, fit: 'cover' } : undefined
104
+
105
+ if (Array.isArray(media)) {
106
+ return Promise.all(
107
+ media.map((m) => _resolveMedia(conn, m, { mediaType, strategy, resize: resizeOpts }))
108
+ )
109
+ }
110
+
111
+ return _resolveMedia(conn, media, { mediaType, strategy, resize: resizeOpts })
112
+ }
113
+
114
+ // ─── Video ────────────────────────────────────────────────────────────────
115
+ /**
116
+ * Parse MP4 duration from a buffer (in seconds). Returns 0 on error when silent=true.
117
+ */
118
+ static getMp4Duration(buffer, { silent = true } = {}) {
119
+ return _getMp4Duration(buffer, { silent })
120
+ }
121
+
122
+ /**
123
+ * Extract a frame from an MP4 buffer as PNG.
124
+ * @param {Buffer} videoBuffer
125
+ * @param {object} [opts]
126
+ * @param {number} [opts.time] Timestamp in seconds (default: 20% of duration)
127
+ * @param {'buffer'|'base64'} [opts.result='buffer']
128
+ * @param {boolean} [opts.resize=true]
129
+ * @param {number} [opts.width=300]
130
+ * @param {number} [opts.height=300]
131
+ * @param {boolean} [opts.silent=true]
132
+ */
133
+ static async getMp4Preview(videoBuffer, opts = {}) {
134
+ const {
135
+ time, result = 'buffer', resize: doResize = true,
136
+ width = 300, height = 300, silent = true,
137
+ } = opts
138
+ return _extractThumbnail(videoBuffer, {
139
+ time, result,
140
+ resizeOutput: doResize, width, height, silent,
141
+ })
142
+ }
143
+ }
144
+
145
+ export default Toolkit
package/src/index.js CHANGED
@@ -29,9 +29,12 @@ import {
29
29
  import { singleLayout, hscrollLayout, actionRowLayout, layoutFor } from './proto/layouts.js'
30
30
  import { LuminaError, ValidationError, MediaError, ProtoError, ConnectionError, ProtocolError } from './errors.js'
31
31
  import { createLogger } from './utils/logger.js'
32
+ import { Toolkit } from './compat/toolkit.js'
32
33
 
33
34
  export const VERSION = '1.0.0'
34
35
 
36
+ // ─── Primary exports ──────────────────────────────────────────────────────────
37
+
35
38
  export {
36
39
  Bot, Connection,
37
40
  ButtonBuilder, ButtonV2Builder, CarouselBuilder, CardBuilder, AIRichBuilder, StickerBuilder,
@@ -49,6 +52,21 @@ export {
49
52
  singleLayout, hscrollLayout, actionRowLayout, layoutFor,
50
53
  LuminaError, ValidationError, MediaError, ProtoError, ConnectionError, ProtocolError,
51
54
  createLogger,
55
+ Toolkit,
52
56
  }
53
57
 
58
+ // ─── Legacy class-name aliases (paste / monolithic library API) ───────────────
59
+ // Allows code written for the original format to work without changes:
60
+ // import { AIRich, Button, ButtonV2, Carousel } from '@kyyinfinite/lumina'
61
+ // new AIRich(sock), new Button(sock), etc.
62
+
63
+ /** @deprecated Use AIRichBuilder */
64
+ export const AIRich = AIRichBuilder
65
+ /** @deprecated Use ButtonBuilder */
66
+ export const Button = ButtonBuilder
67
+ /** @deprecated Use ButtonV2Builder */
68
+ export const ButtonV2 = ButtonV2Builder
69
+ /** @deprecated Use CarouselBuilder */
70
+ export const Carousel = CarouselBuilder
71
+
54
72
  export default Bot
@@ -0,0 +1,41 @@
1
+ import { Connection } from '../client/connection.js'
2
+ import { MediaService } from '../services/media-service.js'
3
+ import { ProtoService } from '../services/proto-service.js'
4
+
5
+ /**
6
+ * JID used by the original monolithic library for media uploads.
7
+ * Kept as default so legacy code (new AIRich(socket)) works without extra config.
8
+ */
9
+ const DEFAULT_UPLOAD_JID = '@newsletter'
10
+
11
+ /**
12
+ * Check whether `v` looks like a raw Baileys socket (has relayMessage + ev).
13
+ */
14
+ export function isRawSocket(v) {
15
+ return (
16
+ v != null &&
17
+ typeof v === 'object' &&
18
+ typeof v.relayMessage === 'function' &&
19
+ v.ev != null
20
+ )
21
+ }
22
+
23
+ /**
24
+ * Wrap a raw Baileys socket into the three service objects that Lumina builders expect.
25
+ * Returns null if `socketOrConn` is already a Connection (not a raw socket).
26
+ *
27
+ * @param {object} socketOrConn Raw Baileys socket or existing Connection
28
+ * @param {object} [opts]
29
+ * @param {string} [opts.uploadJid] JID used for media upload (defaults to @newsletter)
30
+ * @returns {{ conn: Connection, media: MediaService, proto: ProtoService } | null}
31
+ */
32
+ export function socketToServices(socketOrConn, opts = {}) {
33
+ if (!isRawSocket(socketOrConn)) return null
34
+ const uploadJid = opts.uploadJid ?? DEFAULT_UPLOAD_JID
35
+ const conn = new Connection(socketOrConn, { uploadJid })
36
+ const media = new MediaService(conn, { uploadJid })
37
+ const proto = new ProtoService(conn)
38
+ return { conn, media, proto }
39
+ }
40
+
41
+ export default { isRawSocket, socketToServices }