@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
@@ -1,85 +1,27 @@
1
- /**
2
- * @file primitives.js
3
- * @module lumina/proto/primitives
4
- *
5
- * Pure factory functions for every GenAI "UX primitive" used by the AI Rich
6
- * Response envelope. Each factory returns a plain object ready to be wrapped
7
- * by a layout view-model (see {@link module:proto/layouts}).
8
- *
9
- * All `__typename` strings are sourced from {@link module:proto/enums.TYPENAME}
10
- * — never hardcode them here.
11
- */
1
+ import { TYPENAME, SourceType, PromptType } from './enums.js'
12
2
 
13
- import { TYPENAME, ImagineType, SourceType, PromptType } from './enums.js'
14
-
15
- /**
16
- * Markdown-text primitive. May carry inline entities extracted by
17
- * `extractInlineEntities`.
18
- *
19
- * @param {string} text
20
- * @param {Array<object>} [inlineEntities]
21
- */
22
3
  export function markdownTextPrimitive(text, inlineEntities) {
23
4
  const primitive = { text, __typename: TYPENAME.MARKDOWN_TEXT }
24
5
  if (inlineEntities?.length) primitive.inline_entities = inlineEntities
25
6
  return primitive
26
7
  }
27
8
 
28
- /**
29
- * Code primitive.
30
- *
31
- * @param {string} language
32
- * @param {Array<{ content: string, type: string }>} unifiedBlocks
33
- */
34
9
  export function codePrimitive(language, unifiedBlocks) {
35
- return {
36
- language,
37
- code_blocks: unifiedBlocks,
38
- __typename: TYPENAME.CODE,
39
- }
10
+ return { language, code_blocks: unifiedBlocks, __typename: TYPENAME.CODE }
40
11
  }
41
12
 
42
- /**
43
- * Table primitive.
44
- *
45
- * @param {Array<object>} unifiedRows Output of `toTableMetadata().unifiedRows`.
46
- */
47
13
  export function tablePrimitive(unifiedRows) {
48
- return {
49
- rows: unifiedRows,
50
- __typename: TYPENAME.TABLE,
51
- }
14
+ return { rows: unifiedRows, __typename: TYPENAME.TABLE }
52
15
  }
53
16
 
54
- /**
55
- * Search-result (sources) primitive.
56
- *
57
- * @param {Array<object>} sources Pre-shaped source objects.
58
- */
59
17
  export function searchResultPrimitive(sources) {
60
- return {
61
- sources,
62
- __typename: TYPENAME.SEARCH_RESULT,
63
- }
18
+ return { sources, __typename: TYPENAME.SEARCH_RESULT }
64
19
  }
65
20
 
66
- /**
67
- * Reel primitive (one per reel item).
68
- *
69
- * @param {object} item Pre-shaped reel descriptor.
70
- */
71
21
  export function reelPrimitive(item) {
72
22
  return { ...item, __typename: TYPENAME.REEL }
73
23
  }
74
24
 
75
- /**
76
- * Imagine primitive — used for both images and videos (animate).
77
- *
78
- * @param {{ url: string, mime_type: string, file_length?: number, duration?: number }} media
79
- * @param {'IMAGE'|'ANIMATE'} kind
80
- * @param {string} [status='READY']
81
- * @param {string} [thumbnailBase64] Base64 JPEG thumbnail (video only).
82
- */
83
25
  export function imaginePrimitive(media, kind, status = 'READY', thumbnailBase64) {
84
26
  const primitive = {
85
27
  media,
@@ -91,39 +33,18 @@ export function imaginePrimitive(media, kind, status = 'READY', thumbnailBase64)
91
33
  return primitive
92
34
  }
93
35
 
94
- /**
95
- * Product-card primitive.
96
- *
97
- * @param {object} item Pre-shaped product descriptor (already media-resolved).
98
- */
99
36
  export function productCardPrimitive(item) {
100
37
  return { ...item, __typename: TYPENAME.PRODUCT_CARD }
101
38
  }
102
39
 
103
- /**
104
- * Post primitive.
105
- *
106
- * @param {object} p Pre-shaped post descriptor (already media-resolved).
107
- * @param {boolean} isCarousel
108
- */
109
40
  export function postPrimitive(p, isCarousel) {
110
41
  return { ...p, is_carousel: isCarousel, __typename: TYPENAME.POST }
111
42
  }
112
43
 
113
- /**
114
- * Metadata-text primitive (used for footer / tip).
115
- *
116
- * @param {string} text
117
- */
118
44
  export function metadataTextPrimitive(text) {
119
45
  return { text, __typename: TYPENAME.METADATA_TEXT }
120
46
  }
121
47
 
122
- /**
123
- * Follow-up suggestion pill primitive.
124
- *
125
- * @param {string} text
126
- */
127
48
  export function followUpSuggestionPillPrimitive(text) {
128
49
  return {
129
50
  prompt_text: text,
@@ -132,11 +53,6 @@ export function followUpSuggestionPillPrimitive(text) {
132
53
  }
133
54
  }
134
55
 
135
- /**
136
- * Helper: shape a source entry for `searchResultPrimitive`.
137
- *
138
- * @param {{ iconUrl?: string, url?: string, text?: string, subtitle?: string }} src
139
- */
140
56
  export function shapeSourceEntry({ iconUrl = '', url = '', text = '', subtitle = 'AI' } = {}) {
141
57
  return {
142
58
  source_type: SourceType.THIRD_PARTY,
@@ -147,9 +63,6 @@ export function shapeSourceEntry({ iconUrl = '', url = '', text = '', subtitle =
147
63
  }
148
64
  }
149
65
 
150
- /**
151
- * Helper: shape a single reel item for `reelPrimitive`.
152
- */
153
66
  export function shapeReelEntry(item) {
154
67
  return {
155
68
  reels_url: item.videoUrl ?? item.url ?? '',
@@ -1,26 +1,5 @@
1
- /**
2
- * @file relay-nodes.js
3
- * @module lumina/proto/relay-nodes
4
- *
5
- * Single source of truth for the `additionalNodes` block that Baileys
6
- * `relayMessage` expects when sending interactive (native-flow) messages.
7
- *
8
- * The legacy code duplicated this 12-line block three times (Button.send,
9
- * ButtonV2.send, Carousel.send). Lumina centralises it here.
10
- */
11
-
12
1
  import { NativeFlow } from './enums.js'
13
2
 
14
- /**
15
- * Build the standard biz/interactive/native_flow additional-nodes block.
16
- *
17
- * @param {object} [opts]
18
- * @param {string} [opts.outerVersion=NativeFlow.OUTER_VERSION]
19
- * @param {string} [opts.flowVersion=NativeFlow.FLOW_VERSION]
20
- * @param {string} [opts.flowName=NativeFlow.FLOW_NAME_MIXED]
21
- * @param {boolean} [opts.includeBiz=true] Whether to wrap in the `<biz>` node.
22
- * @returns {Array<object>} Array suitable for `relayMessage`'s `additionalNodes`.
23
- */
24
3
  export function createInteractiveNodes(opts = {}) {
25
4
  const outerVersion = opts.outerVersion ?? NativeFlow.OUTER_VERSION
26
5
  const flowVersion = opts.flowVersion ?? NativeFlow.FLOW_VERSION
@@ -35,19 +14,9 @@ export function createInteractiveNodes(opts = {}) {
35
14
 
36
15
  if (!includeBiz) return [interactive]
37
16
 
38
- return [
39
- {
40
- tag: NativeFlow.BIZ_TAG,
41
- attrs: {},
42
- content: [interactive],
43
- },
44
- ]
17
+ return [{ tag: NativeFlow.BIZ_TAG, attrs: {}, content: [interactive] }]
45
18
  }
46
19
 
47
- /**
48
- * Convenience: returns the same block but with `name: 'native_flow'` only
49
- * (no biz wrapper). Useful for non-business accounts.
50
- */
51
20
  export function createBareInteractiveNodes(opts = {}) {
52
21
  return createInteractiveNodes({ ...opts, includeBiz: false })
53
22
  }
@@ -1,46 +1,6 @@
1
- /**
2
- * @file rich-response.js
3
- * @module lumina/proto/rich-response
4
- *
5
- * Assembles the `botForwardedMessage.richResponseMessage` envelope used by
6
- * the AI Rich Response feature. Pure function — no socket access. The
7
- * {@link AIRichBuilder} calls this from its `build()` method.
8
- *
9
- * Replaces the legacy `AIRich.build()` method (1436–1503) which:
10
- * - Hardcoded `botJid: '0@bot'`
11
- * - Hardcoded `forwardOrigin: 4`
12
- * - Hardcoded `forwardingScore: 1`
13
- * - Hardcoded personal leftover `disclaimerText: '~ Ahmad tumbuh kembang'`
14
- * All four are now sourced from {@link module:proto/enums} / parameters.
15
- */
16
-
17
1
  import crypto from 'node:crypto'
18
-
19
2
  import { BOT_JID, ForwardOrigin } from './enums.js'
20
3
 
21
- /**
22
- * @typedef {object} RichResponseOptions
23
- * @property {string} [title] Sets `botMetadata.messageDisclaimerText`.
24
- * @property {Array<object>} sections Pre-resolved layout sections.
25
- * @property {Array<object>} [submessages=[]] Pre-resolved submessage objects.
26
- * @property {Array<object>} [richResponseSources=[]]
27
- * @property {object} [contextInfo={}]
28
- * @property {string} [footer] Appended as a metadata-text section.
29
- * @property {boolean} [forwarded=true] Attach forward metadata.
30
- * @property {boolean|object} [notification=false] Attach session-transparency metadata.
31
- * @property {boolean} [includesUnifiedResponse=true]
32
- * @property {boolean} [includesSubmessages=true]
33
- * @property {object} [quoted] Quoted-message descriptor.
34
- * @property {string} [quotedParticipant]
35
- * @property {object} [extraPayload] Merged at top-level.
36
- */
37
-
38
- /**
39
- * Assemble a `botForwardedMessage` envelope.
40
- *
41
- * @param {RichResponseOptions} opts
42
- * @returns {Promise<object>} The envelope, ready to pass to `relayMessage`.
43
- */
44
4
  export async function assembleRichResponse({
45
5
  title,
46
6
  sections,
@@ -68,11 +28,11 @@ export async function assembleRichResponse({
68
28
  const notificationMeta = notification
69
29
  ? {
70
30
  sessionTransparencyMetadata: {
71
- // User-supplied disclaimer (no personal leftovers).
72
31
  disclaimerText: typeof notification === 'object' ? notification.disclaimerText ?? '' : '',
73
- hcaId: typeof notification === 'object' && notification.hcaId
74
- ? notification.hcaId
75
- : `hca_${crypto.randomUUID()}`,
32
+ hcaId:
33
+ typeof notification === 'object' && notification.hcaId
34
+ ? notification.hcaId
35
+ : `hca_${crypto.randomUUID()}`,
76
36
  sessionTransparencyType: notification.sessionTransparencyType ?? 1,
77
37
  },
78
38
  }
@@ -91,10 +51,6 @@ export async function assembleRichResponse({
91
51
  const finalSections = footer
92
52
  ? [
93
53
  ...sections,
94
- // Footer is appended as a metadata-text primitive, wrapped in a
95
- // Single layout view-model. Built inline to avoid a circular import
96
- // with primitives/layouts — those are pure helpers, but this envelope
97
- // assembler must remain dependency-light for testability.
98
54
  {
99
55
  view_model: {
100
56
  primitive: { text: footer, __typename: 'GenAIMetadataTextPrimitive' },
@@ -123,18 +79,11 @@ export async function assembleRichResponse({
123
79
  unifiedResponse: {
124
80
  data: includesUnifiedResponse
125
81
  ? Buffer.from(
126
- JSON.stringify({
127
- response_id: crypto.randomUUID(),
128
- sections: finalSections,
129
- }),
82
+ JSON.stringify({ response_id: crypto.randomUUID(), sections: finalSections }),
130
83
  ).toString('base64')
131
84
  : '',
132
85
  },
133
- contextInfo: {
134
- ...forwardMeta,
135
- ...quotedMeta,
136
- ...contextInfo,
137
- },
86
+ contextInfo: { ...forwardMeta, ...quotedMeta, ...contextInfo },
138
87
  },
139
88
  },
140
89
  },
@@ -1,24 +1,7 @@
1
- /**
2
- * @file updater.js
3
- * @module lumina/proto/updater
4
1
  *
5
- * ProtoUpdater — automatic WAProto maintenance utility.
6
2
  *
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
3
  *
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
4
  *
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
5
 
23
6
  import { promises as fs } from 'node:fs'
24
7
  import path from 'node:path'
@@ -28,19 +11,12 @@ import { ProtocolError } from '../errors.js'
28
11
  import { sha256 } from '../utils/id.js'
29
12
  import { createLogger } from '../utils/logger.js'
30
13
 
31
- /** @typedef {import('../utils/logger.js').Logger} Logger */
32
14
 
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
15
  const KNOWN_FIXES = [
38
16
  {
39
17
  name: 'history-sync-type-enum',
40
18
  file: /HistorySync\.js$/,
41
19
  apply: (c) =>
42
- // Ensure `HistorySyncType.INITIAL_BOOTSTRAP = 0` exists — some pbjs runs
43
- // emit it as `= 1` which breaks downstream comparison logic.
44
20
  c.replace(
45
21
  /(HistorySyncType\s*=\s*\{)[\s\S]*?\}/,
46
22
  (m) =>
@@ -60,32 +36,21 @@ const KNOWN_FIXES = [
60
36
  },
61
37
  ]
62
38
 
63
- /** Pattern for CommonJS `require('...')` — used by CJS→ESM transform. */
64
39
  const RE_REQUIRE = /require\(\s*(['"])([^'"]+)\1\s*\)/g
65
40
 
66
- /** Pattern for `module.exports = ...` / `exports.x = ...`. */
67
41
  const RE_MODULE_EXPORTS_ASSIGN = /module\.exports\s*=\s*/g
68
42
  const RE_EXPORTS_DOT = /exports\.(\w+)\s*=\s*/g
69
43
 
70
- /** Pattern for `const x = require('...')` — convert to `import`. */
71
44
  const RE_CONST_REQUIRE =
72
45
  /(?:const|let|var)\s+(\w+)\s*=\s*require\(\s*(['"])([^'"]+)\2\s*\)/g
73
46
 
74
- /** Pattern for destructured `const { a, b } = require('...')`. */
75
47
  const RE_DESTRUCT_REQUIRE =
76
48
  /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*(['"])([^'"]+)\2\s*\)/g
77
49
 
78
- /**
79
- * Convert a CommonJS module's source to ESM.
80
50
  *
81
- * @param {string} src Original CommonJS source.
82
- * @returns {string} ESM source.
83
- */
84
51
  export function transformToESM(src) {
85
52
  let out = src
86
- // 1. const X = require('mod') → import X from 'mod'
87
53
  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
54
  out = out.replace(RE_DESTRUCT_REQUIRE, (_, names, _q, mod) => {
90
55
  const cleaned = names
91
56
  .split(',')
@@ -95,24 +60,14 @@ export function transformToESM(src) {
95
60
  .join(', ')
96
61
  return `import { ${cleaned} } from '${mod}'`
97
62
  })
98
- // 3. Bare require('mod') → import 'mod'
99
63
  out = out.replace(RE_REQUIRE, (_, _q, mod) => `import '${mod}'`)
100
- // 4. module.exports = X → export default X
101
64
  out = out.replace(RE_MODULE_EXPORTS_ASSIGN, 'export default ')
102
- // 5. exports.x = ... → export const x = ...
103
65
  out = out.replace(RE_EXPORTS_DOT, (_, name) => `export const ${name} = `)
104
- // 6. 'use strict' is implicit in ESM — drop it.
105
66
  out = out.replace(/^['"]use strict['"];?\s*\n/m, '')
106
67
  return out
107
68
  }
108
69
 
109
- /**
110
- * Apply every known WAProto fix to a single file's content.
111
70
  *
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
71
  export function applyKnownFixes(filename, content) {
117
72
  const applied = []
118
73
  let next = content
@@ -126,20 +81,14 @@ export function applyKnownFixes(filename, content) {
126
81
  return { content: next, applied }
127
82
  }
128
83
 
129
- /**
130
- * ProtoUpdater — high-level orchestration.
131
- */
132
84
  export class ProtoUpdater {
133
- /** @param {object} [opts] */
134
85
  constructor(opts = {}) {
135
86
  this.protoPath = path.resolve(opts.protoPath ?? this.#defaultProtoPath())
136
87
  this.backupDir = path.resolve(opts.backupDir ?? path.join(this.protoPath, '..', 'WAProto.backups'))
137
88
  this.logger = (opts.logger ?? createLogger({ level: 'warn' })).child('proto-updater')
138
- /** @type {Array<{ id: string, timestamp: number, path: string, hash: string }>} */
139
89
  this.history = []
140
90
  }
141
91
 
142
- /** Resolve the default WAProto directory inside the installed Baileys package. */
143
92
  #defaultProtoPath() {
144
93
  try {
145
94
  const baileysPkg = require.resolve('@whiskeysockets/baileys/package.json', {
@@ -147,17 +96,11 @@ export class ProtoUpdater {
147
96
  })
148
97
  return path.join(path.dirname(baileysPkg), 'WAProto')
149
98
  } catch {
150
- // Fallback: assume CWD/WAProto
151
99
  return path.join(process.cwd(), 'WAProto')
152
100
  }
153
101
  }
154
102
 
155
- /**
156
- * Walk a directory recursively, returning every `.js` file path.
157
103
  *
158
- * @param {string} dir
159
- * @returns {Promise<string[]>}
160
- */
161
104
  async #listJsFiles(dir) {
162
105
  const entries = await fs.readdir(dir, { withFileTypes: true })
163
106
  const out = []
@@ -169,11 +112,7 @@ export class ProtoUpdater {
169
112
  return out
170
113
  }
171
114
 
172
- /**
173
- * Create a timestamped backup of the current WAProto tree.
174
115
  *
175
- * @returns {Promise<{ id: string, timestamp: number, path: string, hash: string }>}
176
- */
177
116
  async backup() {
178
117
  await fs.mkdir(this.backupDir, { recursive: true })
179
118
  const timestamp = Date.now()
@@ -188,13 +127,7 @@ export class ProtoUpdater {
188
127
  return record
189
128
  }
190
129
 
191
- /**
192
- * Recursively hash every `.js` file in a directory. Used to detect
193
- * whether a transform actually changed anything.
194
130
  *
195
- * @param {string} dir
196
- * @returns {Promise<string>}
197
- */
198
131
  async #hashTree(dir) {
199
132
  const files = (await this.#listJsFiles(dir)).sort()
200
133
  const hashes = await Promise.all(
@@ -206,11 +139,7 @@ export class ProtoUpdater {
206
139
  return sha256(hashes.join('\n'))
207
140
  }
208
141
 
209
- /**
210
- * Restore a specific backup by id.
211
142
  *
212
- * @param {string} id
213
- */
214
143
  async restore(id) {
215
144
  const record = this.history.find((h) => h.id === id)
216
145
  if (!record) {
@@ -221,9 +150,6 @@ export class ProtoUpdater {
221
150
  this.logger.warn(`restored backup: ${id}`)
222
151
  }
223
152
 
224
- /**
225
- * Rollback to the most recent successful backup.
226
- */
227
153
  async rollback() {
228
154
  if (this.history.length === 0) {
229
155
  throw new ProtocolError('no backup available for rollback', {
@@ -234,11 +160,7 @@ export class ProtoUpdater {
234
160
  await this.restore(last.id)
235
161
  }
236
162
 
237
- /**
238
- * Validate the current WAProto tree by importing every `.js` file.
239
163
  *
240
- * @returns {Promise<{ ok: boolean, errors: Array<{ file: string, message: string }> }>}
241
- */
242
164
  async validate() {
243
165
  const files = await this.#listJsFiles(this.protoPath)
244
166
  const errors = []
@@ -254,14 +176,7 @@ export class ProtoUpdater {
254
176
  return { ok: errors.length === 0, errors }
255
177
  }
256
178
 
257
- /**
258
- * Run the full update pipeline: backup → transform → fix → validate → commit/rollback.
259
179
  *
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
180
  async update(opts = {}) {
266
181
  const autoRollback = opts.autoRollback ?? true
267
182
  const dryRun = opts.dryRun ?? false
@@ -1,10 +1,3 @@
1
- /**
2
- * @file services/index.js
3
- * @module lumina/services
4
- *
5
- * Barrel re-export for the services layer.
6
- */
7
-
8
1
  export { MediaService } from './media-service.js'
9
2
  export { ProtoService } from './proto-service.js'
10
3
  export { MessageService } from './message-service.js'
@@ -1,12 +1,3 @@
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
1
  import { fetchBuffer } from '../media/fetch.js'
11
2
  import { resize, thumbnail } from '../media/image.js'
12
3
  import { getMp4Duration, extractThumbnail } from '../media/video.js'
@@ -14,24 +5,9 @@ import { uploadToWhatsApp } from '../media/uploader.js'
14
5
  import { resolveMedia } from '../media/resolver.js'
15
6
  import { sniffMime, mimeToCategory } from '../utils/mime.js'
16
7
 
17
- /** @typedef {import('../client/connection.js').Connection} Connection */
18
- /** @typedef {import('../utils/logger.js').Logger} Logger */
19
-
20
8
  const LRU_MAX = 128
21
9
 
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
10
  export class MediaService {
31
- /**
32
- * @param {Connection} conn
33
- * @param {MediaServiceOptions} [opts]
34
- */
35
11
  constructor(conn, opts = {}) {
36
12
  if (!conn) throw new Error('Connection is required for MediaService')
37
13
  this.conn = conn
@@ -39,51 +15,30 @@ export class MediaService {
39
15
  this.uploadJid = opts.uploadJid
40
16
  this.#cacheEnabled = opts.cache ?? true
41
17
  this.#cacheMax = opts.cacheMax ?? LRU_MAX
42
- /** @type {Map<string, { buf: Buffer, ts: number }>} */
43
18
  this.#cache = new Map()
44
19
  }
45
20
 
46
- /** @type {boolean} */
47
21
  #cacheEnabled
48
- /** @type {number} */
49
22
  #cacheMax
50
- /** @type {Map<string, { buf: Buffer, ts: number }>} */
51
23
  #cache
52
24
 
53
- /**
54
- * @param {string} url
55
- * @returns {Buffer | undefined}
56
- */
57
25
  #cacheGet(url) {
58
26
  if (!this.#cacheEnabled) return undefined
59
27
  const entry = this.#cache.get(url)
60
28
  if (!entry) return undefined
61
- // Refresh LRU order.
62
29
  this.#cache.delete(url)
63
30
  this.#cache.set(url, entry)
64
31
  return entry.buf
65
32
  }
66
33
 
67
- /**
68
- * @param {string} url
69
- * @param {Buffer} buf
70
- */
71
34
  #cacheSet(url, buf) {
72
35
  if (!this.#cacheEnabled) return
73
36
  if (this.#cache.size >= this.#cacheMax) {
74
- const oldest = this.#cache.keys().next().value
75
- this.#cache.delete(oldest)
37
+ this.#cache.delete(this.#cache.keys().next().value)
76
38
  }
77
39
  this.#cache.set(url, { buf, ts: Date.now() })
78
40
  }
79
41
 
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
42
  async fetch(url, opts) {
88
43
  const cached = this.#cacheGet(url)
89
44
  if (cached) return cached
@@ -92,90 +47,34 @@ export class MediaService {
92
47
  return buf
93
48
  }
94
49
 
95
- /**
96
- * Resize an image buffer.
97
- *
98
- * @param {Buffer} buf
99
- * @param {object} opts
100
- * @returns {Promise<Buffer>}
101
- */
102
50
  async resize(buf, opts) {
103
51
  return resize(buf, opts)
104
52
  }
105
53
 
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
54
  async thumbnail(buf, size, format) {
115
55
  return thumbnail(buf, size, format)
116
56
  }
117
57
 
118
- /**
119
- * Get MP4 duration in seconds. Returns 0 on failure (silent default).
120
- *
121
- * @param {Buffer} buf
122
- * @returns {number}
123
- */
124
58
  duration(buf) {
125
59
  return getMp4Duration(buf)
126
60
  }
127
61
 
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
62
  async videoThumbnail(buf, opts) {
136
63
  return extractThumbnail(buf, opts)
137
64
  }
138
65
 
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
66
  async upload(source, mediaType, opts) {
148
67
  return uploadToWhatsApp(this.conn, source, mediaType, { jid: this.uploadJid, ...opts })
149
68
  }
150
69
 
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
70
  async resolve(source, opts) {
159
71
  return resolveMedia(this.conn, source, opts)
160
72
  }
161
73
 
162
- /**
163
- * Convenience: sniff MIME from a buffer.
164
- *
165
- * @param {Buffer} buf
166
- * @param {string} [filename]
167
- * @returns {string}
168
- */
169
74
  sniff(buf, filename) {
170
75
  return sniffMime(buf, filename)
171
76
  }
172
77
 
173
- /**
174
- * Convenience: derive WA media category from MIME.
175
- *
176
- * @param {string} mime
177
- * @returns {'image'|'video'|'audio'|'document'}
178
- */
179
78
  category(mime) {
180
79
  return mimeToCategory(mime)
181
80
  }