@kyyinfinite/lumina 1.0.5 → 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.5",
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",
@@ -1,61 +1,53 @@
1
-
2
1
  import { applyContentFields } from './base.js'
3
2
  import { ensureObjectOrArray, ensureString, ensureStringArray } from '../utils/validator.js'
4
3
  import { ValidationError } from '../errors.js'
5
-
6
4
  import { extractInlineEntities } from '../parsers/inline-entity.js'
7
5
  import { tokenizeCode } from '../parsers/code-tokenizer.js'
8
6
  import { toTableMetadata } from '../parsers/table-metadata.js'
9
-
10
7
  import {
11
- markdownTextPrimitive,
12
- codePrimitive,
13
- tablePrimitive,
14
- searchResultPrimitive,
15
- reelPrimitive,
16
- imaginePrimitive,
17
- productCardPrimitive,
18
- postPrimitive,
19
- metadataTextPrimitive,
20
- followUpSuggestionPillPrimitive,
21
- shapeSourceEntry,
22
- shapeReelEntry,
8
+ markdownTextPrimitive, codePrimitive, tablePrimitive, searchResultPrimitive,
9
+ reelPrimitive, imaginePrimitive, productCardPrimitive, postPrimitive,
10
+ metadataTextPrimitive, followUpSuggestionPillPrimitive, shapeSourceEntry, shapeReelEntry,
23
11
  } from '../proto/primitives.js'
24
12
  import { singleLayout, hscrollLayout, actionRowLayout } from '../proto/layouts.js'
25
13
  import { assembleRichResponse } from '../proto/rich-response.js'
26
14
  import { MessageType, ImagineType, LayoutKind } from '../proto/enums.js'
27
-
15
+ import { isRawSocket, socketToServices } from '../utils/socket-to-conn.js'
28
16
 
29
17
  export class AIRichBuilder {
18
+ #conn
19
+ #proto
20
+ #media
21
+
22
+ /**
23
+ * @param {object} conn Connection instance OR raw Baileys socket (legacy: new AIRich(socket))
24
+ */
30
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
+
31
30
  applyContentFields(this)
32
31
  this.#conn = conn
33
32
  this.#proto = proto
34
33
  this.#media = media
35
-
36
34
  this._submessages = []
37
35
  this._sections = []
38
36
  this._richResponseSources = []
39
-
40
37
  this._forwarded = true
41
38
  this._notification = false
42
39
  this._includesUnifiedResponse = true
43
40
  this._includesSubmessages = true
44
- this._quoted
45
- this._quotedParticipant
41
+ this._quoted = undefined
42
+ this._quotedParticipant = undefined
46
43
  }
47
44
 
45
+ // ─── Flags ────────────────────────────────────────────────────────────────────
48
46
 
49
-
50
- forwarded(v = true) {
51
- this._forwarded = v
52
- return this
53
- }
54
-
55
- notification(v) {
56
- this._notification = v
57
- return this
58
- }
47
+ forwarded(v = true) { this._forwarded = v; return this }
48
+ notification(v) { this._notification = v; return this }
49
+ includesUnifiedResponse(v = true) { this._includesUnifiedResponse = v; return this }
50
+ includesSubmessages(v = true) { this._includesSubmessages = v; return this }
59
51
 
60
52
  quoted(quoted, quotedParticipant) {
61
53
  this._quoted = quoted
@@ -63,15 +55,7 @@ export class AIRichBuilder {
63
55
  return this
64
56
  }
65
57
 
66
- includesUnifiedResponse(v = true) {
67
- this._includesUnifiedResponse = v
68
- return this
69
- }
70
-
71
- includesSubmessages(v = true) {
72
- this._includesSubmessages = v
73
- return this
74
- }
58
+ // ─── Low-level section/submessage injection ───────────────────────────────────
75
59
 
76
60
  submessage(msg) {
77
61
  const items = Array.isArray(msg) ? msg : [msg]
@@ -80,42 +64,47 @@ export class AIRichBuilder {
80
64
  return this
81
65
  }
82
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 ─────────────────────────────────────────────────────────
83
90
 
84
91
  async text(text, opts = {}) {
85
92
  ensureString(text, 'text')
86
93
  const { hyperlink = true, citation = true, latex = true } = opts
87
-
88
- const { text: rewritten, metadata } = extractInlineEntities(text, {
89
- hyperlink,
90
- citation,
91
- latex,
92
- })
93
-
94
- this._submessages.push({
95
- messageType: MessageType.TEXT,
96
- messageText: rewritten,
97
- })
98
-
99
- this._sections.push(
100
- singleLayout(markdownTextPrimitive(rewritten, metadata.length ? metadata : undefined)),
101
- )
94
+ const { text: rewritten, metadata } = extractInlineEntities(text, { hyperlink, citation, latex })
95
+ this._submessages.push({ messageType: MessageType.TEXT, messageText: rewritten })
96
+ this._sections.push(singleLayout(markdownTextPrimitive(rewritten, metadata.length ? metadata : undefined)))
102
97
  return this
103
98
  }
104
99
 
105
100
  async code(language, code) {
106
101
  ensureString(language, 'language')
107
102
  ensureString(code, 'code')
108
-
109
103
  const { codeBlock, unifiedBlocks } = tokenizeCode(code, language)
110
-
111
104
  this._submessages.push({
112
105
  messageType: MessageType.CODE,
113
- codeMetadata: {
114
- codeLanguage: language,
115
- codeBlocks: codeBlock,
116
- },
106
+ codeMetadata: { codeLanguage: language, codeBlocks: codeBlock },
117
107
  })
118
-
119
108
  this._sections.push(singleLayout(codePrimitive(language, unifiedBlocks)))
120
109
  return this
121
110
  }
@@ -126,34 +115,22 @@ export class AIRichBuilder {
126
115
  }
127
116
  const { title = '', hyperlink = true, citation = true, latex = true } = opts
128
117
  const meta = toTableMetadata(table, { title, hyperlink, citation, latex })
129
-
130
118
  this._submessages.push({
131
119
  messageType: MessageType.TABLE,
132
120
  tableMetadata: { title: meta.title, rows: meta.rows },
133
121
  })
134
-
135
122
  this._sections.push(singleLayout(tablePrimitive(meta.unifiedRows)))
136
123
  return this
137
124
  }
138
125
 
139
126
  async image(source, opts = {}) {
140
- const { resolveUrl = false } = opts
141
127
  const list = Array.isArray(source) ? source : [source]
142
-
143
128
  const resolved = await Promise.all(
144
129
  list.map(async (s) => {
145
- const url = await this.#media.resolve(s, {
146
- mediaType: 'image',
147
- strategy: resolveUrl ? 'auto' : 'auto',
148
- })
149
- return {
150
- imagePreviewUrl: url,
151
- imageHighResUrl: url,
152
- sourceUrl: url,
153
- }
130
+ const url = await this.#media.resolve(s, { mediaType: 'image', strategy: 'auto' })
131
+ return { imagePreviewUrl: url, imageHighResUrl: url, sourceUrl: url }
154
132
  }),
155
133
  )
156
-
157
134
  this._submessages.push({
158
135
  messageType: MessageType.RICH_RESPONSE,
159
136
  gridImageMetadata: {
@@ -161,15 +138,9 @@ export class AIRichBuilder {
161
138
  imageUrls: resolved,
162
139
  },
163
140
  })
164
-
165
141
  for (const r of resolved) {
166
142
  this._sections.push(
167
- singleLayout(
168
- imaginePrimitive(
169
- { url: r.imagePreviewUrl, mime_type: 'image/png' },
170
- ImagineType.IMAGE,
171
- ),
172
- ),
143
+ singleLayout(imaginePrimitive({ url: r.imagePreviewUrl, mime_type: 'image/png' }, ImagineType.IMAGE)),
173
144
  )
174
145
  }
175
146
  return this
@@ -177,22 +148,15 @@ export class AIRichBuilder {
177
148
 
178
149
  async video(source, opts = {}) {
179
150
  const { autoFill = true } = opts
180
- const isObjectVideo = (v) => v && typeof v === 'object' && v.url
151
+ const isObjVideo = (v) => v && typeof v === 'object' && v.url
181
152
  const items = Array.isArray(source) ? source : [source]
182
153
 
183
- this._submessages.push({
184
- messageType: MessageType.TEXT,
185
- messageText: '[ CANNOT_LOAD_VIDEO ]',
186
- })
154
+ this._submessages.push({ messageType: MessageType.TEXT, messageText: '[ CANNOT_LOAD_VIDEO ]' })
187
155
 
188
156
  await Promise.all(
189
157
  items.map(async (item) => {
190
- const isObj = isObjectVideo(item)
191
- const url = await this.#media.resolve(
192
- isObj ? item.url : item,
193
- { mediaType: 'video', strategy: 'auto' },
194
- )
195
-
158
+ const isObj = isObjVideo(item)
159
+ const url = await this.#media.resolve(isObj ? item.url : item, { mediaType: 'video', strategy: 'auto' })
196
160
  let fileLength = isObj && item.file_length != null ? item.file_length : 0
197
161
  let duration = isObj && item.duration != null ? item.duration : 0
198
162
  let thumbnailB64 = null
@@ -204,26 +168,18 @@ export class AIRichBuilder {
204
168
  fileLength = buf.length
205
169
  duration = this.#media.duration(buf)
206
170
  thumbnailB64 = await this.#media.videoThumbnail(buf, {
207
- time: 0,
208
- result: 'base64',
209
- resizeOutput: true,
210
- width: 300,
211
- height: 300,
171
+ time: 0, result: 'base64', resizeOutput: true, width: 300, height: 300,
212
172
  })
213
173
  }
214
- } catch {
215
- }
174
+ } catch {}
216
175
  }
217
176
 
218
177
  const mimeType = isObj ? item.mime_type ?? 'video/mp4' : 'video/mp4'
219
-
220
178
  this._sections.push(
221
179
  singleLayout(
222
180
  imaginePrimitive(
223
181
  { url, mime_type: mimeType, file_length: fileLength, duration },
224
- ImagineType.ANIMATE,
225
- 'READY',
226
- thumbnailB64 || undefined,
182
+ ImagineType.ANIMATE, 'READY', thumbnailB64 || undefined,
227
183
  ),
228
184
  ),
229
185
  )
@@ -236,12 +192,7 @@ export class AIRichBuilder {
236
192
  if (!Array.isArray(sources)) {
237
193
  throw new ValidationError('sources must be an array', { code: 'LUMINA_VALIDATION_SOURCE' })
238
194
  }
239
-
240
- const normalised =
241
- sources.length && typeof sources[0] === 'string'
242
- ? [sources]
243
- : sources
244
-
195
+ const normalised = sources.length && typeof sources[0] === 'string' ? [sources] : sources
245
196
  const entries = await Promise.all(
246
197
  normalised.map(async (entry) => {
247
198
  const [iconUrl, url, text] = entry
@@ -251,7 +202,6 @@ export class AIRichBuilder {
251
202
  return shapeSourceEntry({ iconUrl: resolvedIcon, url, text })
252
203
  }),
253
204
  )
254
-
255
205
  this._sections.push(singleLayout(searchResultPrimitive(entries)))
256
206
  return this
257
207
  }
@@ -259,27 +209,17 @@ export class AIRichBuilder {
259
209
  async reels(items) {
260
210
  ensureObjectOrArray(items, 'reels')
261
211
  const list = Array.isArray(items) ? items : [items]
262
-
263
212
  const resolved = await Promise.all(
264
213
  list.map(async (item) => {
265
- const [avatar, thumb] = await Promise.all([
266
- item.profileIconUrl ?? item.profile_url ?? item.profile
267
- ? this.#media.resolve(item.profileIconUrl ?? item.profile_url ?? item.profile, {
268
- mediaType: 'image',
269
- strategy: 'auto',
270
- })
271
- : Promise.resolve(''),
272
- item.thumbnailUrl ?? item.thumbnail
273
- ? this.#media.resolve(item.thumbnailUrl ?? item.thumbnail, {
274
- mediaType: 'image',
275
- strategy: 'auto',
276
- })
277
- : Promise.resolve(''),
214
+ const avatarSrc = item.profileIconUrl ?? item.profile_url ?? item.profile
215
+ const thumbSrc = item.thumbnailUrl ?? item.thumbnail
216
+ const [avatar, thumbnail] = await Promise.all([
217
+ avatarSrc ? this.#media.resolve(avatarSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
218
+ thumbSrc ? this.#media.resolve(thumbSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
278
219
  ])
279
- return { ...item, avatar, thumbnail: thumb }
220
+ return { ...item, avatar, thumbnail }
280
221
  }),
281
222
  )
282
-
283
223
  this._submessages.push({
284
224
  messageType: MessageType.REELS,
285
225
  contentItemsMetadata: {
@@ -294,7 +234,6 @@ export class AIRichBuilder {
294
234
  })),
295
235
  },
296
236
  })
297
-
298
237
  resolved.forEach((item, idx) => {
299
238
  this._richResponseSources.push({
300
239
  provider: 'LUMINA',
@@ -306,110 +245,62 @@ export class AIRichBuilder {
306
245
  sourceTitle: item.username ?? '',
307
246
  })
308
247
  })
309
-
310
- this._sections.push(
311
- hscrollLayout(resolved.map((item) => reelPrimitive(shapeReelEntry(item)))),
312
- )
248
+ this._sections.push(hscrollLayout(resolved.map((item) => reelPrimitive(shapeReelEntry(item)))))
313
249
  return this
314
250
  }
315
251
 
316
252
  async product(item) {
317
253
  ensureObjectOrArray(item, 'product')
318
254
  const list = Array.isArray(item) ? item : [item]
319
-
320
- this._submessages.push({
321
- messageType: MessageType.TEXT,
322
- messageText: '[ CANNOT_LOAD_PRODUCT ]',
323
- })
324
-
255
+ this._submessages.push({ messageType: MessageType.TEXT, messageText: '[ CANNOT_LOAD_PRODUCT ]' })
325
256
  const products = await Promise.all(
326
257
  list.map(async (p) => {
258
+ const imgSrc = p.image_url ?? p.image
259
+ const iconSrc = p.icon_url ?? p.icon
327
260
  const [imageUrl, iconUrl] = await Promise.all([
328
- p.image_url ?? p.image
329
- ? this.#media.resolve(p.image_url ?? p.image, { mediaType: 'image', strategy: 'auto' })
330
- : Promise.resolve(''),
331
- p.icon_url ?? p.icon
332
- ? this.#media.resolve(p.icon_url ?? p.icon, { mediaType: 'image', strategy: 'auto' })
333
- : Promise.resolve(''),
261
+ imgSrc ? this.#media.resolve(imgSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
262
+ iconSrc ? this.#media.resolve(iconSrc, { mediaType: 'image', strategy: 'auto' }) : Promise.resolve(''),
334
263
  ])
335
264
  return {
336
- title: p.title,
337
- brand: p.brand,
338
- price: p.price,
339
- sale_price: p.sale_price,
265
+ title: p.title, brand: p.brand, price: p.price, sale_price: p.sale_price,
340
266
  product_url: p.product_url ?? p.url,
341
267
  image: { url: imageUrl },
342
268
  additional_images: [{ url: iconUrl }],
343
269
  }
344
270
  }),
345
271
  )
346
-
347
272
  const primitives = products.map((p) => productCardPrimitive(p))
348
- this._sections.push(
349
- list.length === 1 ? singleLayout(primitives[0]) : hscrollLayout(primitives),
350
- )
273
+ this._sections.push(list.length === 1 ? singleLayout(primitives[0]) : hscrollLayout(primitives))
351
274
  return this
352
275
  }
353
276
 
354
277
  async post(item) {
355
278
  ensureObjectOrArray(item, 'post')
356
279
  const list = Array.isArray(item) ? item : [item]
357
-
358
- this._submessages.push({
359
- messageType: MessageType.TEXT,
360
- messageText: '[ CANNOT_LOAD_POST ]',
361
- })
362
-
280
+ this._submessages.push({ messageType: MessageType.TEXT, messageText: '[ CANNOT_LOAD_POST ]' })
363
281
  const isCarousel = list.length > 1
364
-
365
282
  const primitives = await Promise.all(
366
283
  list.map(async (p) => {
284
+ const profileSrc = p.profile_picture_url ?? p.profile_url ?? p.profile
285
+ const thumbSrc = p.thumbnail_url ?? p.thumbnail
286
+ const footerSrc = p.footer_icon ?? p.icon
367
287
  const [profileUrl, thumbUrl, footerIcon] = await Promise.all([
368
- p.profile_picture_url ?? p.profile_url ?? p.profile
369
- ? this.#media.resolve(p.profile_picture_url ?? p.profile_url ?? p.profile, {
370
- mediaType: 'image',
371
- strategy: 'auto',
372
- })
373
- : Promise.resolve(''),
374
- p.thumbnail_url ?? p.thumbnail
375
- ? this.#media.resolve(p.thumbnail_url ?? p.thumbnail, {
376
- mediaType: 'image',
377
- strategy: 'auto',
378
- })
379
- : Promise.resolve(''),
380
- p.footer_icon ?? p.icon
381
- ? this.#media.resolve(p.footer_icon ?? p.icon, {
382
- mediaType: 'image',
383
- strategy: 'auto',
384
- })
385
- : Promise.resolve(''),
288
+ profileSrc ? this.#media.resolve(profileSrc, { 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(''),
386
291
  ])
387
-
388
- return postPrimitive(
389
- {
390
- title: p.title ?? '',
391
- subtitle: p.subtitle ?? '',
392
- username: p.username ?? '',
393
- profile_picture_url: profileUrl,
394
- is_verified: !!(p.is_verified ?? p.verified),
395
- thumbnail_url: thumbUrl,
396
- post_caption: p.post_caption ?? p.caption ?? '',
397
- likes_count: p.likes_count ?? p.like ?? 0,
398
- comments_count: p.comments_count ?? p.comment ?? 0,
399
- shares_count: p.shares_count ?? p.share ?? 0,
400
- post_url: p.post_url ?? p.url ?? '',
401
- post_deeplink: p.post_deeplink ?? p.deeplink ?? '',
402
- source_app: p.source_app ?? p.source ?? 'INSTAGRAM',
403
- footer_label: p.footer_label ?? p.footer ?? '',
404
- footer_icon: footerIcon,
405
- orientation: p.orientation ?? 'LANDSCAPE',
406
- post_type: p.post_type ?? 'VIDEO',
407
- },
408
- isCarousel,
409
- )
292
+ return postPrimitive({
293
+ title: p.title ?? '', subtitle: p.subtitle ?? '', username: p.username ?? '',
294
+ profile_picture_url: profileUrl, is_verified: !!(p.is_verified ?? p.verified),
295
+ thumbnail_url: thumbUrl, post_caption: p.post_caption ?? p.caption ?? '',
296
+ likes_count: p.likes_count ?? p.like ?? 0, comments_count: p.comments_count ?? p.comment ?? 0,
297
+ shares_count: p.shares_count ?? p.share ?? 0, post_url: p.post_url ?? p.url ?? '',
298
+ post_deeplink: p.post_deeplink ?? p.deeplink ?? '', source_app: p.source_app ?? p.source ?? 'INSTAGRAM',
299
+ footer_label: p.footer_label ?? p.footer ?? '', footer_icon: footerIcon,
300
+ orientation: p.orientation ?? 'LANDSCAPE', post_type: p.post_type ?? 'VIDEO',
301
+ }, isCarousel)
410
302
  }),
411
303
  )
412
-
413
304
  this._sections.push(hscrollLayout(primitives))
414
305
  return this
415
306
  }
@@ -423,23 +314,16 @@ export class AIRichBuilder {
423
314
 
424
315
  async suggest(suggestion, opts = {}) {
425
316
  const { scroll = true, layout } = opts
426
-
427
317
  if (typeof suggestion === 'string') {
428
318
  suggestion = [suggestion]
429
319
  } else {
430
320
  ensureStringArray(suggestion, 'suggestion')
431
321
  }
432
-
433
322
  const pills = suggestion.map((text) => followUpSuggestionPillPrimitive(text))
434
-
435
323
  let kind
436
- if (layout) {
437
- kind = layout
438
- } else if (pills.length === 1) {
439
- kind = LayoutKind.SINGLE
440
- } else {
441
- kind = scroll ? LayoutKind.HSCROLL : LayoutKind.ACTION_ROW
442
- }
324
+ if (layout) kind = layout
325
+ else if (pills.length === 1) kind = LayoutKind.SINGLE
326
+ else kind = scroll ? LayoutKind.HSCROLL : LayoutKind.ACTION_ROW
443
327
 
444
328
  if (kind === LayoutKind.SINGLE) {
445
329
  this._sections.push(singleLayout(pills[0], { __typename: 'GenAIUnifiedResponseSection' }))
@@ -451,6 +335,31 @@ export class AIRichBuilder {
451
335
  return this
452
336
  }
453
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 ─────────────────────────────────────────────────────────────
454
363
 
455
364
  async build(opts = {}) {
456
365
  return assembleRichResponse({
@@ -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 }