@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,109 @@
1
+ /**
2
+ * @file builders/base.js
3
+ * @module lumina/builders/base
4
+ *
5
+ * Builder-state composer. Lumina deliberately avoids a `BaseBuilder` parent
6
+ * class — inheritance forces every builder to carry every state field
7
+ * (`_title`, `_subtitle`, etc.) whether or not they need it, and locks the
8
+ * framework into a single hierarchy.
9
+ *
10
+ * Instead, we expose `applyContentFields(target)` — a mixin that attaches
11
+ * the fluent content methods (`title`, `body`, `footer`, `contextInfo`,
12
+ * `payload`) to whatever builder invokes it. A builder composes only the
13
+ * capabilities it needs.
14
+ */
15
+
16
+ import { ensurePlainObject } from '../utils/validator.js'
17
+ import { ValidationError } from '../errors.js'
18
+
19
+ /**
20
+ * Apply the shared content-mixin to a builder instance.
21
+ *
22
+ * Attaches:
23
+ * - title(t) → sets _title
24
+ * - subtitle(t) → sets _subtitle
25
+ * - body(t) → sets _body
26
+ * - footer(t) → sets _footer
27
+ * - contextInfo(o) → sets _contextInfo (validated plain object)
28
+ * - payload(o) → merges into _extraPayload (validated plain object)
29
+ *
30
+ * Each method returns `this` for chaining.
31
+ *
32
+ * @template {object} T
33
+ * @param {T} target The builder instance to mixin into.
34
+ * @param {object} [initial] Optional initial state.
35
+ * @returns {T & ContentFields}
36
+ */
37
+ export function applyContentFields(target, initial = {}) {
38
+ target._title = initial.title ?? ''
39
+ target._subtitle = initial.subtitle ?? ''
40
+ target._body = initial.body ?? ''
41
+ target._footer = initial.footer ?? ''
42
+ target._contextInfo = initial.contextInfo ?? {}
43
+ target._extraPayload = initial.extraPayload ?? {}
44
+
45
+ target.title = function (t) {
46
+ if (typeof t !== 'string') {
47
+ throw new ValidationError('title must be a string', { code: 'LUMINA_VALIDATION_TITLE' })
48
+ }
49
+ target._title = t
50
+ return target
51
+ }
52
+
53
+ target.subtitle = function (t) {
54
+ if (typeof t !== 'string') {
55
+ throw new ValidationError('subtitle must be a string', { code: 'LUMINA_VALIDATION_SUBTITLE' })
56
+ }
57
+ target._subtitle = t
58
+ return target
59
+ }
60
+
61
+ target.body = function (t) {
62
+ if (typeof t !== 'string') {
63
+ throw new ValidationError('body must be a string', { code: 'LUMINA_VALIDATION_BODY' })
64
+ }
65
+ target._body = t
66
+ return target
67
+ }
68
+
69
+ target.footer = function (t) {
70
+ if (typeof t !== 'string') {
71
+ throw new ValidationError('footer must be a string', { code: 'LUMINA_VALIDATION_FOOTER' })
72
+ }
73
+ target._footer = t
74
+ return target
75
+ }
76
+
77
+ target.contextInfo = function (o) {
78
+ ensurePlainObject(o, 'contextInfo')
79
+ target._contextInfo = o
80
+ return target
81
+ }
82
+
83
+ target.payload = function (o) {
84
+ ensurePlainObject(o, 'payload')
85
+ Object.assign(target._extraPayload, o)
86
+ return target
87
+ }
88
+
89
+ return target
90
+ }
91
+
92
+ /**
93
+ * Helper: extract the standard content fields as an envelope fragment.
94
+ *
95
+ * @param {object} target A builder that has had applyContentFields called on it.
96
+ * @returns {{ title: string, subtitle: string, body: string, footer: string, contextInfo: object, extraPayload: object }}
97
+ */
98
+ export function readContentFields(target) {
99
+ return {
100
+ title: target._title,
101
+ subtitle: target._subtitle,
102
+ body: target._body,
103
+ footer: target._footer,
104
+ contextInfo: target._contextInfo,
105
+ extraPayload: target._extraPayload,
106
+ }
107
+ }
108
+
109
+ export default { applyContentFields, readContentFields }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @file builders/button-v2.js
3
+ * @module lumina/builders/button-v2
4
+ *
5
+ * ButtonV2Builder — legacy `buttonsMessage` format. Preserved for users who
6
+ * target WhatsApp clients that do not yet support native flow.
7
+ *
8
+ * Refactored to use the same mixin-based content fields, registry-based
9
+ * button factory, and centralised interactive relay nodes as ButtonBuilder.
10
+ * The hardcoded `headerType: 6` (LOCATION_THUMBNAIL) magic number is now
11
+ * sourced from `proto/enums.HeaderType`.
12
+ */
13
+
14
+ import { applyContentFields } from './base.js'
15
+ import { coerceMediaSource, ensurePlainObject } from '../utils/validator.js'
16
+ import { createInteractiveNodes } from '../proto/relay-nodes.js'
17
+ import { HeaderType } from '../proto/enums.js'
18
+ import { uuid } from '../utils/id.js'
19
+
20
+ import { fetchBuffer } from '../media/fetch.js'
21
+ import { resize } from '../media/image.js'
22
+
23
+ /** @typedef {import('../client/connection.js').Connection} Connection */
24
+
25
+ export class ButtonV2Builder {
26
+ /** @param {Connection} conn */
27
+ constructor(conn) {
28
+ applyContentFields(this)
29
+ this.#conn = conn
30
+
31
+ /** @type {Array<object>} */
32
+ this._buttons = []
33
+ /** @type {string | Buffer | null} */
34
+ this._thumbnailSource = null
35
+ /** @type {object | null} */
36
+ this._data = null
37
+ }
38
+
39
+ /** @type {Connection} */ #conn
40
+
41
+ /**
42
+ * Add a legacy button with a display text and (optional) explicit ID.
43
+ * Auto-generates a UUID when `id` is omitted.
44
+ *
45
+ * @param {string} displayText
46
+ * @param {string} [id]
47
+ */
48
+ button(displayText, id = uuid()) {
49
+ this._buttons.push({
50
+ buttonId: id,
51
+ buttonText: { displayText },
52
+ type: 1,
53
+ })
54
+ return this
55
+ }
56
+
57
+ /**
58
+ * Push a raw, already-shaped button object.
59
+ *
60
+ * @param {object} obj
61
+ */
62
+ rawButton(obj) {
63
+ ensurePlainObject(obj, 'button')
64
+ this._buttons.push(obj)
65
+ return this
66
+ }
67
+
68
+ /**
69
+ * Set the JPEG thumbnail (used by the location-fallback path).
70
+ *
71
+ * @param {string | Buffer} source
72
+ */
73
+ thumbnail(source) {
74
+ if (!source) throw new TypeError('thumbnail source is required')
75
+ this._thumbnailSource = source
76
+ return this
77
+ }
78
+
79
+ /**
80
+ * Provide a fully-shaped media descriptor that overrides the
81
+ * location-thumbnail fallback entirely.
82
+ *
83
+ * @param {object} obj
84
+ */
85
+ media(obj) {
86
+ ensurePlainObject(obj, 'media')
87
+ this._data = obj
88
+ return this
89
+ }
90
+
91
+ /**
92
+ * Build the proto message object (without relaying).
93
+ *
94
+ * @param {string} jid
95
+ * @param {object} [opts]
96
+ * @returns {Promise<object>}
97
+ */
98
+ async build(jid, opts = {}) {
99
+ let jpegThumbnail = null
100
+ if (this._thumbnailSource) {
101
+ const buf = Buffer.isBuffer(this._thumbnailSource)
102
+ ? this._thumbnailSource
103
+ : await fetchBuffer(this._thumbnailSource, { silent: true })
104
+ if (buf.length) {
105
+ jpegThumbnail = await resize(buf, { width: 300, height: 300, fit: 'cover', format: 'png' })
106
+ }
107
+ }
108
+
109
+ const fallback =
110
+ this._data ??
111
+ {
112
+ headerType: HeaderType.LOCATION_THUMBNAIL,
113
+ locationMessage: {
114
+ degreesLatitude: 0,
115
+ degreesLongitude: 0,
116
+ name: this._title,
117
+ address: this._subtitle,
118
+ jpegThumbnail,
119
+ },
120
+ }
121
+
122
+ return this.#conn.generateMessage(
123
+ jid,
124
+ {
125
+ ...this._extraPayload,
126
+ buttonsMessage: {
127
+ contentText: this._body,
128
+ footerText: this._footer,
129
+ ...fallback,
130
+ viewOnce: true,
131
+ contextInfo: this._contextInfo,
132
+ buttons: [...this._buttons],
133
+ },
134
+ },
135
+ opts,
136
+ )
137
+ }
138
+
139
+ /**
140
+ * Build + relay.
141
+ *
142
+ * @param {string} jid
143
+ * @param {object} [opts]
144
+ */
145
+ async send(jid, opts = {}) {
146
+ if (this._buttons.length === 0) {
147
+ throw new Error('ButtonV2 requires at least one button')
148
+ }
149
+ const msg = await this.build(jid, opts)
150
+ await this.#conn.relayMessage(msg.key.remoteJid, msg.message, {
151
+ messageId: msg.key.id,
152
+ additionalNodes: opts.additionalNodes ?? createInteractiveNodes(),
153
+ ...opts,
154
+ })
155
+ return msg
156
+ }
157
+ }
158
+
159
+ export default ButtonV2Builder
@@ -0,0 +1,398 @@
1
+ /**
2
+ * @file builders/button.js
3
+ * @module lumina/builders/button
4
+ *
5
+ * ButtonBuilder — modern, chainable builder for native-flow interactive
6
+ * messages.
7
+ *
8
+ * Key improvements over the legacy `Button` class:
9
+ *
10
+ * 1. Verb-first fluent API (`title()`, `body()`, `reply()`, `url()`, …)
11
+ * instead of Java-style `setTitle()` / `addReply()`.
12
+ * 2. Single unified `media(type, source)` method replaces the 3x
13
+ * duplicated `setVideo/setImage/setDocument` of the legacy code.
14
+ * 3. Registry-based button factory: the 5 identical `addReply/addCall/
15
+ * addReminder/addCancelReminder/addAddress` methods collapse to one
16
+ * `BUTTON_TYPES` table + one factory.
17
+ * 4. Callback-based selection API (no more mutable `_currentSelectionIndex`
18
+ * / `_currentSectionIndex` state juggling).
19
+ * 5. Interactive relay nodes sourced from `proto/relay-nodes.js` (was
20
+ * duplicated 3x in the legacy code).
21
+ * 6. Dead `paramsList` static field removed entirely.
22
+ */
23
+
24
+ import { applyContentFields } from './base.js'
25
+ import { coerceMediaSource, ensurePlainObject } from '../utils/validator.js'
26
+ import { createInteractiveNodes } from '../proto/relay-nodes.js'
27
+
28
+ /** @typedef {import('../client/connection.js').Connection} Connection */
29
+ /** @typedef {import('../services/proto-service.js').ProtoService} ProtoService */
30
+ /** @typedef {import('../services/media-service.js').MediaService} MediaService */
31
+
32
+ /**
33
+ * Registry of "simple" button types — those whose `buttonParamsJson` is just
34
+ * `{ display_text, id, ...opts }`. Adding a new simple type is a one-line change.
35
+ *
36
+ * @type {Record<string, string>}
37
+ */
38
+ const SIMPLE_BUTTON_TYPES = {
39
+ reply: 'quick_reply',
40
+ call: 'cta_call',
41
+ reminder: 'cta_reminder',
42
+ cancelReminder: 'cta_cancel_reminder',
43
+ address: 'address_message',
44
+ }
45
+
46
+ /**
47
+ * Selection builder — passed to the `selection()` callback so users can
48
+ * compose sections & rows safely without mutating outer state.
49
+ */
50
+ class SelectionBuilder {
51
+ constructor() {
52
+ this._title = ''
53
+ this._sections = []
54
+ this._currentSection = null
55
+ }
56
+
57
+ /** @param {string} t */
58
+ title(t) {
59
+ this._title = t
60
+ return this
61
+ }
62
+
63
+ /**
64
+ * @param {string} title
65
+ * @param {(s: SectionBuilder) => void} builder
66
+ */
67
+ section(title, builder) {
68
+ const section = { title, highlight_label: '', rows: [] }
69
+ const sb = new SectionBuilder(section.rows)
70
+ builder(sb)
71
+ this._sections.push(section)
72
+ return this
73
+ }
74
+
75
+ /** @returns {object} proto-ready selection params */
76
+ build() {
77
+ return { title: this._title, sections: this._sections }
78
+ }
79
+ }
80
+
81
+ class SectionBuilder {
82
+ /** @param {Array} rowsRef */
83
+ constructor(rowsRef) {
84
+ this._rows = rowsRef
85
+ }
86
+
87
+ /**
88
+ * @param {string} header
89
+ * @param {string} title
90
+ * @param {string} description
91
+ * @param {string} id
92
+ */
93
+ row(header, title, description, id) {
94
+ this._rows.push({ header, title, description, id })
95
+ return this
96
+ }
97
+ }
98
+
99
+ export class ButtonBuilder {
100
+ /**
101
+ * @param {Connection} conn
102
+ * @param {ProtoService} proto
103
+ * @param {MediaService} media
104
+ */
105
+ constructor(conn, proto, media) {
106
+ applyContentFields(this)
107
+
108
+ this.#conn = conn
109
+ this.#proto = proto
110
+ this.#media = media
111
+
112
+ /** @type {Array<{ name: string, buttonParamsJson: string }>} */
113
+ this._buttons = []
114
+ /** @type {object | null} */
115
+ this._data = null
116
+ /** @type {object} */
117
+ this._params = {}
118
+ }
119
+
120
+ /** @type {Connection} */ #conn
121
+ /** @type {ProtoService} */ #proto
122
+ /** @type {MediaService} */ #media
123
+
124
+ // ─── Media ──────────────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Unified media setter. Replaces the legacy `setVideo/setImage/setDocument`
128
+ * triplicate.
129
+ *
130
+ * @param {'image'|'video'|'document'} type
131
+ * @param {string | Buffer | object} source
132
+ * @param {object} [opts] Additional fields merged into the media descriptor.
133
+ */
134
+ media(type, source, opts = {}) {
135
+ if (!['image', 'video', 'document'].includes(type)) {
136
+ throw new TypeError(`media type must be image | video | document, got: ${type}`)
137
+ }
138
+ if (!source) throw new TypeError('media source is required')
139
+ this._data = { [type]: coerceMediaSource(source).raw, ...opts }
140
+ return this
141
+ }
142
+
143
+ /** @param {string | Buffer} source */
144
+ image(source, opts) {
145
+ return this.media('image', source, opts)
146
+ }
147
+
148
+ /** @param {string | Buffer} source */
149
+ video(source, opts) {
150
+ return this.media('video', source, opts)
151
+ }
152
+
153
+ /** @param {string | Buffer} source */
154
+ document(source, opts) {
155
+ return this.media('document', source, opts)
156
+ }
157
+
158
+ // ─── Buttons ────────────────────────────────────────────────────────
159
+
160
+ /**
161
+ * Generic raw-button pusher. Use this to attach button types Lumina does
162
+ * not have a shortcut for.
163
+ *
164
+ * @param {string} name Button name as expected by WAProto.
165
+ * @param {object | string} params Object (will be JSON.stringify-ed) or pre-stringified JSON.
166
+ * @param {object} [extra] Extra top-level fields merged onto the button entry.
167
+ */
168
+ button(name, params, extra = {}) {
169
+ this._buttons.push({
170
+ name,
171
+ buttonParamsJson: typeof params === 'string' ? params : JSON.stringify(params),
172
+ ...extra,
173
+ })
174
+ return this
175
+ }
176
+
177
+ /**
178
+ * Configure `messageParamsJson` (top-level interactive message params).
179
+ *
180
+ * @param {object} obj
181
+ */
182
+ params(obj) {
183
+ ensurePlainObject(obj, 'params')
184
+ this._params = obj
185
+ return this
186
+ }
187
+
188
+ /**
189
+ * Build a "simple" button (reply / call / reminder / cancelReminder /
190
+ * address) via the SIMPLE_BUTTON_TYPES registry. Each shortcut below
191
+ * delegates here.
192
+ *
193
+ * @param {keyof typeof SIMPLE_BUTTON_TYPES} kind
194
+ * @param {string} displayText
195
+ * @param {string} [id] Auto-generated UUID when omitted.
196
+ * @param {object} [opts] Extra params merged into buttonParamsJson.
197
+ */
198
+ simpleButton(kind, displayText, id, opts = {}) {
199
+ const name = SIMPLE_BUTTON_TYPES[kind]
200
+ if (!name) throw new TypeError(`unknown simple button kind: ${kind}`)
201
+ this._buttons.push({
202
+ name,
203
+ buttonParamsJson: JSON.stringify({ display_text: displayText, id, ...opts }),
204
+ })
205
+ return this
206
+ }
207
+
208
+ /** @param {string} displayText @param {string} [id] @param {object} [opts] */
209
+ reply(displayText, id, opts) {
210
+ return this.simpleButton('reply', displayText, id, opts)
211
+ }
212
+
213
+ /** @param {string} displayText @param {string} [id] @param {object} [opts] */
214
+ call(displayText, id, opts) {
215
+ return this.simpleButton('call', displayText, id, opts)
216
+ }
217
+
218
+ /** @param {string} displayText @param {string} [id] @param {object} [opts] */
219
+ reminder(displayText, id, opts) {
220
+ return this.simpleButton('reminder', displayText, id, opts)
221
+ }
222
+
223
+ /** @param {string} displayText @param {string} [id] @param {object} [opts] */
224
+ cancelReminder(displayText, id, opts) {
225
+ return this.simpleButton('cancelReminder', displayText, id, opts)
226
+ }
227
+
228
+ /** @param {string} displayText @param {string} [id] @param {object} [opts] */
229
+ address(displayText, id, opts) {
230
+ return this.simpleButton('address', displayText, id, opts)
231
+ }
232
+
233
+ /**
234
+ * @param {string} displayText
235
+ * @param {string} url
236
+ * @param {object} [opts] { webview_interaction, ... }
237
+ */
238
+ url(displayText, url, opts = {}) {
239
+ this._buttons.push({
240
+ name: 'cta_url',
241
+ buttonParamsJson: JSON.stringify({
242
+ display_text: displayText,
243
+ url,
244
+ webview_interaction: opts.webview_interaction ?? false,
245
+ ...opts,
246
+ }),
247
+ })
248
+ return this
249
+ }
250
+
251
+ /**
252
+ * @param {string} displayText
253
+ * @param {string} copyCode
254
+ * @param {object} [opts]
255
+ */
256
+ copy(displayText, copyCode, opts = {}) {
257
+ this._buttons.push({
258
+ name: 'cta_copy',
259
+ buttonParamsJson: JSON.stringify({
260
+ display_text: displayText,
261
+ copy_code: copyCode,
262
+ ...opts,
263
+ }),
264
+ })
265
+ return this
266
+ }
267
+
268
+ /**
269
+ * Send-location button. No display text — the params object is opaque.
270
+ *
271
+ * @param {object} [opts]
272
+ */
273
+ location(opts = {}) {
274
+ this._buttons.push({
275
+ name: 'send_location',
276
+ buttonParamsJson: JSON.stringify(opts),
277
+ })
278
+ return this
279
+ }
280
+
281
+ /**
282
+ * Add a single-select button via a callback. The callback receives a
283
+ * {@link SelectionBuilder} so sections & rows can be composed without
284
+ * touching outer state.
285
+ *
286
+ * @example
287
+ * button.selection('Menu', sel => sel
288
+ * .section('Main', s => s.row('A', 'Sub A', 'desc', 'a_id').row('B', 'Sub B', 'desc', 'b_id'))
289
+ * .section('Extras', s => s.row('C', 'Sub C', 'desc', 'c_id'))
290
+ * )
291
+ *
292
+ * @param {string} title
293
+ * @param {(sel: SelectionBuilder) => void} builder
294
+ * @param {object} [extra]
295
+ */
296
+ selection(title, builder, extra = {}) {
297
+ const sel = new SelectionBuilder()
298
+ sel.title(title)
299
+ builder(sel)
300
+ const params = sel.build()
301
+ this._buttons.push({
302
+ ...extra,
303
+ name: 'single_select',
304
+ buttonParamsJson: JSON.stringify(params),
305
+ })
306
+ return this
307
+ }
308
+
309
+ /** Remove all configured buttons. */
310
+ clear() {
311
+ this._buttons = []
312
+ return this
313
+ }
314
+
315
+ // ─── Lifecycle ──────────────────────────────────────────────────────
316
+
317
+ /**
318
+ * Build a proto-ready "card" object — useful when this button is being
319
+ * embedded inside a carousel card rather than sent standalone.
320
+ *
321
+ * @returns {Promise<object>}
322
+ */
323
+ async toCard() {
324
+ let mediaPayload = {}
325
+ if (this._data) {
326
+ try {
327
+ // Use Connection.uploadMedia — Layer 4 must not touch Baileys directly.
328
+ mediaPayload = await this.#conn.uploadMedia(this._data)
329
+ } catch (err) {
330
+ // Legacy code string-matched 'Invalid media type' — too brittle.
331
+ // We surface the original error but keep the raw `_data` as a
332
+ // fallback so a builder with a non-uploadable source still produces
333
+ // something usable.
334
+ if (err?.message?.includes('Invalid media type')) {
335
+ mediaPayload = this._data
336
+ } else {
337
+ throw err
338
+ }
339
+ }
340
+ }
341
+
342
+ return {
343
+ body: { text: this._body },
344
+ footer: { text: this._footer },
345
+ header: {
346
+ title: this._title,
347
+ subtitle: this._subtitle,
348
+ hasMediaAttachment: !!this._data,
349
+ ...(this._data ? mediaPayload : {}),
350
+ },
351
+ nativeFlowMessage: {
352
+ messageParamsJson: JSON.stringify(this._params),
353
+ buttons: this._buttons,
354
+ },
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Build the full proto message object (without relaying).
360
+ *
361
+ * @param {string} jid
362
+ * @param {object} [opts]
363
+ * @returns {Promise<object>}
364
+ */
365
+ async build(jid, opts = {}) {
366
+ const card = await this.toCard()
367
+ return this.#conn.generateMessage(
368
+ jid,
369
+ {
370
+ ...this._extraPayload,
371
+ interactiveMessage: {
372
+ ...card,
373
+ contextInfo: this._contextInfo,
374
+ },
375
+ },
376
+ opts,
377
+ )
378
+ }
379
+
380
+ /**
381
+ * Build + relay via the interactive channel (with biz/native_flow nodes).
382
+ *
383
+ * @param {string} jid
384
+ * @param {object} [opts]
385
+ * @returns {Promise<object>}
386
+ */
387
+ async send(jid, opts = {}) {
388
+ const msg = await this.build(jid, opts)
389
+ await this.#conn.relayMessage(msg.key.remoteJid, msg.message, {
390
+ messageId: msg.key.id,
391
+ additionalNodes: opts.additionalNodes ?? createInteractiveNodes(),
392
+ ...opts,
393
+ })
394
+ return msg
395
+ }
396
+ }
397
+
398
+ export default ButtonBuilder