@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,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
|