@kyyinfinite/lumina 1.0.0
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/LICENSE +21 -0
- package/README.md +629 -0
- package/examples/ai-rich.js +84 -0
- package/examples/button.js +57 -0
- package/examples/carousel.js +51 -0
- package/examples/interactive.js +102 -0
- package/examples/media.js +66 -0
- package/examples/simple-bot.js +56 -0
- package/package.json +86 -0
- package/src/builders/ai-rich.js +644 -0
- package/src/builders/base.js +109 -0
- package/src/builders/button-v2.js +159 -0
- package/src/builders/button.js +398 -0
- package/src/builders/card.js +168 -0
- package/src/builders/carousel.js +122 -0
- package/src/builders/index.d.ts +1 -0
- package/src/builders/index.js +13 -0
- package/src/client/bot.js +192 -0
- package/src/client/connection.js +180 -0
- package/src/errors.js +88 -0
- package/src/index.d.ts +458 -0
- package/src/index.js +152 -0
- package/src/media/fetch.js +67 -0
- package/src/media/image.js +86 -0
- package/src/media/index.d.ts +1 -0
- package/src/media/index.js +12 -0
- package/src/media/resolver.js +115 -0
- package/src/media/uploader.js +65 -0
- package/src/media/video.js +195 -0
- package/src/parsers/code-tokenizer-keywords.js +128 -0
- package/src/parsers/code-tokenizer.js +191 -0
- package/src/parsers/index.d.ts +1 -0
- package/src/parsers/index.js +11 -0
- package/src/parsers/inline-entity.js +231 -0
- package/src/parsers/table-metadata.js +69 -0
- package/src/proto/enums.js +170 -0
- package/src/proto/index.d.ts +1 -0
- package/src/proto/index.js +13 -0
- package/src/proto/layouts.js +89 -0
- package/src/proto/primitives.js +181 -0
- package/src/proto/relay-nodes.js +55 -0
- package/src/proto/rich-response.js +144 -0
- package/src/proto/updater.js +318 -0
- package/src/services/index.d.ts +1 -0
- package/src/services/index.js +10 -0
- package/src/services/media-service.js +184 -0
- package/src/services/message-service.js +288 -0
- package/src/services/proto-service.js +90 -0
- package/src/utils/id.js +42 -0
- package/src/utils/logger.js +65 -0
- package/src/utils/mime.js +104 -0
- package/src/utils/promise.js +52 -0
- package/src/utils/validator.js +129 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file updater.js
|
|
3
|
+
* @module lumina/proto/updater
|
|
4
|
+
*
|
|
5
|
+
* ProtoUpdater — automatic WAProto maintenance utility.
|
|
6
|
+
*
|
|
7
|
+
* WhatsApp constantly ships new protobuf definitions to its Web client, and
|
|
8
|
+
* the community `@whiskeysockets/baileys` package vendors a snapshot of those
|
|
9
|
+
* definitions as `WAProto/`. Lumina ships a small utility that:
|
|
10
|
+
*
|
|
11
|
+
* 1. Backs up the current WAProto tree before any mutation.
|
|
12
|
+
* 2. Transforms the generated CommonJS files into ESM (`require` → `import`,
|
|
13
|
+
* `module.exports` → `export`).
|
|
14
|
+
* 3. Fixes known upstream issues (e.g. the `HistorySyncType` enum that the
|
|
15
|
+
* `pbjs` generator sometimes emits with the wrong numeric tag).
|
|
16
|
+
* 4. Validates the result by `import()`-ing each module file.
|
|
17
|
+
* 5. Rolls back automatically on validation failure (configurable).
|
|
18
|
+
*
|
|
19
|
+
* The updater is opt-in: it never runs at import time. Users invoke it from
|
|
20
|
+
* the CLI or programmatically when they want to refresh the proto bundle.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { promises as fs } from 'node:fs'
|
|
24
|
+
import path from 'node:path'
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
26
|
+
|
|
27
|
+
import { ProtocolError } from '../errors.js'
|
|
28
|
+
import { sha256 } from '../utils/id.js'
|
|
29
|
+
import { createLogger } from '../utils/logger.js'
|
|
30
|
+
|
|
31
|
+
/** @typedef {import('../utils/logger.js').Logger} Logger */
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Known WAProto fixes. Each entry: { file: RegExp, apply: (content: string) => string }.
|
|
35
|
+
* Keeping these in one place makes them trivial to audit & extend.
|
|
36
|
+
*/
|
|
37
|
+
const KNOWN_FIXES = [
|
|
38
|
+
{
|
|
39
|
+
name: 'history-sync-type-enum',
|
|
40
|
+
file: /HistorySync\.js$/,
|
|
41
|
+
apply: (c) =>
|
|
42
|
+
// Ensure `HistorySyncType.INITIAL_BOOTSTRAP = 0` exists — some pbjs runs
|
|
43
|
+
// emit it as `= 1` which breaks downstream comparison logic.
|
|
44
|
+
c.replace(
|
|
45
|
+
/(HistorySyncType\s*=\s*\{)[\s\S]*?\}/,
|
|
46
|
+
(m) =>
|
|
47
|
+
m
|
|
48
|
+
.replace(/INITIAL_BOOTSTRAP\s*:\s*[1-9]\d*/, 'INITIAL_BOOTSTRAP: 0')
|
|
49
|
+
.replace(/INITIAL_BOOTSTRAP\s*=\s*[1-9]\d*/, 'INITIAL_BOOTSTRAP = 0'),
|
|
50
|
+
),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'recent-messages-enum',
|
|
54
|
+
file: /HistorySync\.js$/,
|
|
55
|
+
apply: (c) =>
|
|
56
|
+
c.replace(
|
|
57
|
+
/(RecentMessagesWeightInheritance\s*=\s*\{)[\s\S]*?\}/,
|
|
58
|
+
(m) => m.replace(/CHRONOLOGICAL\s*:\s*0/, 'CHRONOLOGICAL: 1'),
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
/** Pattern for CommonJS `require('...')` — used by CJS→ESM transform. */
|
|
64
|
+
const RE_REQUIRE = /require\(\s*(['"])([^'"]+)\1\s*\)/g
|
|
65
|
+
|
|
66
|
+
/** Pattern for `module.exports = ...` / `exports.x = ...`. */
|
|
67
|
+
const RE_MODULE_EXPORTS_ASSIGN = /module\.exports\s*=\s*/g
|
|
68
|
+
const RE_EXPORTS_DOT = /exports\.(\w+)\s*=\s*/g
|
|
69
|
+
|
|
70
|
+
/** Pattern for `const x = require('...')` — convert to `import`. */
|
|
71
|
+
const RE_CONST_REQUIRE =
|
|
72
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*require\(\s*(['"])([^'"]+)\2\s*\)/g
|
|
73
|
+
|
|
74
|
+
/** Pattern for destructured `const { a, b } = require('...')`. */
|
|
75
|
+
const RE_DESTRUCT_REQUIRE =
|
|
76
|
+
/(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*(['"])([^'"]+)\2\s*\)/g
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convert a CommonJS module's source to ESM.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} src Original CommonJS source.
|
|
82
|
+
* @returns {string} ESM source.
|
|
83
|
+
*/
|
|
84
|
+
export function transformToESM(src) {
|
|
85
|
+
let out = src
|
|
86
|
+
// 1. const X = require('mod') → import X from 'mod'
|
|
87
|
+
out = out.replace(RE_CONST_REQUIRE, (_, name, _q, mod) => `import ${name} from '${mod}'`)
|
|
88
|
+
// 2. const { a, b } = require('mod') → import { a, b } from 'mod'
|
|
89
|
+
out = out.replace(RE_DESTRUCT_REQUIRE, (_, names, _q, mod) => {
|
|
90
|
+
const cleaned = names
|
|
91
|
+
.split(',')
|
|
92
|
+
.map((n) => n.trim())
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.map((n) => (n.includes(':') ? n : n))
|
|
95
|
+
.join(', ')
|
|
96
|
+
return `import { ${cleaned} } from '${mod}'`
|
|
97
|
+
})
|
|
98
|
+
// 3. Bare require('mod') → import 'mod'
|
|
99
|
+
out = out.replace(RE_REQUIRE, (_, _q, mod) => `import '${mod}'`)
|
|
100
|
+
// 4. module.exports = X → export default X
|
|
101
|
+
out = out.replace(RE_MODULE_EXPORTS_ASSIGN, 'export default ')
|
|
102
|
+
// 5. exports.x = ... → export const x = ...
|
|
103
|
+
out = out.replace(RE_EXPORTS_DOT, (_, name) => `export const ${name} = `)
|
|
104
|
+
// 6. 'use strict' is implicit in ESM — drop it.
|
|
105
|
+
out = out.replace(/^['"]use strict['"];?\s*\n/m, '')
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Apply every known WAProto fix to a single file's content.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} filename Absolute path of the file being patched.
|
|
113
|
+
* @param {string} content Current file content.
|
|
114
|
+
* @returns {{ content: string, applied: string[] }} New content + list of fix names that fired.
|
|
115
|
+
*/
|
|
116
|
+
export function applyKnownFixes(filename, content) {
|
|
117
|
+
const applied = []
|
|
118
|
+
let next = content
|
|
119
|
+
for (const fix of KNOWN_FIXES) {
|
|
120
|
+
if (fix.file.test(filename)) {
|
|
121
|
+
const before = next
|
|
122
|
+
next = fix.apply(next)
|
|
123
|
+
if (next !== before) applied.push(fix.name)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { content: next, applied }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* ProtoUpdater — high-level orchestration.
|
|
131
|
+
*/
|
|
132
|
+
export class ProtoUpdater {
|
|
133
|
+
/** @param {object} [opts] */
|
|
134
|
+
constructor(opts = {}) {
|
|
135
|
+
this.protoPath = path.resolve(opts.protoPath ?? this.#defaultProtoPath())
|
|
136
|
+
this.backupDir = path.resolve(opts.backupDir ?? path.join(this.protoPath, '..', 'WAProto.backups'))
|
|
137
|
+
this.logger = (opts.logger ?? createLogger({ level: 'warn' })).child('proto-updater')
|
|
138
|
+
/** @type {Array<{ id: string, timestamp: number, path: string, hash: string }>} */
|
|
139
|
+
this.history = []
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Resolve the default WAProto directory inside the installed Baileys package. */
|
|
143
|
+
#defaultProtoPath() {
|
|
144
|
+
try {
|
|
145
|
+
const baileysPkg = require.resolve('@whiskeysockets/baileys/package.json', {
|
|
146
|
+
paths: [process.cwd()],
|
|
147
|
+
})
|
|
148
|
+
return path.join(path.dirname(baileysPkg), 'WAProto')
|
|
149
|
+
} catch {
|
|
150
|
+
// Fallback: assume CWD/WAProto
|
|
151
|
+
return path.join(process.cwd(), 'WAProto')
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Walk a directory recursively, returning every `.js` file path.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} dir
|
|
159
|
+
* @returns {Promise<string[]>}
|
|
160
|
+
*/
|
|
161
|
+
async #listJsFiles(dir) {
|
|
162
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
163
|
+
const out = []
|
|
164
|
+
for (const e of entries) {
|
|
165
|
+
const full = path.join(dir, e.name)
|
|
166
|
+
if (e.isDirectory()) out.push(...(await this.#listJsFiles(full)))
|
|
167
|
+
else if (e.isFile() && e.name.endsWith('.js')) out.push(full)
|
|
168
|
+
}
|
|
169
|
+
return out
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a timestamped backup of the current WAProto tree.
|
|
174
|
+
*
|
|
175
|
+
* @returns {Promise<{ id: string, timestamp: number, path: string, hash: string }>}
|
|
176
|
+
*/
|
|
177
|
+
async backup() {
|
|
178
|
+
await fs.mkdir(this.backupDir, { recursive: true })
|
|
179
|
+
const timestamp = Date.now()
|
|
180
|
+
const id = `backup-${timestamp}`
|
|
181
|
+
const dest = path.join(this.backupDir, id)
|
|
182
|
+
await fs.cp(this.protoPath, dest, { recursive: true })
|
|
183
|
+
|
|
184
|
+
const hash = await this.#hashTree(dest)
|
|
185
|
+
const record = { id, timestamp, path: dest, hash }
|
|
186
|
+
this.history.push(record)
|
|
187
|
+
this.logger.info(`backup created: ${id} (sha256=${hash.slice(0, 12)}…)`)
|
|
188
|
+
return record
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Recursively hash every `.js` file in a directory. Used to detect
|
|
193
|
+
* whether a transform actually changed anything.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} dir
|
|
196
|
+
* @returns {Promise<string>}
|
|
197
|
+
*/
|
|
198
|
+
async #hashTree(dir) {
|
|
199
|
+
const files = (await this.#listJsFiles(dir)).sort()
|
|
200
|
+
const hashes = await Promise.all(
|
|
201
|
+
files.map(async (f) => {
|
|
202
|
+
const c = await fs.readFile(f, 'utf8')
|
|
203
|
+
return `${path.relative(dir, f)}:${sha256(c)}`
|
|
204
|
+
}),
|
|
205
|
+
)
|
|
206
|
+
return sha256(hashes.join('\n'))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Restore a specific backup by id.
|
|
211
|
+
*
|
|
212
|
+
* @param {string} id
|
|
213
|
+
*/
|
|
214
|
+
async restore(id) {
|
|
215
|
+
const record = this.history.find((h) => h.id === id)
|
|
216
|
+
if (!record) {
|
|
217
|
+
throw new ProtocolError(`backup not found: ${id}`, { code: 'LUMINA_PROTOCOL_BACKUP_NOT_FOUND' })
|
|
218
|
+
}
|
|
219
|
+
await fs.rm(this.protoPath, { recursive: true, force: true })
|
|
220
|
+
await fs.cp(record.path, this.protoPath, { recursive: true })
|
|
221
|
+
this.logger.warn(`restored backup: ${id}`)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Rollback to the most recent successful backup.
|
|
226
|
+
*/
|
|
227
|
+
async rollback() {
|
|
228
|
+
if (this.history.length === 0) {
|
|
229
|
+
throw new ProtocolError('no backup available for rollback', {
|
|
230
|
+
code: 'LUMINA_PROTOCOL_NO_BACKUP',
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
const last = this.history[this.history.length - 1]
|
|
234
|
+
await this.restore(last.id)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validate the current WAProto tree by importing every `.js` file.
|
|
239
|
+
*
|
|
240
|
+
* @returns {Promise<{ ok: boolean, errors: Array<{ file: string, message: string }> }>}
|
|
241
|
+
*/
|
|
242
|
+
async validate() {
|
|
243
|
+
const files = await this.#listJsFiles(this.protoPath)
|
|
244
|
+
const errors = []
|
|
245
|
+
await Promise.all(
|
|
246
|
+
files.map(async (f) => {
|
|
247
|
+
try {
|
|
248
|
+
await import(pathToFileURL(f).href)
|
|
249
|
+
} catch (err) {
|
|
250
|
+
errors.push({ file: path.relative(this.protoPath, f), message: err?.message ?? String(err) })
|
|
251
|
+
}
|
|
252
|
+
}),
|
|
253
|
+
)
|
|
254
|
+
return { ok: errors.length === 0, errors }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Run the full update pipeline: backup → transform → fix → validate → commit/rollback.
|
|
259
|
+
*
|
|
260
|
+
* @param {object} [opts]
|
|
261
|
+
* @param {boolean} [opts.autoRollback=true] Rollback automatically on validation failure.
|
|
262
|
+
* @param {boolean} [opts.dryRun=false] Don't write to disk — return a diff summary.
|
|
263
|
+
* @returns {Promise<{ success: boolean, fromHash: string, toHash: string, backupId: string|null, appliedFixes: string[], errors: any[] }>}
|
|
264
|
+
*/
|
|
265
|
+
async update(opts = {}) {
|
|
266
|
+
const autoRollback = opts.autoRollback ?? true
|
|
267
|
+
const dryRun = opts.dryRun ?? false
|
|
268
|
+
|
|
269
|
+
const fromHash = await this.#hashTree(this.protoPath)
|
|
270
|
+
const files = await this.#listJsFiles(this.protoPath)
|
|
271
|
+
|
|
272
|
+
let backupId = null
|
|
273
|
+
if (!dryRun) {
|
|
274
|
+
const backup = await this.backup()
|
|
275
|
+
backupId = backup.id
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const appliedFixes = new Set()
|
|
279
|
+
const errors = []
|
|
280
|
+
|
|
281
|
+
for (const file of files) {
|
|
282
|
+
try {
|
|
283
|
+
const original = await fs.readFile(file, 'utf8')
|
|
284
|
+
const transformed = transformToESM(original)
|
|
285
|
+
const fixed = applyKnownFixes(file, transformed)
|
|
286
|
+
fixed.applied.forEach((n) => appliedFixes.add(n))
|
|
287
|
+
|
|
288
|
+
if (!dryRun && fixed.content !== original) {
|
|
289
|
+
await fs.writeFile(file, fixed.content, 'utf8')
|
|
290
|
+
}
|
|
291
|
+
} catch (err) {
|
|
292
|
+
errors.push({ file: path.relative(this.protoPath, file), message: err?.message ?? String(err) })
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const toHash = dryRun ? fromHash : await this.#hashTree(this.protoPath)
|
|
297
|
+
|
|
298
|
+
if (dryRun) {
|
|
299
|
+
this.logger.info(`dry-run complete: ${appliedFixes.size} fixes would apply, ${errors.length} errors`)
|
|
300
|
+
return { success: errors.length === 0, fromHash, toHash: fromHash, backupId: null, appliedFixes: [...appliedFixes], errors }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const validation = await this.validate()
|
|
304
|
+
if (!validation.ok) {
|
|
305
|
+
errors.push(...validation.errors)
|
|
306
|
+
if (autoRollback && backupId) {
|
|
307
|
+
this.logger.warn(`validation failed (${validation.errors.length} errors) — rolling back`)
|
|
308
|
+
await this.rollback()
|
|
309
|
+
return { success: false, fromHash, toHash: fromHash, backupId, appliedFixes: [...appliedFixes], errors }
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this.logger.info(`update complete: ${appliedFixes.size} fixes applied, hash ${fromHash.slice(0, 8)} → ${toHash.slice(0, 8)}`)
|
|
314
|
+
return { success: errors.length === 0, fromHash, toHash, backupId, appliedFixes: [...appliedFixes], errors }
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export default ProtoUpdater
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../index.d.ts'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file services/index.js
|
|
3
|
+
* @module lumina/services
|
|
4
|
+
*
|
|
5
|
+
* Barrel re-export for the services layer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { MediaService } from './media-service.js'
|
|
9
|
+
export { ProtoService } from './proto-service.js'
|
|
10
|
+
export { MessageService } from './message-service.js'
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file media-service.js
|
|
3
|
+
* @module lumina/services/media-service
|
|
4
|
+
*
|
|
5
|
+
* High-level orchestrator for every media operation. Composes the lower-level
|
|
6
|
+
* `media/*` modules with a Connection instance and (optionally) a small LRU
|
|
7
|
+
* cache for repeated URL fetches.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { fetchBuffer } from '../media/fetch.js'
|
|
11
|
+
import { resize, thumbnail } from '../media/image.js'
|
|
12
|
+
import { getMp4Duration, extractThumbnail } from '../media/video.js'
|
|
13
|
+
import { uploadToWhatsApp } from '../media/uploader.js'
|
|
14
|
+
import { resolveMedia } from '../media/resolver.js'
|
|
15
|
+
import { sniffMime, mimeToCategory } from '../utils/mime.js'
|
|
16
|
+
|
|
17
|
+
/** @typedef {import('../client/connection.js').Connection} Connection */
|
|
18
|
+
/** @typedef {import('../utils/logger.js').Logger} Logger */
|
|
19
|
+
|
|
20
|
+
const LRU_MAX = 128
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} MediaServiceOptions
|
|
24
|
+
* @property {boolean} [cache=true] Enable URL→buffer LRU cache.
|
|
25
|
+
* @property {number} [cacheMax=128] Max cache entries.
|
|
26
|
+
* @property {Logger} [logger]
|
|
27
|
+
* @property {string} [uploadJid]
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export class MediaService {
|
|
31
|
+
/**
|
|
32
|
+
* @param {Connection} conn
|
|
33
|
+
* @param {MediaServiceOptions} [opts]
|
|
34
|
+
*/
|
|
35
|
+
constructor(conn, opts = {}) {
|
|
36
|
+
if (!conn) throw new Error('Connection is required for MediaService')
|
|
37
|
+
this.conn = conn
|
|
38
|
+
this.logger = opts.logger?.child('media')
|
|
39
|
+
this.uploadJid = opts.uploadJid
|
|
40
|
+
this.#cacheEnabled = opts.cache ?? true
|
|
41
|
+
this.#cacheMax = opts.cacheMax ?? LRU_MAX
|
|
42
|
+
/** @type {Map<string, { buf: Buffer, ts: number }>} */
|
|
43
|
+
this.#cache = new Map()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @type {boolean} */
|
|
47
|
+
#cacheEnabled
|
|
48
|
+
/** @type {number} */
|
|
49
|
+
#cacheMax
|
|
50
|
+
/** @type {Map<string, { buf: Buffer, ts: number }>} */
|
|
51
|
+
#cache
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} url
|
|
55
|
+
* @returns {Buffer | undefined}
|
|
56
|
+
*/
|
|
57
|
+
#cacheGet(url) {
|
|
58
|
+
if (!this.#cacheEnabled) return undefined
|
|
59
|
+
const entry = this.#cache.get(url)
|
|
60
|
+
if (!entry) return undefined
|
|
61
|
+
// Refresh LRU order.
|
|
62
|
+
this.#cache.delete(url)
|
|
63
|
+
this.#cache.set(url, entry)
|
|
64
|
+
return entry.buf
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} url
|
|
69
|
+
* @param {Buffer} buf
|
|
70
|
+
*/
|
|
71
|
+
#cacheSet(url, buf) {
|
|
72
|
+
if (!this.#cacheEnabled) return
|
|
73
|
+
if (this.#cache.size >= this.#cacheMax) {
|
|
74
|
+
const oldest = this.#cache.keys().next().value
|
|
75
|
+
this.#cache.delete(oldest)
|
|
76
|
+
}
|
|
77
|
+
this.#cache.set(url, { buf, ts: Date.now() })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fetch a URL into a Buffer. Uses the cache when enabled.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} url
|
|
84
|
+
* @param {object} [opts] Forwarded to fetchBuffer.
|
|
85
|
+
* @returns {Promise<Buffer>}
|
|
86
|
+
*/
|
|
87
|
+
async fetch(url, opts) {
|
|
88
|
+
const cached = this.#cacheGet(url)
|
|
89
|
+
if (cached) return cached
|
|
90
|
+
const buf = await fetchBuffer(url, opts)
|
|
91
|
+
if (buf.length) this.#cacheSet(url, buf)
|
|
92
|
+
return buf
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resize an image buffer.
|
|
97
|
+
*
|
|
98
|
+
* @param {Buffer} buf
|
|
99
|
+
* @param {object} opts
|
|
100
|
+
* @returns {Promise<Buffer>}
|
|
101
|
+
*/
|
|
102
|
+
async resize(buf, opts) {
|
|
103
|
+
return resize(buf, opts)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate a square thumbnail.
|
|
108
|
+
*
|
|
109
|
+
* @param {Buffer} buf
|
|
110
|
+
* @param {number} [size=300]
|
|
111
|
+
* @param {'png'|'jpeg'|'webp'} [format]
|
|
112
|
+
* @returns {Promise<Buffer>}
|
|
113
|
+
*/
|
|
114
|
+
async thumbnail(buf, size, format) {
|
|
115
|
+
return thumbnail(buf, size, format)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get MP4 duration in seconds. Returns 0 on failure (silent default).
|
|
120
|
+
*
|
|
121
|
+
* @param {Buffer} buf
|
|
122
|
+
* @returns {number}
|
|
123
|
+
*/
|
|
124
|
+
duration(buf) {
|
|
125
|
+
return getMp4Duration(buf)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Extract a thumbnail frame from a video buffer.
|
|
130
|
+
*
|
|
131
|
+
* @param {Buffer} buf
|
|
132
|
+
* @param {object} [opts]
|
|
133
|
+
* @returns {Promise<Buffer|string>}
|
|
134
|
+
*/
|
|
135
|
+
async videoThumbnail(buf, opts) {
|
|
136
|
+
return extractThumbnail(buf, opts)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Upload a buffer to WhatsApp.
|
|
141
|
+
*
|
|
142
|
+
* @param {Buffer | { url: string }} source
|
|
143
|
+
* @param {'image'|'video'|'audio'|'document'} mediaType
|
|
144
|
+
* @param {object} [opts]
|
|
145
|
+
* @returns {Promise<string>}
|
|
146
|
+
*/
|
|
147
|
+
async upload(source, mediaType, opts) {
|
|
148
|
+
return uploadToWhatsApp(this.conn, source, mediaType, { jid: this.uploadJid, ...opts })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resolve a media source (URL | base64 | Buffer | array) per strategy.
|
|
153
|
+
*
|
|
154
|
+
* @param {string | Buffer | Array} source
|
|
155
|
+
* @param {object} [opts]
|
|
156
|
+
* @returns {Promise<string | Buffer | Array<string | Buffer>>}
|
|
157
|
+
*/
|
|
158
|
+
async resolve(source, opts) {
|
|
159
|
+
return resolveMedia(this.conn, source, opts)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Convenience: sniff MIME from a buffer.
|
|
164
|
+
*
|
|
165
|
+
* @param {Buffer} buf
|
|
166
|
+
* @param {string} [filename]
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
sniff(buf, filename) {
|
|
170
|
+
return sniffMime(buf, filename)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Convenience: derive WA media category from MIME.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} mime
|
|
177
|
+
* @returns {'image'|'video'|'audio'|'document'}
|
|
178
|
+
*/
|
|
179
|
+
category(mime) {
|
|
180
|
+
return mimeToCategory(mime)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export default MediaService
|