@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.
- package/package.json +1 -1
- package/src/builders/ai-rich.js +0 -165
- package/src/builders/base.js +4 -51
- package/src/builders/button-v2.js +13 -78
- package/src/builders/button.js +20 -234
- package/src/builders/card.js +9 -76
- package/src/builders/carousel.js +4 -61
- package/src/builders/index.js +1 -7
- package/src/builders/sticker.js +102 -0
- package/src/client/bot.js +28 -153
- package/src/client/connection.js +4 -111
- package/src/errors.js +0 -37
- package/src/index.d.ts +1 -28
- package/src/index.js +23 -121
- package/src/media/fetch.js +2 -33
- package/src/media/image.js +1 -41
- package/src/media/resolver.js +3 -55
- package/src/media/sticker.js +124 -0
- package/src/media/uploader.js +0 -30
- package/src/media/video.js +1 -39
- package/src/parsers/code-tokenizer-keywords.js +0 -12
- package/src/parsers/code-tokenizer.js +0 -42
- package/src/parsers/index.js +0 -7
- package/src/parsers/inline-entity.js +8 -117
- package/src/parsers/table-metadata.js +1 -35
- package/src/proto/enums.js +9 -65
- package/src/proto/layouts.js +3 -64
- package/src/proto/primitives.js +4 -91
- package/src/proto/relay-nodes.js +1 -32
- package/src/proto/rich-response.js +6 -57
- package/src/proto/updater.js +0 -85
- package/src/services/index.js +0 -7
- package/src/services/media-service.js +1 -102
- package/src/services/message-service.js +16 -158
- package/src/services/proto-service.js +3 -57
- package/src/utils/id.js +0 -25
- package/src/utils/logger.js +2 -39
- package/src/utils/mime.js +17 -73
- package/src/utils/promise.js +0 -26
- 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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
package/src/media/fetch.js
CHANGED
|
@@ -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())
|
package/src/media/image.js
CHANGED
|
@@ -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
|
}
|
package/src/media/resolver.js
CHANGED
|
@@ -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
|
-
|
|
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 }
|
package/src/media/uploader.js
CHANGED
|
@@ -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', {
|
package/src/media/video.js
CHANGED
|
@@ -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
|
])
|