@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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +629 -0
  3. package/examples/ai-rich.js +84 -0
  4. package/examples/button.js +57 -0
  5. package/examples/carousel.js +51 -0
  6. package/examples/interactive.js +102 -0
  7. package/examples/media.js +66 -0
  8. package/examples/simple-bot.js +56 -0
  9. package/package.json +86 -0
  10. package/src/builders/ai-rich.js +644 -0
  11. package/src/builders/base.js +109 -0
  12. package/src/builders/button-v2.js +159 -0
  13. package/src/builders/button.js +398 -0
  14. package/src/builders/card.js +168 -0
  15. package/src/builders/carousel.js +122 -0
  16. package/src/builders/index.d.ts +1 -0
  17. package/src/builders/index.js +13 -0
  18. package/src/client/bot.js +192 -0
  19. package/src/client/connection.js +180 -0
  20. package/src/errors.js +88 -0
  21. package/src/index.d.ts +458 -0
  22. package/src/index.js +152 -0
  23. package/src/media/fetch.js +67 -0
  24. package/src/media/image.js +86 -0
  25. package/src/media/index.d.ts +1 -0
  26. package/src/media/index.js +12 -0
  27. package/src/media/resolver.js +115 -0
  28. package/src/media/uploader.js +65 -0
  29. package/src/media/video.js +195 -0
  30. package/src/parsers/code-tokenizer-keywords.js +128 -0
  31. package/src/parsers/code-tokenizer.js +191 -0
  32. package/src/parsers/index.d.ts +1 -0
  33. package/src/parsers/index.js +11 -0
  34. package/src/parsers/inline-entity.js +231 -0
  35. package/src/parsers/table-metadata.js +69 -0
  36. package/src/proto/enums.js +170 -0
  37. package/src/proto/index.d.ts +1 -0
  38. package/src/proto/index.js +13 -0
  39. package/src/proto/layouts.js +89 -0
  40. package/src/proto/primitives.js +181 -0
  41. package/src/proto/relay-nodes.js +55 -0
  42. package/src/proto/rich-response.js +144 -0
  43. package/src/proto/updater.js +318 -0
  44. package/src/services/index.d.ts +1 -0
  45. package/src/services/index.js +10 -0
  46. package/src/services/media-service.js +184 -0
  47. package/src/services/message-service.js +288 -0
  48. package/src/services/proto-service.js +90 -0
  49. package/src/utils/id.js +42 -0
  50. package/src/utils/logger.js +65 -0
  51. package/src/utils/mime.js +104 -0
  52. package/src/utils/promise.js +52 -0
  53. package/src/utils/validator.js +129 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @file builders/card.js
3
+ * @module lumina/builders/card
4
+ *
5
+ * CardBuilder — composes a single carousel card. The legacy `Carousel` class
6
+ * required the user to construct card objects manually (including calling
7
+ * `prepareWAMessageMedia` themselves), which leaked the proto layer.
8
+ *
9
+ * CardBuilder is structurally identical to {@link ButtonBuilder} minus the
10
+ * `send()` method — cards are not standalone messages, they're embedded
11
+ * inside a carousel.
12
+ */
13
+
14
+ import { applyContentFields } from './base.js'
15
+ import { coerceMediaSource, ensurePlainObject } from '../utils/validator.js'
16
+ import { uuid } from '../utils/id.js'
17
+
18
+ /** @typedef {import('../client/connection.js').Connection} Connection */
19
+
20
+ /**
21
+ * Registry of simple button types (same as ButtonBuilder, kept local so the
22
+ * card module stays self-contained for tree-shaking).
23
+ * @type {Record<string, string>}
24
+ */
25
+ const SIMPLE_BUTTON_TYPES = {
26
+ reply: 'quick_reply',
27
+ call: 'cta_call',
28
+ reminder: 'cta_reminder',
29
+ cancelReminder: 'cta_cancel_reminder',
30
+ address: 'address_message',
31
+ }
32
+
33
+ export class CardBuilder {
34
+ /** @param {Connection} conn */
35
+ constructor(conn) {
36
+ applyContentFields(this)
37
+ this.#conn = conn
38
+ /** @type {Array<{ name: string, buttonParamsJson: string }>} */
39
+ this._buttons = []
40
+ /** @type {object | null} */
41
+ this._data = null
42
+ /** @type {object} */
43
+ this._params = {}
44
+ }
45
+
46
+ /** @type {Connection} */ #conn
47
+
48
+ // ─── Media ──────────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * @param {'image'|'video'|'document'} type
52
+ * @param {string | Buffer | object} source
53
+ * @param {object} [opts]
54
+ */
55
+ media(type, source, opts = {}) {
56
+ if (!['image', 'video', 'document'].includes(type)) {
57
+ throw new TypeError(`media type must be image | video | document, got: ${type}`)
58
+ }
59
+ if (!source) throw new TypeError('media source is required')
60
+ this._data = { [type]: coerceMediaSource(source).raw, ...opts }
61
+ return this
62
+ }
63
+
64
+ /** @param {string | Buffer} source */
65
+ image(source, opts) {
66
+ return this.media('image', source, opts)
67
+ }
68
+
69
+ /** @param {string | Buffer} source */
70
+ video(source, opts) {
71
+ return this.media('video', source, opts)
72
+ }
73
+
74
+ /** @param {string | Buffer} source */
75
+ document(source, opts) {
76
+ return this.media('document', source, opts)
77
+ }
78
+
79
+ // ─── Buttons ────────────────────────────────────────────────────────
80
+
81
+ params(obj) {
82
+ ensurePlainObject(obj, 'params')
83
+ this._params = obj
84
+ return this
85
+ }
86
+
87
+ button(name, params, extra = {}) {
88
+ this._buttons.push({
89
+ name,
90
+ buttonParamsJson: typeof params === 'string' ? params : JSON.stringify(params),
91
+ ...extra,
92
+ })
93
+ return this
94
+ }
95
+
96
+ reply(displayText, id = uuid(), opts = {}) {
97
+ this._buttons.push({
98
+ name: SIMPLE_BUTTON_TYPES.reply,
99
+ buttonParamsJson: JSON.stringify({ display_text: displayText, id, ...opts }),
100
+ })
101
+ return this
102
+ }
103
+
104
+ url(displayText, url, opts = {}) {
105
+ this._buttons.push({
106
+ name: 'cta_url',
107
+ buttonParamsJson: JSON.stringify({
108
+ display_text: displayText,
109
+ url,
110
+ webview_interaction: opts.webview_interaction ?? false,
111
+ ...opts,
112
+ }),
113
+ })
114
+ return this
115
+ }
116
+
117
+ copy(displayText, copyCode, opts = {}) {
118
+ this._buttons.push({
119
+ name: 'cta_copy',
120
+ buttonParamsJson: JSON.stringify({
121
+ display_text: displayText,
122
+ copy_code: copyCode,
123
+ ...opts,
124
+ }),
125
+ })
126
+ return this
127
+ }
128
+
129
+ // ─── Lifecycle ──────────────────────────────────────────────────────
130
+
131
+ /**
132
+ * Build the proto-ready card object.
133
+ *
134
+ * @returns {Promise<object>}
135
+ */
136
+ async build() {
137
+ let mediaPayload = {}
138
+ if (this._data) {
139
+ try {
140
+ // Layer 4 must not touch Baileys directly — go through Connection.
141
+ mediaPayload = await this.#conn.uploadMedia(this._data)
142
+ } catch (err) {
143
+ if (err?.message?.includes('Invalid media type')) {
144
+ mediaPayload = this._data
145
+ } else {
146
+ throw err
147
+ }
148
+ }
149
+ }
150
+
151
+ return {
152
+ body: { text: this._body },
153
+ footer: { text: this._footer },
154
+ header: {
155
+ title: this._title,
156
+ subtitle: this._subtitle,
157
+ hasMediaAttachment: !!this._data,
158
+ ...(this._data ? mediaPayload : {}),
159
+ },
160
+ nativeFlowMessage: {
161
+ messageParamsJson: JSON.stringify(this._params),
162
+ buttons: this._buttons,
163
+ },
164
+ }
165
+ }
166
+ }
167
+
168
+ export default CardBuilder
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @file builders/carousel.js
3
+ * @module lumina/builders/carousel
4
+ *
5
+ * CarouselBuilder — composes a `carouselMessage` containing one or more
6
+ * cards produced by {@link CardBuilder}.
7
+ *
8
+ * Bug fix vs legacy: the original `Carousel.send()` forgot to `await`
9
+ * `build()`. Lumina's version is consistently async and awaits throughout.
10
+ */
11
+
12
+ import { applyContentFields } from './base.js'
13
+ import { createInteractiveNodes } from '../proto/relay-nodes.js'
14
+ import { CardBuilder } from './card.js'
15
+
16
+ /** @typedef {import('../client/connection.js').Connection} Connection */
17
+ /** @typedef {import('../services/proto-service.js').ProtoService} ProtoService */
18
+ /** @typedef {import('../services/media-service.js').MediaService} MediaService */
19
+
20
+ export class CarouselBuilder {
21
+ /**
22
+ * @param {Connection} conn
23
+ * @param {ProtoService} proto
24
+ * @param {MediaService} media
25
+ */
26
+ constructor(conn, proto, media) {
27
+ applyContentFields(this, { body: '', footer: '' })
28
+ this.#conn = conn
29
+ this.#proto = proto
30
+ this.#media = media
31
+ /** @type {Array<object>} raw, already-built card objects */
32
+ this._cards = []
33
+ }
34
+
35
+ /** @type {Connection} */ #conn
36
+ /** @type {ProtoService} */ #proto
37
+ /** @type {MediaService} */ #media
38
+
39
+ /**
40
+ * Create a fresh {@link CardBuilder} bound to this carousel's connection.
41
+ * User builds it inline, calls `.build()` on it, and passes the result
42
+ * back to {@link card}.
43
+ *
44
+ * @example
45
+ * const card = await carousel.newCard()
46
+ * .title('A').image('a.jpg').reply('Buy', 'buy_a').build()
47
+ * carousel.card(card)
48
+ *
49
+ * @returns {CardBuilder}
50
+ */
51
+ newCard() {
52
+ return new CardBuilder(this.#conn)
53
+ }
54
+
55
+ /**
56
+ * Append one or more pre-built cards.
57
+ *
58
+ * @param {object | object[]} card
59
+ */
60
+ card(card) {
61
+ const cards = Array.isArray(card) ? card : [card]
62
+ for (const [i, c] of cards.entries()) {
63
+ if (!c?.header?.hasMediaAttachment) {
64
+ throw new Error(`Card [${this._cards.length + i}] must include an image or video in header`)
65
+ }
66
+ }
67
+ this._cards.push(...cards)
68
+ return this
69
+ }
70
+
71
+ /**
72
+ * Variadic alias for {@link card}.
73
+ *
74
+ * @param {...object} cards
75
+ */
76
+ cards(...cards) {
77
+ return this.card(cards)
78
+ }
79
+
80
+ /**
81
+ * Build the proto message object (without relaying).
82
+ *
83
+ * @param {string} jid
84
+ * @param {object} [opts]
85
+ * @returns {Promise<object>}
86
+ */
87
+ async build(jid, opts = {}) {
88
+ return this.#conn.generateMessage(
89
+ jid,
90
+ {
91
+ ...this._extraPayload,
92
+ interactiveMessage: {
93
+ header: { hasMediaAttachment: false },
94
+ body: { text: this._body },
95
+ footer: { text: this._footer },
96
+ contextInfo: this._contextInfo,
97
+ carouselMessage: { cards: this._cards },
98
+ },
99
+ },
100
+ opts,
101
+ )
102
+ }
103
+
104
+ /**
105
+ * Build + relay.
106
+ *
107
+ * @param {string} jid
108
+ * @param {object} [opts]
109
+ * @returns {Promise<object>}
110
+ */
111
+ async send(jid, opts = {}) {
112
+ const msg = await this.build(jid, opts)
113
+ await this.#conn.relayMessage(msg.key.remoteJid, msg.message, {
114
+ messageId: msg.key.id,
115
+ additionalNodes: opts.additionalNodes ?? createInteractiveNodes(),
116
+ ...opts,
117
+ })
118
+ return msg
119
+ }
120
+ }
121
+
122
+ export default CarouselBuilder
@@ -0,0 +1 @@
1
+ export * from '../index.d.ts'
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @file builders/index.js
3
+ * @module lumina/builders
4
+ *
5
+ * Barrel re-export for the builders layer.
6
+ */
7
+
8
+ export { ButtonBuilder } from './button.js'
9
+ export { ButtonV2Builder } from './button-v2.js'
10
+ export { CarouselBuilder } from './carousel.js'
11
+ export { CardBuilder } from './card.js'
12
+ export { AIRichBuilder } from './ai-rich.js'
13
+ export { applyContentFields, readContentFields } from './base.js'
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @file client/bot.js
3
+ * @module lumina/client/bot
4
+ *
5
+ * Bot — primary entry point. Wraps a Baileys socket, exposes a fluent,
6
+ * verb-first API, and provides factory methods for every builder.
7
+ *
8
+ * const bot = new Bot(socket)
9
+ * await bot.text(jid, 'Halo')
10
+ * await bot.button().title('T').body('B').reply('OK').send(jid)
11
+ * await bot.ai().text('hi').code('js', 'console.log(1)').send(jid)
12
+ */
13
+
14
+ import { Connection } from './connection.js'
15
+ import { MediaService } from '../services/media-service.js'
16
+ import { ProtoService } from '../services/proto-service.js'
17
+ import { MessageService } from '../services/message-service.js'
18
+
19
+ import { ButtonBuilder } from '../builders/button.js'
20
+ import { ButtonV2Builder } from '../builders/button-v2.js'
21
+ import { CarouselBuilder } from '../builders/carousel.js'
22
+ import { AIRichBuilder } from '../builders/ai-rich.js'
23
+
24
+ import { extractInlineEntities, tokenizeCode, toTableMetadata } from '../parsers/index.js'
25
+ import { createLogger } from '../utils/logger.js'
26
+
27
+ /** @typedef {import('@whiskeysockets/baileys').WASocket} WASocket */
28
+ /** @typedef {import('../utils/logger.js').Logger} Logger */
29
+
30
+ /**
31
+ * @typedef {object} BotOptions
32
+ * @property {string} [uploadJid] Pre-upload JID for media uploads.
33
+ * @property {Logger} [logger] Custom logger.
34
+ * @property {boolean} [mediaCache=true] Enable MediaService URL cache.
35
+ * @property {string} [uploadJidDefault] Alias for uploadJid.
36
+ */
37
+
38
+ export class Bot {
39
+ /**
40
+ * @param {WASocket} socket
41
+ * @param {BotOptions} [opts]
42
+ */
43
+ constructor(socket, opts = {}) {
44
+ this.logger = opts.logger ?? createLogger({ level: 'warn' })
45
+
46
+ this.connection = new Connection(socket, {
47
+ uploadJid: opts.uploadJid ?? opts.uploadJidDefault,
48
+ logger: this.logger,
49
+ })
50
+
51
+ this.media = new MediaService(this.connection, {
52
+ logger: this.logger,
53
+ uploadJid: opts.uploadJid,
54
+ cache: opts.mediaCache ?? true,
55
+ })
56
+
57
+ this.proto = new ProtoService(this.connection)
58
+ this.message = new MessageService(this.connection, this.proto, this.media)
59
+
60
+ // Expose util facade as a property — bot.util.extractInlineEntities(text)
61
+ this.util = {
62
+ extractInlineEntities,
63
+ tokenizeCode,
64
+ toTableMetadata,
65
+ }
66
+ }
67
+
68
+ // ─── Basic senders ─────────────────────────────────────────────────
69
+
70
+ /** @param {string} jid @param {string} text @param {object} [opts] */
71
+ text(jid, text, opts) {
72
+ return this.message.text(jid, text, opts)
73
+ }
74
+
75
+ /** @param {string} jid @param {string | Buffer} source @param {string} [caption] @param {object} [opts] */
76
+ image(jid, source, caption, opts) {
77
+ return this.message.image(jid, source, caption, opts)
78
+ }
79
+
80
+ /** @param {string} jid @param {string | Buffer} source @param {string} [caption] @param {object} [opts] */
81
+ video(jid, source, caption, opts) {
82
+ return this.message.video(jid, source, caption, opts)
83
+ }
84
+
85
+ /** @param {string} jid @param {string | Buffer} source @param {object} [opts] */
86
+ audio(jid, source, opts) {
87
+ return this.message.audio(jid, source, opts)
88
+ }
89
+
90
+ /** @param {string} jid @param {string | Buffer} source @param {object} [opts] */
91
+ document(jid, source, opts) {
92
+ return this.message.document(jid, source, opts)
93
+ }
94
+
95
+ /** @param {string} jid @param {string | Buffer} source @param {object} [opts] */
96
+ sticker(jid, source, opts) {
97
+ return this.message.sticker(jid, source, opts)
98
+ }
99
+
100
+ /** @param {string} jid @param {Array} contacts @param {object} [opts] */
101
+ contact(jid, contacts, opts) {
102
+ return this.message.contact(jid, contacts, opts)
103
+ }
104
+
105
+ /** @param {string} jid @param {number} lat @param {number} lng @param {object} [opts] */
106
+ location(jid, lat, lng, opts) {
107
+ return this.message.location(jid, lat, lng, opts)
108
+ }
109
+
110
+ /** @param {string} jid @param {string} name @param {string[]} options @param {object} [opts] */
111
+ poll(jid, name, options, opts) {
112
+ return this.message.poll(jid, name, options, opts)
113
+ }
114
+
115
+ /** @param {string} jid @param {string} text @param {object} quoted @param {object} [opts] */
116
+ reply(jid, text, quoted, opts) {
117
+ return this.message.reply(jid, text, quoted, opts)
118
+ }
119
+
120
+ /** @param {string} jid @param {object} key @param {string} emoji @param {object} [opts] */
121
+ react(jid, key, emoji, opts) {
122
+ return this.message.react(jid, key, emoji, opts)
123
+ }
124
+
125
+ /** @param {string} jid @param {object} key */
126
+ delete(jid, key) {
127
+ return this.message.delete(jid, key)
128
+ }
129
+
130
+ /** @param {string} jid @param {object} message @param {object} [opts] */
131
+ forward(jid, message, opts) {
132
+ return this.message.forward(jid, message, opts)
133
+ }
134
+
135
+ /** @param {string} jid @param {object} message @param {object} [opts] */
136
+ copy(jid, message, opts) {
137
+ return this.message.copy(jid, message, opts)
138
+ }
139
+
140
+ /** @param {string} jid @param {object} key @param {string} newText @param {object} [opts] */
141
+ edit(jid, key, newText, opts) {
142
+ return this.message.edit(jid, key, newText, opts)
143
+ }
144
+
145
+ // ─── Builder factories ─────────────────────────────────────────────
146
+
147
+ /** @returns {ButtonBuilder} */
148
+ button() {
149
+ return new ButtonBuilder(this.connection, this.proto, this.media)
150
+ }
151
+
152
+ /** @returns {ButtonV2Builder} */
153
+ buttonV2() {
154
+ return new ButtonV2Builder(this.connection)
155
+ }
156
+
157
+ /** @returns {CarouselBuilder} */
158
+ carousel() {
159
+ return new CarouselBuilder(this.connection, this.proto, this.media)
160
+ }
161
+
162
+ /** @returns {AIRichBuilder} */
163
+ ai() {
164
+ return new AIRichBuilder(this.connection, this.proto, this.media)
165
+ }
166
+
167
+ // ─── Event bus ─────────────────────────────────────────────────────
168
+
169
+ /** @param {string} event @param {(...a: any[]) => void} handler @returns {() => void} */
170
+ on(event, handler) {
171
+ return this.connection.on(event, handler)
172
+ }
173
+
174
+ /** @param {string} event @param {(...a: any[]) => void} handler @returns {() => void} */
175
+ once(event, handler) {
176
+ return this.connection.once(event, handler)
177
+ }
178
+
179
+ /** @param {string} event @param {(...a: any[]) => void} handler */
180
+ off(event, handler) {
181
+ this.connection.off(event, handler)
182
+ }
183
+
184
+ // ─── Raw escape ────────────────────────────────────────────────────
185
+
186
+ /** @returns {WASocket} */
187
+ get raw() {
188
+ return this.connection.raw
189
+ }
190
+ }
191
+
192
+ export default Bot
@@ -0,0 +1,180 @@
1
+ /**
2
+ * @file connection.js
3
+ * @module lumina/client/connection
4
+ *
5
+ * Connection — Baileys socket adapter. This is the ONLY module in the whole
6
+ * framework that knows about Baileys' raw API surface. Every other module
7
+ * talks to Baileys through one of the four ports Connection exposes:
8
+ *
9
+ * - MediaPort → uploadMedia
10
+ * - GeneratePort → generateMessage
11
+ * - RelayPort → relayMessage
12
+ * - EventBus → on / once / off
13
+ *
14
+ * If Baileys renames `relayMessage` to `sendRaw`, this is the only file
15
+ * that needs updating.
16
+ */
17
+
18
+ import { ConnectionError } from '../errors.js'
19
+
20
+ /** @typedef {import('@whiskeysockets/baileys').WASocket} WASocket */
21
+
22
+ /**
23
+ * @typedef {object} ConnectionOptions
24
+ * @property {string} [uploadJid] Default JID used for pre-uploading media.
25
+ * @property {import('../utils/logger.js').Logger} [logger]
26
+ */
27
+
28
+ export class Connection {
29
+ /** @param {WASocket} socket */
30
+ /** @param {ConnectionOptions} [opts] */
31
+ constructor(socket, opts = {}) {
32
+ if (!socket) {
33
+ throw new ConnectionError('socket is required', { code: 'LUMINA_CONNECTION_NO_SOCKET' })
34
+ }
35
+ this.#socket = socket
36
+ this.uploadJid = opts.uploadJid
37
+ this.logger = opts.logger
38
+
39
+ // Verify the bare-minimum API surface we depend on.
40
+ const required = ['relayMessage', 'ev']
41
+ for (const m of required) {
42
+ if (typeof socket[m] === 'undefined') {
43
+ throw new ConnectionError(`socket is missing required field: ${m}`, {
44
+ code: 'LUMINA_CONNECTION_INCOMPATIBLE',
45
+ })
46
+ }
47
+ }
48
+ }
49
+
50
+ /** @type {WASocket | null} */
51
+ #socket = null
52
+
53
+ /**
54
+ * Upload media via Baileys' `prepareWAMessageMedia`. We lazy-import it so
55
+ * that the framework loads even if the user's Baileys version is older
56
+ * than the one we tested against.
57
+ *
58
+ * @param {object} media `{ image: Buffer | { url } }` etc.
59
+ * @param {object} [opts] `{ jid, ... }` forwarded to prepareWAMessageMedia.
60
+ * @returns {Promise<object>}
61
+ */
62
+ async uploadMedia(media, opts = {}) {
63
+ const { prepareWAMessageMedia } = await import('@whiskeysockets/baileys')
64
+ const jid = opts.jid ?? this.uploadJid
65
+ if (!jid) {
66
+ throw new ConnectionError('uploadJid must be set (via constructor or per-call)', {
67
+ code: 'LUMINA_CONNECTION_NO_UPLOAD_JID',
68
+ })
69
+ }
70
+ return prepareWAMessageMedia(media, {
71
+ upload: this.#socket.waUploadToServer,
72
+ jid,
73
+ ...opts,
74
+ })
75
+ }
76
+
77
+ /**
78
+ * Generate a WA message object from a content descriptor.
79
+ *
80
+ * @param {string} jid
81
+ * @param {object} content
82
+ * @param {object} [opts]
83
+ * @returns {Promise<object>}
84
+ */
85
+ async generateMessage(jid, content, opts = {}) {
86
+ const { generateWAMessageFromContent } = await import('@whiskeysockets/baileys')
87
+ return generateWAMessageFromContent(jid, content, opts)
88
+ }
89
+
90
+ /**
91
+ * Generate a poll message.
92
+ *
93
+ * @param {string} jid
94
+ * @param {object} opts { name, values, selectableCount, toJid, messageSecret }
95
+ * @returns {Promise<object>}
96
+ */
97
+ async generatePoll(jid, opts) {
98
+ const { generatePollMessage } = await import('@whiskeysockets/baileys')
99
+ return generatePollMessage(jid, opts)
100
+ }
101
+
102
+ /**
103
+ * Generate a reaction message.
104
+ *
105
+ * @param {string} jid
106
+ * @param {object} opts { key, text }
107
+ * @returns {Promise<object>}
108
+ */
109
+ async generateReaction(jid, opts) {
110
+ const { generateReactionMessage } = await import('@whiskeysockets/baileys')
111
+ return generateReactionMessage(jid, opts)
112
+ }
113
+
114
+ /**
115
+ * Relay a pre-built message via `relayMessage`. Returns the message ID.
116
+ *
117
+ * @param {string} jid
118
+ * @param {object} message
119
+ * @param {object} [opts]
120
+ * @returns {Promise<string>}
121
+ */
122
+ async relayMessage(jid, message, opts = {}) {
123
+ const messageId = opts.messageId ?? this.#socket.generateMessageTag()
124
+ await this.#socket.relayMessage(jid, message, {
125
+ messageId,
126
+ ...opts,
127
+ })
128
+ return messageId
129
+ }
130
+
131
+ /**
132
+ * Subscribe to a Baileys event. Returns an unsubscriber.
133
+ *
134
+ * @param {string} event
135
+ * @param {(...args: any[]) => void} handler
136
+ * @returns {() => void}
137
+ */
138
+ on(event, handler) {
139
+ this.#socket.ev.on(event, handler)
140
+ return () => this.#socket.ev.off(event, handler)
141
+ }
142
+
143
+ /**
144
+ * Subscribe once.
145
+ *
146
+ * @param {string} event
147
+ * @param {(...args: any[]) => void} handler
148
+ * @returns {() => void}
149
+ */
150
+ once(event, handler) {
151
+ const off = this.on(event, (...args) => {
152
+ off()
153
+ handler(...args)
154
+ })
155
+ return off
156
+ }
157
+
158
+ /**
159
+ * Unsubscribe.
160
+ *
161
+ * @param {string} event
162
+ * @param {(...args: any[]) => void} handler
163
+ */
164
+ off(event, handler) {
165
+ this.#socket.ev.off(event, handler)
166
+ }
167
+
168
+ /**
169
+ * Raw Baileys socket — escape hatch for advanced users. Use sparingly;
170
+ * any code that touches `raw` bypasses Lumina's contract and may break
171
+ * on Baileys upgrades.
172
+ *
173
+ * @returns {WASocket}
174
+ */
175
+ get raw() {
176
+ return this.#socket
177
+ }
178
+ }
179
+
180
+ export default Connection