@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/src/index.js CHANGED
@@ -1,151 +1,53 @@
1
- /**
2
- * @file index.js
3
- * @module @kyyinfinite/lumina
4
- *
5
- * Lumina — Modern WhatsApp framework built on top of Baileys.
6
- *
7
- * Public API surface:
8
- *
9
- * import { Bot } from '@kyyinfinite/lumina'
10
- * const bot = new Bot(socket)
11
- * await bot.text(jid, 'Halo')
12
- *
13
- * Subpath exports (tree-shakable):
14
- *
15
- * import { extractInlineEntities } from '@kyyinfinite/lumina/parsers'
16
- * import { MediaService } from '@kyyinfinite/lumina/services'
17
- * import { ButtonBuilder } from '@kyyinfinite/lumina/builders'
18
- * import { ProtoUpdater } from '@kyyinfinite/lumina/proto'
19
- * import { resize } from '@kyyinfinite/lumina/media'
20
- */
21
-
22
1
  import { Bot } from './client/bot.js'
23
2
  import { Connection } from './client/connection.js'
24
-
25
3
  import { ButtonBuilder } from './builders/button.js'
26
4
  import { ButtonV2Builder } from './builders/button-v2.js'
27
5
  import { CarouselBuilder } from './builders/carousel.js'
28
6
  import { CardBuilder } from './builders/card.js'
29
7
  import { AIRichBuilder } from './builders/ai-rich.js'
30
-
8
+ import { StickerBuilder } from './builders/sticker.js'
9
+ import { toSticker, addExif } from './media/sticker.js'
31
10
  import { MediaService } from './services/media-service.js'
32
11
  import { ProtoService } from './services/proto-service.js'
33
12
  import { MessageService } from './services/message-service.js'
34
-
35
13
  import { extractInlineEntities } from './parsers/inline-entity.js'
36
14
  import { tokenizeCode } from './parsers/code-tokenizer.js'
37
15
  import { toTableMetadata } from './parsers/table-metadata.js'
38
-
39
16
  import { ProtoUpdater, transformToESM, applyKnownFixes } from './proto/updater.js'
40
17
  import { assembleRichResponse } from './proto/rich-response.js'
41
18
  import { createInteractiveNodes, createBareInteractiveNodes } from './proto/relay-nodes.js'
42
-
43
- // Catalog re-exports — these are imported so that the named exports below
44
- // are available from the root `@kyyinfinite/lumina` entry point.
45
19
  import {
46
- MessageType,
47
- ForwardOrigin,
48
- HeaderType,
49
- NativeFlow,
50
- BOT_JID,
51
- LayoutKind,
52
- HighlightType,
53
- HighlightLabel,
54
- ImagineType,
55
- SourceType,
56
- PromptType,
57
- SessionTransparencyType,
58
- TYPENAME,
20
+ MessageType, ForwardOrigin, HeaderType, NativeFlow, BOT_JID, LayoutKind,
21
+ HighlightType, HighlightLabel, ImagineType, SourceType, PromptType,
22
+ SessionTransparencyType, TYPENAME, SimpleButtonType,
59
23
  } from './proto/enums.js'
60
-
61
24
  import {
62
- markdownTextPrimitive,
63
- codePrimitive,
64
- tablePrimitive,
65
- searchResultPrimitive,
66
- reelPrimitive,
67
- imaginePrimitive,
68
- productCardPrimitive,
69
- postPrimitive,
70
- metadataTextPrimitive,
71
- followUpSuggestionPillPrimitive,
72
- shapeSourceEntry,
73
- shapeReelEntry,
25
+ markdownTextPrimitive, codePrimitive, tablePrimitive, searchResultPrimitive,
26
+ reelPrimitive, imaginePrimitive, productCardPrimitive, postPrimitive,
27
+ metadataTextPrimitive, followUpSuggestionPillPrimitive, shapeSourceEntry, shapeReelEntry,
74
28
  } from './proto/primitives.js'
75
-
76
29
  import { singleLayout, hscrollLayout, actionRowLayout, layoutFor } from './proto/layouts.js'
77
-
78
30
  import { LuminaError, ValidationError, MediaError, ProtoError, ConnectionError, ProtocolError } from './errors.js'
79
31
  import { createLogger } from './utils/logger.js'
80
32
 
81
- /** Semver version of the Lumina package. */
82
33
  export const VERSION = '1.0.0'
83
34
 
84
- // Public API
85
35
  export {
86
- Bot,
87
- Connection,
88
- // Builders
89
- ButtonBuilder,
90
- ButtonV2Builder,
91
- CarouselBuilder,
92
- CardBuilder,
93
- AIRichBuilder,
94
- // Services
95
- MediaService,
96
- ProtoService,
97
- MessageService,
98
- // Parsers
99
- extractInlineEntities,
100
- tokenizeCode,
101
- toTableMetadata,
102
- // Proto
103
- ProtoUpdater,
104
- transformToESM,
105
- applyKnownFixes,
106
- assembleRichResponse,
107
- createInteractiveNodes,
108
- createBareInteractiveNodes,
109
- // Catalog
110
- MessageType,
111
- ForwardOrigin,
112
- HeaderType,
113
- NativeFlow,
114
- BOT_JID,
115
- LayoutKind,
116
- HighlightType,
117
- HighlightLabel,
118
- ImagineType,
119
- SourceType,
120
- PromptType,
121
- SessionTransparencyType,
122
- TYPENAME,
123
- // Primitive factories
124
- markdownTextPrimitive,
125
- codePrimitive,
126
- tablePrimitive,
127
- searchResultPrimitive,
128
- reelPrimitive,
129
- imaginePrimitive,
130
- productCardPrimitive,
131
- postPrimitive,
132
- metadataTextPrimitive,
133
- followUpSuggestionPillPrimitive,
134
- shapeSourceEntry,
135
- shapeReelEntry,
136
- // Layout factories
137
- singleLayout,
138
- hscrollLayout,
139
- actionRowLayout,
140
- layoutFor,
141
- // Errors
142
- LuminaError,
143
- ValidationError,
144
- MediaError,
145
- ProtoError,
146
- ConnectionError,
147
- ProtocolError,
148
- // Utils
36
+ Bot, Connection,
37
+ ButtonBuilder, ButtonV2Builder, CarouselBuilder, CardBuilder, AIRichBuilder, StickerBuilder,
38
+ toSticker, addExif,
39
+ MediaService, ProtoService, MessageService,
40
+ extractInlineEntities, tokenizeCode, toTableMetadata,
41
+ ProtoUpdater, transformToESM, applyKnownFixes,
42
+ assembleRichResponse, createInteractiveNodes, createBareInteractiveNodes,
43
+ MessageType, ForwardOrigin, HeaderType, NativeFlow, BOT_JID, LayoutKind,
44
+ HighlightType, HighlightLabel, ImagineType, SourceType, PromptType,
45
+ SessionTransparencyType, TYPENAME, SimpleButtonType,
46
+ markdownTextPrimitive, codePrimitive, tablePrimitive, searchResultPrimitive,
47
+ reelPrimitive, imaginePrimitive, productCardPrimitive, postPrimitive,
48
+ metadataTextPrimitive, followUpSuggestionPillPrimitive, shapeSourceEntry, shapeReelEntry,
49
+ singleLayout, hscrollLayout, actionRowLayout, layoutFor,
50
+ LuminaError, ValidationError, MediaError, ProtoError, ConnectionError, ProtocolError,
149
51
  createLogger,
150
52
  }
151
53
 
@@ -1,29 +1,5 @@
1
- /**
2
- * @file fetch.js
3
- * @module lumina/media/fetch
4
- *
5
- * HTTP fetcher for media URLs. Returns a Buffer. Honours a configurable
6
- * timeout and an opt-in `silent` mode (default OFF — fail loud).
7
- */
8
-
9
1
  import { MediaError } from '../errors.js'
10
2
 
11
- /**
12
- * @typedef {object} FetchOptions
13
- * @property {boolean} [silent=false] Swallow HTTP errors and return Buffer.alloc(0).
14
- * @property {number} [timeout=30000] Request timeout in milliseconds.
15
- * @property {Record<string, string>} [headers] Additional request headers.
16
- * @property {AbortSignal} [signal] External abort signal.
17
- */
18
-
19
- /**
20
- * Fetch a URL and return its body as a Buffer.
21
- *
22
- * @param {string} url
23
- * @param {FetchOptions} [opts]
24
- * @returns {Promise<Buffer>}
25
- * @throws {MediaError} When `silent` is false (default) and the request fails.
26
- */
27
3
  export async function fetchBuffer(url, opts = {}) {
28
4
  const { silent = false, timeout = 30_000, headers, signal } = opts
29
5
 
@@ -34,23 +10,16 @@ export async function fetchBuffer(url, opts = {}) {
34
10
 
35
11
  const controller = new AbortController()
36
12
  const timer = setTimeout(() => controller.abort(), timeout)
37
- // Combine external signal with our timeout signal.
38
13
  if (signal) {
39
14
  if (signal.aborted) controller.abort()
40
15
  else signal.addEventListener('abort', () => controller.abort(), { once: true })
41
16
  }
42
17
 
43
- const response = await fetch(url, {
44
- headers,
45
- signal: controller.signal,
46
- redirect: 'follow',
47
- })
18
+ const response = await fetch(url, { headers, signal: controller.signal, redirect: 'follow' })
48
19
  clearTimeout(timer)
49
20
 
50
21
  if (!response.ok) {
51
- throw new MediaError(`HTTP ${response.status} for ${url}`, {
52
- code: 'LUMINA_MEDIA_HTTP_ERROR',
53
- })
22
+ throw new MediaError(`HTTP ${response.status} for ${url}`, { code: 'LUMINA_MEDIA_HTTP_ERROR' })
54
23
  }
55
24
 
56
25
  return Buffer.from(await response.arrayBuffer())
@@ -1,21 +1,7 @@
1
- /**
2
- * @file image.js
3
- * @module lumina/media/image
4
- *
5
- * Image resizing via `sharp`. Sharp is an optional peer dependency — the
6
- * module lazy-imports it on first use so that users who never touch image
7
- * resizing do not need it installed.
8
- */
9
-
10
1
  import { MediaError } from '../errors.js'
11
2
 
12
3
  let sharpPromise = null
13
4
 
14
- /**
15
- * Lazy-load sharp. Cached so the import cost is paid only once.
16
- *
17
- * @returns {Promise<typeof import('sharp')>}
18
- */
19
5
  async function loadSharp() {
20
6
  if (!sharpPromise) {
21
7
  sharpPromise = (async () => {
@@ -32,27 +18,9 @@ async function loadSharp() {
32
18
  return sharpPromise
33
19
  }
34
20
 
35
- /**
36
- * @typedef {object} ResizeOptions
37
- * @property {number} [width]
38
- * @property {number} [height]
39
- * @property {'cover'|'contain'|'inside'|'outside'} [fit='cover']
40
- * @property {'png'|'jpeg'|'webp'} [format='png']
41
- * @property {object} [background] Background colour for transparent areas.
42
- */
43
-
44
- /**
45
- * Resize a buffer containing an image. Returns a Buffer in the requested format.
46
- *
47
- * @param {Buffer} buffer
48
- * @param {ResizeOptions} [opts]
49
- * @returns {Promise<Buffer>}
50
- */
51
21
  export async function resize(buffer, opts = {}) {
52
22
  if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
53
- throw new MediaError('resize() requires a non-empty Buffer', {
54
- code: 'LUMINA_MEDIA_EMPTY_BUFFER',
55
- })
23
+ throw new MediaError('resize() requires a non-empty Buffer', { code: 'LUMINA_MEDIA_EMPTY_BUFFER' })
56
24
  }
57
25
 
58
26
  const { width = 300, height = 300, fit = 'cover', format = 'png', background } = opts
@@ -71,14 +39,6 @@ export async function resize(buffer, opts = {}) {
71
39
  return pipeline.toBuffer()
72
40
  }
73
41
 
74
- /**
75
- * Convenience: resize to a square thumbnail (default 300×300 PNG cover).
76
- *
77
- * @param {Buffer} buffer
78
- * @param {number} [size=300]
79
- * @param {ResizeOptions['format']} [format]
80
- * @returns {Promise<Buffer>}
81
- */
82
42
  export async function thumbnail(buffer, size = 300, format = 'png') {
83
43
  return resize(buffer, { width: size, height: size, fit: 'cover', format })
84
44
  }
@@ -1,16 +1,3 @@
1
- /**
2
- * @file resolver.js
3
- * @module lumina/media/resolver
4
- *
5
- * Unified media resolver. Accepts URL | base64 string | Buffer | array-of-any
6
- * and returns a normalised representation per the requested strategy.
7
- *
8
- * Bug fix vs legacy: the original `Toolkit.resolveMedia` had two near-identical
9
- * branches (WAUrl vs non-WAUrl) that both called `fetchBuffer` — collapsed
10
- * into a single, explicit 3-strategy switch (`auto | url-only | buffer |
11
- * base64 | upload`).
12
- */
13
-
14
1
  import { MediaError } from '../errors.js'
15
2
  import { fetchBuffer } from './fetch.js'
16
3
  import { resize } from './image.js'
@@ -19,42 +6,14 @@ import { uploadToWhatsApp } from './uploader.js'
19
6
  const HTTP_RE = /^https?:\/\/.+/i
20
7
  const WA_HOST_RE = /^https?:\/\/[^/]*\.whatsapp\.net\//i
21
8
 
22
- /** @typedef {'auto'|'url-only'|'buffer'|'base64'|'upload'} ResolveStrategy */
23
-
24
- /**
25
- * @typedef {object} ResolveOptions
26
- * @property {'image'|'video'|'audio'|'document'} [mediaType='image']
27
- * @property {ResolveStrategy} [strategy='auto']
28
- * @property {{ width: number, height: number, fit?: string }} [resize] Resize before returning (buffer/base64 only).
29
- * @property {boolean} [silent=false]
30
- * @property {string} [jid] Override upload JID.
31
- */
32
-
33
- /**
34
- * Resolve a single media source.
35
- *
36
- * @param {import('../client/connection.js').Connection} conn
37
- * @param {string | Buffer} source
38
- * @param {ResolveOptions} [opts]
39
- * @returns {Promise<string | Buffer>}
40
- */
41
9
  export async function resolveMedia(conn, source, opts = {}) {
42
- const {
43
- mediaType = 'image',
44
- strategy = 'auto',
45
- resize: resizeOpts,
46
- silent = false,
47
- jid,
48
- } = opts
10
+ const { mediaType = 'image', strategy = 'auto', resize: resizeOpts, silent = false, jid } = opts
49
11
 
50
- // Array support: recurse with identical opts.
51
12
  if (Array.isArray(source)) {
52
13
  return Promise.all(source.map((s) => resolveMedia(conn, s, opts)))
53
14
  }
54
15
 
55
16
  const originalIsBuffer = Buffer.isBuffer(source)
56
-
57
- // Stage 1: normalise to either { url } or { buffer }.
58
17
  let buffer = null
59
18
  let url = null
60
19
 
@@ -62,12 +21,9 @@ export async function resolveMedia(conn, source, opts = {}) {
62
21
  if (HTTP_RE.test(source)) {
63
22
  const isWaUrl = WA_HOST_RE.test(source)
64
23
  if (strategy === 'url-only' && isWaUrl) return source
65
- // For HTTP URLs we always need the bytes — either because the user
66
- // asked for buffer/base64, or because we need to upload to WhatsApp.
67
24
  buffer = await fetchBuffer(source, { silent: true })
68
25
  url = source
69
26
  } else {
70
- // Treat as base64.
71
27
  buffer = Buffer.from(source, 'base64')
72
28
  }
73
29
  } else if (originalIsBuffer) {
@@ -85,12 +41,8 @@ export async function resolveMedia(conn, source, opts = {}) {
85
41
  })
86
42
  }
87
43
 
88
- // Stage 2: optional resize.
89
- if (resizeOpts && buffer) {
90
- buffer = await resize(buffer, resizeOpts)
91
- }
44
+ if (resizeOpts && buffer) buffer = await resize(buffer, resizeOpts)
92
45
 
93
- // Stage 3: format per strategy.
94
46
  switch (strategy) {
95
47
  case 'buffer':
96
48
  return buffer
@@ -98,16 +50,12 @@ export async function resolveMedia(conn, source, opts = {}) {
98
50
  return buffer.toString('base64')
99
51
  case 'url-only':
100
52
  if (url && WA_HOST_RE.test(url)) return url
101
- throw new MediaError('url-only strategy requires a wa.me URL', {
102
- code: 'LUMINA_MEDIA_URL_ONLY_FAILED',
103
- })
53
+ throw new MediaError('url-only strategy requires a wa.me URL', { code: 'LUMINA_MEDIA_URL_ONLY_FAILED' })
104
54
  case 'upload':
105
55
  return uploadToWhatsApp(conn, buffer, mediaType, { jid })
106
56
  case 'auto':
107
57
  default:
108
58
  if (url && WA_HOST_RE.test(url)) return url
109
- if (originalIsBuffer) return uploadToWhatsApp(conn, buffer, mediaType, { jid })
110
- // We have a non-WA URL — upload to WhatsApp so the consumer gets a CDN URL.
111
59
  return uploadToWhatsApp(conn, buffer, mediaType, { jid })
112
60
  }
113
61
  }
@@ -0,0 +1,124 @@
1
+ import fs from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import crypto from 'node:crypto'
4
+ import path from 'node:path'
5
+ import { MediaError } from '../errors.js'
6
+
7
+ function tmpPath(ext) {
8
+ return path.join(tmpdir(), `${crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.${ext}`)
9
+ }
10
+
11
+ function cleanup(...files) {
12
+ for (const f of files) {
13
+ try { fs.unlinkSync(f) } catch {}
14
+ }
15
+ }
16
+
17
+ async function loadFfmpeg() {
18
+ try {
19
+ const { default: ffmpeg } = await import('fluent-ffmpeg')
20
+ return ffmpeg
21
+ } catch {
22
+ throw new MediaError('fluent-ffmpeg is not installed. Run: npm i fluent-ffmpeg', {
23
+ code: 'LUMINA_STICKER_NO_FFMPEG',
24
+ })
25
+ }
26
+ }
27
+
28
+ async function loadWebp() {
29
+ try {
30
+ const { default: webp } = await import('node-webpmux')
31
+ return webp
32
+ } catch {
33
+ throw new MediaError('node-webpmux is not installed. Run: npm i node-webpmux', {
34
+ code: 'LUMINA_STICKER_NO_WEBPMUX',
35
+ })
36
+ }
37
+ }
38
+
39
+ const FFMPEG_IMAGE_OPTS = [
40
+ '-vcodec', 'libwebp',
41
+ '-vf', "scale='min(320,iw)':min'(320,ih)':force_original_aspect_ratio=decrease,fps=15,pad=320:320:-1:-1:color=white@0.0,split[a][b];[a]palettegen=reserve_transparent=on:transparency_color=ffffff[p];[b][p]paletteuse",
42
+ ]
43
+
44
+ const FFMPEG_VIDEO_OPTS = [
45
+ '-vcodec', 'libwebp',
46
+ '-vf', "scale='min(320,iw)':min'(320,ih)':force_original_aspect_ratio=decrease,fps=15,pad=320:320:-1:-1:color=white@0.0,split[a][b];[a]palettegen=reserve_transparent=on:transparency_color=ffffff[p];[b][p]paletteuse",
47
+ '-loop', '0',
48
+ '-ss', '00:00:00',
49
+ '-t', '00:00:05',
50
+ '-preset', 'default',
51
+ '-an',
52
+ '-vsync', '0',
53
+ ]
54
+
55
+ async function convertToWebp(buffer, type) {
56
+ const ffmpeg = await loadFfmpeg()
57
+ const inExt = type === 'video' ? 'mp4' : 'jpg'
58
+ const inFile = tmpPath(inExt)
59
+ const outFile = tmpPath('webp')
60
+
61
+ fs.writeFileSync(inFile, buffer)
62
+
63
+ await new Promise((resolve, reject) => {
64
+ ffmpeg(inFile)
65
+ .on('error', reject)
66
+ .on('end', () => resolve())
67
+ .addOutputOptions(type === 'video' ? FFMPEG_VIDEO_OPTS : FFMPEG_IMAGE_OPTS)
68
+ .toFormat('webp')
69
+ .save(outFile)
70
+ })
71
+
72
+ const result = fs.readFileSync(outFile)
73
+ cleanup(inFile, outFile)
74
+ return result
75
+ }
76
+
77
+ function buildExif(metadata) {
78
+ const json = {
79
+ 'sticker-pack-id': crypto.randomBytes(32).toString('hex'),
80
+ 'sticker-pack-name': metadata.packname ?? 'Lumina',
81
+ 'sticker-pack-publisher': metadata.author ?? '',
82
+ 'emojis': metadata.categories ?? [''],
83
+ ...metadata.extra,
84
+ }
85
+
86
+ const attr = Buffer.from([
87
+ 0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00,
88
+ 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00,
89
+ 0x00, 0x00, 0x16, 0x00, 0x00, 0x00,
90
+ ])
91
+ const jsonBuf = Buffer.from(JSON.stringify(json), 'utf8')
92
+ const exif = Buffer.concat([attr, jsonBuf])
93
+ exif.writeUIntLE(jsonBuf.length, 14, 4)
94
+ return exif
95
+ }
96
+
97
+ async function injectExif(webpBuffer, metadata) {
98
+ const webp = await loadWebp()
99
+ const img = new webp.Image()
100
+ await img.load(webpBuffer)
101
+ img.exif = buildExif(metadata)
102
+ return img.save(null)
103
+ }
104
+
105
+ export async function toSticker(buffer, type = 'image', metadata = {}) {
106
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
107
+ throw new MediaError('toSticker() requires a non-empty Buffer', { code: 'LUMINA_STICKER_EMPTY' })
108
+ }
109
+ if (!['image', 'video'].includes(type)) {
110
+ throw new MediaError(`type must be image or video, got: ${type}`, { code: 'LUMINA_STICKER_BAD_TYPE' })
111
+ }
112
+
113
+ const webp = await convertToWebp(buffer, type)
114
+ return injectExif(webp, metadata)
115
+ }
116
+
117
+ export async function addExif(webpBuffer, metadata = {}) {
118
+ if (!Buffer.isBuffer(webpBuffer) || webpBuffer.length === 0) {
119
+ throw new MediaError('addExif() requires a non-empty Buffer', { code: 'LUMINA_STICKER_EMPTY' })
120
+ }
121
+ return injectExif(webpBuffer, metadata)
122
+ }
123
+
124
+ export default { toSticker, addExif }
@@ -1,37 +1,7 @@
1
- /**
2
- * @file uploader.js
3
- * @module lumina/media/uploader
4
- *
5
- * Thin wrapper around Baileys' `prepareWAMessageMedia` that uploads a buffer
6
- * (or `{ url }`) to WhatsApp and returns the resulting CDN URL.
7
- *
8
- * Bug fix vs legacy: the hardcoded `jid: '@newsletter'` (which silently
9
- * forced every media upload through the newsletter queue) is replaced by a
10
- * configurable default that the user can override via the `Connection`
11
- * options or per-call `jid`.
12
- */
13
-
14
1
  import { MediaError } from '../errors.js'
15
2
 
16
- /**
17
- * Default pre-upload JID. Use a stable bot JID that WhatsApp accepts for
18
- * media pre-uploads — historically the legacy code used `@newsletter`, but
19
- * that route is rate-limited and intended for broadcast use. The default
20
- * `s.whatsapp.net` route is more permissive and works for bot accounts.
21
- */
22
3
  export const DEFAULT_UPLOAD_JID = '62831@s.whatsapp.net'
23
4
 
24
- /**
25
- * Upload a media source to WhatsApp and return its CDN URL.
26
- *
27
- * @param {import('../client/connection.js').Connection} conn
28
- * @param {Buffer | { url: string }} source
29
- * @param {'image'|'video'|'audio'|'document'} mediaType
30
- * @param {object} [opts]
31
- * @param {string} [opts.jid=DEFAULT_UPLOAD_JID] Pre-upload JID.
32
- * @param {object} [opts.options] Extra options forwarded to `prepareWAMessageMedia`.
33
- * @returns {Promise<string>} CDN URL.
34
- */
35
5
  export async function uploadToWhatsApp(conn, source, mediaType, opts = {}) {
36
6
  if (!conn?.uploadMedia) {
37
7
  throw new MediaError('connection.uploadMedia is not available', {
@@ -1,17 +1,6 @@
1
- /**
2
- * @file video.js
3
- * @module lumina/media/video
4
1
  *
5
- * Pure-Node MP4 box parser (for duration) + ffmpeg-based thumbnail extractor.
6
2
  *
7
- * The legacy `Toolkit.getMp4Duration` was a hand-rolled ISO-BMFF box walker
8
- * that worked but had no tests and several silent-failure paths. Lumina
9
- * keeps the same approach (no native deps for duration) but factors it into
10
- * a single, documented function.
11
3
  *
12
- * `extractThumbnail` lazy-imports `fluent-ffmpeg` — users who never send
13
- * video thumbnails do not need it installed.
14
- */
15
4
 
16
5
  import { PassThrough, Readable } from 'node:stream'
17
6
 
@@ -36,19 +25,8 @@ async function loadFfmpeg() {
36
25
  return ffmpegPromise
37
26
  }
38
27
 
39
- /**
40
- * Walk the ISO-BMFF box tree of an MP4 buffer and extract the duration
41
- * (in seconds) from the first `mvhd` box found inside `moov`.
42
28
  *
43
- * Returns 0 when the buffer is invalid, truncated, or lacks a `moov`/`mvhd`
44
- * box — `silent: true` (default) preserves the legacy swallow-on-failure
45
- * behaviour so that video uploads do not hard-fail when metadata is missing.
46
29
  *
47
- * @param {Buffer} buffer
48
- * @param {object} [opts]
49
- * @param {boolean} [opts.silent=true]
50
- * @returns {number} Duration in seconds (0 on failure).
51
- */
52
30
  export function getMp4Duration(buffer, opts = {}) {
53
31
  const { silent = true } = opts
54
32
 
@@ -109,24 +87,8 @@ export function getMp4Duration(buffer, opts = {}) {
109
87
  }
110
88
  }
111
89
 
112
- /**
113
- * @typedef {object} ThumbnailOptions
114
- * @property {number|string} [time='auto'] Seek position (seconds) or 'auto' (= 20% of duration, capped at 10s).
115
- * @property {number} [width=300]
116
- * @property {number} [height=300]
117
- * @property {'png'|'jpeg'|'webp'} [format='png']
118
- * @property {boolean} [resizeOutput=true] Pass the frame through `sharp.resize()`.
119
- * @property {boolean} [silent=true] Swallow ffmpeg errors and return empty Buffer.
120
- * @property {'buffer'|'base64'} [result='buffer']
121
- */
122
-
123
- /**
124
- * Extract a single frame from an MP4 buffer at the given timestamp.
90
+
125
91
  *
126
- * @param {Buffer} videoBuffer
127
- * @param {ThumbnailOptions} [opts]
128
- * @returns {Promise<Buffer|string>} Buffer or base64 string (per `result`).
129
- */
130
92
  export async function extractThumbnail(videoBuffer, opts = {}) {
131
93
  const {
132
94
  time,
@@ -1,15 +1,6 @@
1
- /**
2
- * @file code-tokenizer-keywords.js
3
- * @module lumina/parsers/code-tokenizer-keywords
4
1
  *
5
- * Language keyword catalog. Built ONCE at module-load time (vs the legacy
6
- * `AIRich.tokenizer` which rebuilt a `new Set(...)` on every call).
7
2
  *
8
- * Twelve languages are supported out of the box. To add a new language,
9
- * append a `Set` here — the lexer picks it up automatically.
10
- */
11
3
 
12
- /** @type {Record<string, Set<string>>} */
13
4
  export const KEYWORDS = {
14
5
  javascript: new Set([
15
6
  'break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete', 'do', 'else',
@@ -112,15 +103,12 @@ export const KEYWORDS = {
112
103
  markdown: new Set(['#', '##', '###', '####', '#####', '######']),
113
104
  }
114
105
 
115
- /** Languages where `//` introduces a line comment. */
116
106
  export const SLASH_COMMENT_LANGS = new Set([
117
107
  'javascript', 'typescript', 'java', 'golang', 'c', 'cpp', 'php', 'rust', 'css',
118
108
  ])
119
109
 
120
- /** Languages where `#` introduces a line comment. */
121
110
  export const HASH_COMMENT_LANGS = new Set(['python', 'bash', 'php', 'rust'])
122
111
 
123
- /** Languages that support block comments with `/* ... *\/`. */
124
112
  export const BLOCK_COMMENT_LANGS = new Set([
125
113
  'javascript', 'typescript', 'java', 'c', 'cpp', 'php', 'css',
126
114
  ])