@zervonex/baileys 3.0.3 → 3.0.5
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/README.md +33 -3
- package/WAProto/WAProtoCompile.js +1 -0
- package/WAProto/fetcher.js +17 -33
- package/WAProto/index.js +34 -26
- package/WAProto/parser.js +333 -312
- package/lib/Builders/AIRich.js +14 -14
- package/lib/Builders/base.js +3 -3
- package/lib/Builders/builders.js +460 -448
- package/lib/Defaults/index.js +2 -3
- package/lib/Socket/messages-recv.js +1 -1
- package/lib/Socket/messages-send.js +1 -1
- package/lib/Socket/newsletter.js +9 -7
- package/lib/Socket/socket.js +4 -3
- package/lib/Socket/zervonex-handler.js +1 -1
- package/lib/Utils/browser-utils.js +8 -4
- package/lib/Utils/chat-utils.js +1 -1
- package/lib/Utils/logger.js +2 -1
- package/lib/Utils/messages.js +91 -40
- package/lib/Utils/use-multi-file-auth-state.js +155 -207
- package/lib/Utils/validate-connection.js +19 -28
- package/lib/WABinary/generic-utils.js +43 -19
- package/lib/index.js +1 -1
- package/package.json +62 -97
package/lib/Builders/builders.js
CHANGED
|
@@ -1,449 +1,461 @@
|
|
|
1
|
-
import crypto from 'crypto'
|
|
2
|
-
import { proto } from '../../WAProto/index.js'
|
|
3
|
-
import { BOT_RENDERING_CONFIG_METADATA } from '../Defaults/index.js'
|
|
4
|
-
import { generateWAMessage, generateWAMessageFromContent, generateMessageIDV2, prepareStickerPackMessage, prepareWAMessageMedia } from '../Utils/index.js'
|
|
5
|
-
import { getBizBinaryNode } from '../WABinary/index.js'
|
|
6
|
-
import { BaseBuilder, norm, q } from './base.js'
|
|
7
|
-
|
|
8
|
-
const delay = ms => new Promise(r => setTimeout(r, ms))
|
|
9
|
-
|
|
10
|
-
const SPECIAL_FLOW = {
|
|
11
|
-
cta_url: { v: '2', name: 'cta_url' }, cta_call: { v: '2', name: 'cta_call' },
|
|
12
|
-
cta_copy: { v: '2', name: 'cta_copy' }, cta_reminder: { v: '2', name: 'cta_reminder' },
|
|
13
|
-
cta_cancel_reminder: { v: '2', name: 'cta_cancel_reminder' }, address: { v: '2', name: 'address_message' },
|
|
14
|
-
send_location: { v: '2', name: 'send_location' }, cta_open_webview: { v: '3', name: 'cta_open_webview' },
|
|
15
|
-
review_and_pay: { v: '4', name: 'review_and_pay' }, review_order: { v: '4', name: 'review_order' },
|
|
16
|
-
payment_status: { v: '4', name: 'payment_status' }, transaction_details: { v: '4', name: 'transaction_details' },
|
|
17
|
-
order_details: { v: '4', name: 'order_details' }, multi_product: { v: '4', name: 'multi_product' },
|
|
18
|
-
catalog: { v: '5', name: 'catalog_message' }, flow: { v: '5', name: 'flow' },
|
|
19
|
-
galaxy_message: { v: '9', name: 'galaxy_message' },
|
|
20
|
-
}
|
|
21
|
-
const DEFAULT_FLOW = { v: '9', name: 'mixed' }
|
|
22
|
-
|
|
23
|
-
// ─── Button ───────────────────────────────────────────────────────────────────
|
|
24
|
-
export class Button {
|
|
25
|
-
#client; #buttons = []; #header = {}; #body = ''; #footer = ''
|
|
26
|
-
#title = ''; #subtitle = ''; #params = {}; #mode = 'native'; #bloks = null
|
|
27
|
-
|
|
28
|
-
constructor(client, opts = {}) {
|
|
29
|
-
if (!client) throw new Error('Socket client is required')
|
|
30
|
-
this.#client = client
|
|
31
|
-
if (opts.mode) this.#mode = opts.mode
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
setTitle(t) { this.#title = t; return this }
|
|
35
|
-
setSubtitle(s) { this.#subtitle = s; return this }
|
|
36
|
-
setBody(t) { this.#body = t; return this }
|
|
37
|
-
setFooter(t) { this.#footer = t; return this }
|
|
38
|
-
setImage(url, opts = {}) { this.#header = { hasMediaAttachment: true, imageMessage: { url, ...opts } }; return this }
|
|
39
|
-
setVideo(url, opts = {}) { this.#header = { hasMediaAttachment: true, videoMessage: { url, ...opts } }; return this }
|
|
40
|
-
setDocument(url, fileName = 'file', opts = {}) { this.#header = { hasMediaAttachment: true, documentMessage: { url, fileName, ...opts } }; return this }
|
|
41
|
-
setLocation(lat, lon, name = '') { this.#header = { hasMediaAttachment: false, locationMessage: { degreesLatitude: lat, degreesLongitude: lon, name } }; return this }
|
|
42
|
-
setParams(p) { this.#params = p; return this }
|
|
43
|
-
setLimitedTimeOffer(exp) { this.#params.limited_time_offer = { expiration_time_ms: exp }; return this }
|
|
44
|
-
setBottomSheet(opts = {}) { this.#params.bottom_sheet = opts; return this }
|
|
45
|
-
setTapTargetConfiguration(opts = {}) { this.#params.tap_target_configuration = opts; return this }
|
|
46
|
-
|
|
47
|
-
#addNative(name, params) { this.#mode = 'native'; this.#buttons.push({ name, buttonParamsJson: JSON.stringify(params) }); return this }
|
|
48
|
-
|
|
49
|
-
reply(text, id) { return this.#addNative('quick_reply', { display_text: text, id }) }
|
|
50
|
-
url(text, url, merchantUrl) { return this.#addNative('cta_url', { display_text: text, url, merchant_url: merchantUrl ?? url }) }
|
|
51
|
-
call(text, phone) { return this.#addNative('cta_call', { display_text: text, phone_number: phone }) }
|
|
52
|
-
copy(text, code) { return this.#addNative('cta_copy', { display_text: text, copy_code: code }) }
|
|
53
|
-
openWebview(text, url, opts = {}) { return this.#addNative('cta_open_webview', { display_text: text, url, ...opts }) }
|
|
54
|
-
catalog(text, opts = {}) { return this.#addNative('catalog', { display_text: text, ...opts }) }
|
|
55
|
-
flow(text, flowId, opts = {}) { return this.#addNative('flow', { display_text: text, flow_id: flowId, flow_token: opts.token ?? crypto.randomUUID(), ...opts }) }
|
|
56
|
-
remind(text, opts = {}) { return this.#addNative('cta_reminder', { display_text: text, ...opts }) }
|
|
57
|
-
cancelReminder(text, opts = {}) { return this.#addNative('cta_cancel_reminder', { display_text: text, ...opts }) }
|
|
58
|
-
address(text, opts = {}) { return this.#addNative('address', { display_text: text, ...opts }) }
|
|
59
|
-
sendLocation(text) { return this.#addNative('send_location', { display_text: text }) }
|
|
60
|
-
reviewAndPay(text, opts = {}) { return this.#addNative('review_and_pay', { display_text: text, ...opts }) }
|
|
61
|
-
reviewOrder(text, opts = {}) { return this.#addNative('review_order', { display_text: text, ...opts }) }
|
|
62
|
-
orderDetails(text, opts = {}) { return this.#addNative('order_details', { display_text: text, ...opts }) }
|
|
63
|
-
paymentStatus(text, opts = {}) { return this.#addNative('payment_status', { display_text: text, ...opts }) }
|
|
64
|
-
transactionDetails(text, opts = {}) { return this.#addNative('transaction_details', { display_text: text, ...opts }) }
|
|
65
|
-
select(title, sections) { return this.#addNative('single_select', { title, sections }) }
|
|
66
|
-
|
|
67
|
-
legacyButton(text, id) { this.#mode = 'legacy'; this.#buttons.push({ buttonId: id, buttonText: { displayText: text }, type: 1 }); return this }
|
|
68
|
-
templateReply(text, id) { this.#mode = 'template'; this.#buttons.push({ quickReplyButton: { displayText: text, id } }); return this }
|
|
69
|
-
templateUrl(text, url) { this.#mode = 'template'; this.#buttons.push({ urlButton: { displayText: text, url } }); return this }
|
|
70
|
-
templateCall(text, phone) { this.#mode = 'template'; this.#buttons.push({ callButton: { displayText: text, phoneNumber: phone } }); return this }
|
|
71
|
-
|
|
72
|
-
setBloksWidget(components) {
|
|
73
|
-
const flat = []
|
|
74
|
-
const resolve = c => { if (!c.__id) c.__id = `c_${flat.length}`; flat.push(c); if (c.children) c.children = c.children.map(ch => { resolve(ch); return ch.__id }); if (c.$ref) c.$ref = c.$ref.__id }
|
|
75
|
-
; (Array.isArray(components) ? components : [components]).forEach(c => resolve(c))
|
|
76
|
-
this.#bloks = flat; this.#mode = 'native'; return this
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
toCard() { return { header: this.#header, body: { text: this.#body }, footer: { text: this.#footer }, nativeFlowMessage: { buttons: this.#buttons } } }
|
|
80
|
-
|
|
81
|
-
async build(jid, opts = {}) {
|
|
82
|
-
const messageId = opts.messageId ?? generateMessageIDV2()
|
|
83
|
-
const userJid = this.#client.user?.id
|
|
84
|
-
if (this.#mode === 'legacy') return generateWAMessageFromContent(jid, { buttonsMessage: { ...(Object.keys(this.#header).length ? this.#header : { contentText: this.#body }), footer: this.#footer, buttons: this.#buttons, headerType: Object.keys(this.#header).length ? 3 : 1 } }, { userJid, messageId })
|
|
85
|
-
if (this.#mode === 'template') return generateWAMessageFromContent(jid, { templateMessage: { hydratedTemplate: { hydratedContentText: this.#body, hydratedFooterText: this.#footer, hydratedButtons: this.#buttons } } }, { userJid, messageId })
|
|
86
|
-
const isSingleSelect = this.#buttons.length === 1 && this.#buttons[0].name === 'single_select'
|
|
87
|
-
if (isSingleSelect && !this.#bloks) { const params = JSON.parse(this.#buttons[0].buttonParamsJson); return generateWAMessageFromContent(jid, { listMessage: { title: this.#title, description: this.#body, footerText: this.#footer, buttonText: params.title ?? 'Select', listType: 1, sections: params.sections ?? [] } }, { userJid, messageId }) }
|
|
88
|
-
const flowInfo = this.#buttons.reduce((acc, b) => { const f = SPECIAL_FLOW[b.name]; return (!acc || (f && parseInt(f.v) > parseInt(acc.v))) ? (f ?? DEFAULT_FLOW) : acc }, null) ?? DEFAULT_FLOW
|
|
89
|
-
return generateWAMessageFromContent(jid, { interactiveMessage: { header: Object.keys(this.#header).length ? this.#header : undefined, body: { text: this.#body }, footer: { text: this.#footer }, nativeFlowMessage: { messageParamsJson: Object.keys(this.#params).length ? JSON.stringify(this.#params) : undefined, buttons: this.#bloks ? [{ name: 'galaxy_message', buttonParamsJson: JSON.stringify({ wa_flow_response_params: { title: this.#title }, components: this.#bloks }) }] : this.#buttons }, ...(this.#title ? { title: this.#title } : {}), ...(this.#subtitle ? { subtitle: this.#subtitle } : {}) } }, { userJid, messageId })
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async send(jid, opts = {}) {
|
|
93
|
-
const msg = await this.build(jid, opts)
|
|
94
|
-
const bizNode = getBizBinaryNode(msg.message)
|
|
95
|
-
await this.#client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
|
|
96
|
-
return msg
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ─── Interactive ──────────────────────────────────────────────────────────────
|
|
101
|
-
export class Interactive extends BaseBuilder {
|
|
102
|
-
static dispatchKeys = ['interactiveMessage']
|
|
103
|
-
static shortcuts = {
|
|
104
|
-
sendInteractiveMessage: sock => (jid, data, quoted) => new Interactive(sock).from(data).send(jid, q(quoted)),
|
|
105
|
-
sendProductMessage: sock => (jid, data, quoted) => new Interactive(sock).from(data, { __product: true }).send(jid, q(quoted)),
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async send(jid, opts = {}) {
|
|
109
|
-
const i = this.data
|
|
110
|
-
const userJid = this.client.user?.id
|
|
111
|
-
const upload = this.client.waUploadToServer
|
|
112
|
-
let media = null
|
|
113
|
-
if (i.thumbnail) media = await prepareWAMessageMedia({ image: { url: i.thumbnail } }, { upload })
|
|
114
|
-
else if (i.image) media = await prepareWAMessageMedia({ image: i.image }, { upload })
|
|
115
|
-
else if (i.video) media = await prepareWAMessageMedia({ video: i.video }, { upload })
|
|
116
|
-
else if (i.document) media = await prepareWAMessageMedia({ document: i.document }, { upload })
|
|
117
|
-
const bodyText = i.body?.text ?? i.title ?? ''
|
|
118
|
-
const footerText = typeof i.footer === 'string' ? i.footer : (i.footer?.text ?? '')
|
|
119
|
-
const headerTitle = typeof i.header === 'string' ? i.header : (i.header?.title ?? '')
|
|
120
|
-
let nativeFlow = null
|
|
121
|
-
if (i.buttons?.length || i.nativeFlowMessage) { const nfm = i.nativeFlowMessage ?? {}; nativeFlow = proto.Message.InteractiveMessage.NativeFlowMessage.create({ buttons: i.buttons ?? nfm.buttons ?? [], messageParamsJson: nfm.messageParamsJson ?? '' }) }
|
|
122
|
-
const headerMedia = {}
|
|
123
|
-
if (media?.imageMessage) headerMedia.imageMessage = media.imageMessage
|
|
124
|
-
if (media?.videoMessage) headerMedia.videoMessage = media.videoMessage
|
|
125
|
-
if (media?.documentMessage) headerMedia.documentMessage = media.documentMessage
|
|
126
|
-
const interactive = proto.Message.InteractiveMessage.create({ body: proto.Message.InteractiveMessage.Body.create({ text: bodyText }), footer: proto.Message.InteractiveMessage.Footer.create({ text: footerText }), header: proto.Message.InteractiveMessage.Header.create({ title: headerTitle, hasMediaAttachment: !!media, ...headerMedia }), ...(nativeFlow ? { nativeFlowMessage: nativeFlow } : {}) })
|
|
127
|
-
if (i.contextInfo) interactive.contextInfo = i.contextInfo
|
|
128
|
-
const msg = await generateWAMessageFromContent(jid, { interactiveMessage: interactive }, { userJid, quoted: opts.quoted })
|
|
129
|
-
const bizNode = getBizBinaryNode(msg.message)
|
|
130
|
-
await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
|
|
131
|
-
return msg
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ─── Carousel ─────────────────────────────────────────────────────────────────
|
|
136
|
-
export class Carousel {
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
static
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
await this.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
]
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import { proto } from '../../WAProto/index.js'
|
|
3
|
+
import { BOT_RENDERING_CONFIG_METADATA } from '../Defaults/index.js'
|
|
4
|
+
import { generateWAMessage, generateWAMessageFromContent, generateMessageIDV2, prepareStickerPackMessage, prepareWAMessageMedia } from '../Utils/index.js'
|
|
5
|
+
import { getBizBinaryNode } from '../WABinary/index.js'
|
|
6
|
+
import { BaseBuilder, norm, q } from './base.js'
|
|
7
|
+
|
|
8
|
+
const delay = ms => new Promise(r => setTimeout(r, ms))
|
|
9
|
+
|
|
10
|
+
const SPECIAL_FLOW = {
|
|
11
|
+
cta_url: { v: '2', name: 'cta_url' }, cta_call: { v: '2', name: 'cta_call' },
|
|
12
|
+
cta_copy: { v: '2', name: 'cta_copy' }, cta_reminder: { v: '2', name: 'cta_reminder' },
|
|
13
|
+
cta_cancel_reminder: { v: '2', name: 'cta_cancel_reminder' }, address: { v: '2', name: 'address_message' },
|
|
14
|
+
send_location: { v: '2', name: 'send_location' }, cta_open_webview: { v: '3', name: 'cta_open_webview' },
|
|
15
|
+
review_and_pay: { v: '4', name: 'review_and_pay' }, review_order: { v: '4', name: 'review_order' },
|
|
16
|
+
payment_status: { v: '4', name: 'payment_status' }, transaction_details: { v: '4', name: 'transaction_details' },
|
|
17
|
+
order_details: { v: '4', name: 'order_details' }, multi_product: { v: '4', name: 'multi_product' },
|
|
18
|
+
catalog: { v: '5', name: 'catalog_message' }, flow: { v: '5', name: 'flow' },
|
|
19
|
+
galaxy_message: { v: '9', name: 'galaxy_message' },
|
|
20
|
+
}
|
|
21
|
+
const DEFAULT_FLOW = { v: '9', name: 'mixed' }
|
|
22
|
+
|
|
23
|
+
// ─── Button ───────────────────────────────────────────────────────────────────
|
|
24
|
+
export class Button {
|
|
25
|
+
#client; #buttons = []; #header = {}; #body = ''; #footer = ''
|
|
26
|
+
#title = ''; #subtitle = ''; #params = {}; #mode = 'native'; #bloks = null
|
|
27
|
+
|
|
28
|
+
constructor(client, opts = {}) {
|
|
29
|
+
if (!client) throw new Error('Socket client is required')
|
|
30
|
+
this.#client = client
|
|
31
|
+
if (opts.mode) this.#mode = opts.mode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setTitle(t) { this.#title = t; return this }
|
|
35
|
+
setSubtitle(s) { this.#subtitle = s; return this }
|
|
36
|
+
setBody(t) { this.#body = t; return this }
|
|
37
|
+
setFooter(t) { this.#footer = t; return this }
|
|
38
|
+
setImage(url, opts = {}) { this.#header = { hasMediaAttachment: true, imageMessage: { url, ...opts } }; return this }
|
|
39
|
+
setVideo(url, opts = {}) { this.#header = { hasMediaAttachment: true, videoMessage: { url, ...opts } }; return this }
|
|
40
|
+
setDocument(url, fileName = 'file', opts = {}) { this.#header = { hasMediaAttachment: true, documentMessage: { url, fileName, ...opts } }; return this }
|
|
41
|
+
setLocation(lat, lon, name = '') { this.#header = { hasMediaAttachment: false, locationMessage: { degreesLatitude: lat, degreesLongitude: lon, name } }; return this }
|
|
42
|
+
setParams(p) { this.#params = p; return this }
|
|
43
|
+
setLimitedTimeOffer(exp) { this.#params.limited_time_offer = { expiration_time_ms: exp }; return this }
|
|
44
|
+
setBottomSheet(opts = {}) { this.#params.bottom_sheet = opts; return this }
|
|
45
|
+
setTapTargetConfiguration(opts = {}) { this.#params.tap_target_configuration = opts; return this }
|
|
46
|
+
|
|
47
|
+
#addNative(name, params) { this.#mode = 'native'; this.#buttons.push({ name, buttonParamsJson: JSON.stringify(params) }); return this }
|
|
48
|
+
|
|
49
|
+
reply(text, id) { return this.#addNative('quick_reply', { display_text: text, id }) }
|
|
50
|
+
url(text, url, merchantUrl) { return this.#addNative('cta_url', { display_text: text, url, merchant_url: merchantUrl ?? url }) }
|
|
51
|
+
call(text, phone) { return this.#addNative('cta_call', { display_text: text, phone_number: phone }) }
|
|
52
|
+
copy(text, code) { return this.#addNative('cta_copy', { display_text: text, copy_code: code }) }
|
|
53
|
+
openWebview(text, url, opts = {}) { return this.#addNative('cta_open_webview', { display_text: text, url, ...opts }) }
|
|
54
|
+
catalog(text, opts = {}) { return this.#addNative('catalog', { display_text: text, ...opts }) }
|
|
55
|
+
flow(text, flowId, opts = {}) { return this.#addNative('flow', { display_text: text, flow_id: flowId, flow_token: opts.token ?? crypto.randomUUID(), ...opts }) }
|
|
56
|
+
remind(text, opts = {}) { return this.#addNative('cta_reminder', { display_text: text, ...opts }) }
|
|
57
|
+
cancelReminder(text, opts = {}) { return this.#addNative('cta_cancel_reminder', { display_text: text, ...opts }) }
|
|
58
|
+
address(text, opts = {}) { return this.#addNative('address', { display_text: text, ...opts }) }
|
|
59
|
+
sendLocation(text) { return this.#addNative('send_location', { display_text: text }) }
|
|
60
|
+
reviewAndPay(text, opts = {}) { return this.#addNative('review_and_pay', { display_text: text, ...opts }) }
|
|
61
|
+
reviewOrder(text, opts = {}) { return this.#addNative('review_order', { display_text: text, ...opts }) }
|
|
62
|
+
orderDetails(text, opts = {}) { return this.#addNative('order_details', { display_text: text, ...opts }) }
|
|
63
|
+
paymentStatus(text, opts = {}) { return this.#addNative('payment_status', { display_text: text, ...opts }) }
|
|
64
|
+
transactionDetails(text, opts = {}) { return this.#addNative('transaction_details', { display_text: text, ...opts }) }
|
|
65
|
+
select(title, sections) { return this.#addNative('single_select', { title, sections }) }
|
|
66
|
+
|
|
67
|
+
legacyButton(text, id) { this.#mode = 'legacy'; this.#buttons.push({ buttonId: id, buttonText: { displayText: text }, type: 1 }); return this }
|
|
68
|
+
templateReply(text, id) { this.#mode = 'template'; this.#buttons.push({ quickReplyButton: { displayText: text, id } }); return this }
|
|
69
|
+
templateUrl(text, url) { this.#mode = 'template'; this.#buttons.push({ urlButton: { displayText: text, url } }); return this }
|
|
70
|
+
templateCall(text, phone) { this.#mode = 'template'; this.#buttons.push({ callButton: { displayText: text, phoneNumber: phone } }); return this }
|
|
71
|
+
|
|
72
|
+
setBloksWidget(components) {
|
|
73
|
+
const flat = []
|
|
74
|
+
const resolve = c => { if (!c.__id) c.__id = `c_${flat.length}`; flat.push(c); if (c.children) c.children = c.children.map(ch => { resolve(ch); return ch.__id }); if (c.$ref) c.$ref = c.$ref.__id }
|
|
75
|
+
; (Array.isArray(components) ? components : [components]).forEach(c => resolve(c))
|
|
76
|
+
this.#bloks = flat; this.#mode = 'native'; return this
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
toCard() { return { header: this.#header, body: { text: this.#body }, footer: { text: this.#footer }, nativeFlowMessage: { buttons: this.#buttons } } }
|
|
80
|
+
|
|
81
|
+
async build(jid, opts = {}) {
|
|
82
|
+
const messageId = opts.messageId ?? generateMessageIDV2()
|
|
83
|
+
const userJid = this.#client.user?.id
|
|
84
|
+
if (this.#mode === 'legacy') return await generateWAMessageFromContent(jid, { buttonsMessage: { ...(Object.keys(this.#header).length ? this.#header : { contentText: this.#body }), footer: this.#footer, buttons: this.#buttons, headerType: Object.keys(this.#header).length ? 3 : 1 } }, { userJid, messageId, waUploadToServer: this.#client.waUploadToServer })
|
|
85
|
+
if (this.#mode === 'template') return await generateWAMessageFromContent(jid, { templateMessage: { hydratedTemplate: { hydratedContentText: this.#body, hydratedFooterText: this.#footer, hydratedButtons: this.#buttons } } }, { userJid, messageId, waUploadToServer: this.#client.waUploadToServer })
|
|
86
|
+
const isSingleSelect = this.#buttons.length === 1 && this.#buttons[0].name === 'single_select'
|
|
87
|
+
if (isSingleSelect && !this.#bloks) { const params = JSON.parse(this.#buttons[0].buttonParamsJson); return await generateWAMessageFromContent(jid, { listMessage: { title: this.#title, description: this.#body, footerText: this.#footer, buttonText: params.title ?? 'Select', listType: 1, sections: params.sections ?? [] } }, { userJid, messageId, waUploadToServer: this.#client.waUploadToServer }) }
|
|
88
|
+
const flowInfo = this.#buttons.reduce((acc, b) => { const f = SPECIAL_FLOW[b.name]; return (!acc || (f && parseInt(f.v) > parseInt(acc.v))) ? (f ?? DEFAULT_FLOW) : acc }, null) ?? DEFAULT_FLOW
|
|
89
|
+
return await generateWAMessageFromContent(jid, { interactiveMessage: { header: Object.keys(this.#header).length ? this.#header : undefined, body: { text: this.#body }, footer: { text: this.#footer }, nativeFlowMessage: { messageParamsJson: Object.keys(this.#params).length ? JSON.stringify(this.#params) : undefined, buttons: this.#bloks ? [{ name: 'galaxy_message', buttonParamsJson: JSON.stringify({ wa_flow_response_params: { title: this.#title }, components: this.#bloks }) }] : this.#buttons }, ...(this.#title ? { title: this.#title } : {}), ...(this.#subtitle ? { subtitle: this.#subtitle } : {}) } }, { userJid, messageId, waUploadToServer: this.#client.waUploadToServer })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async send(jid, opts = {}) {
|
|
93
|
+
const msg = await this.build(jid, opts)
|
|
94
|
+
const bizNode = getBizBinaryNode(msg.message)
|
|
95
|
+
await this.#client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
|
|
96
|
+
return msg
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Interactive ──────────────────────────────────────────────────────────────
|
|
101
|
+
export class Interactive extends BaseBuilder {
|
|
102
|
+
static dispatchKeys = ['interactiveMessage']
|
|
103
|
+
static shortcuts = {
|
|
104
|
+
sendInteractiveMessage: sock => (jid, data, quoted) => new Interactive(sock).from(data).send(jid, q(quoted)),
|
|
105
|
+
sendProductMessage: sock => (jid, data, quoted) => new Interactive(sock).from(data, { __product: true }).send(jid, q(quoted)),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async send(jid, opts = {}) {
|
|
109
|
+
const i = this.data
|
|
110
|
+
const userJid = this.client.user?.id
|
|
111
|
+
const upload = this.client.waUploadToServer
|
|
112
|
+
let media = null
|
|
113
|
+
if (i.thumbnail) media = await prepareWAMessageMedia({ image: { url: i.thumbnail } }, { upload })
|
|
114
|
+
else if (i.image) media = await prepareWAMessageMedia({ image: i.image }, { upload })
|
|
115
|
+
else if (i.video) media = await prepareWAMessageMedia({ video: i.video }, { upload })
|
|
116
|
+
else if (i.document) media = await prepareWAMessageMedia({ document: i.document }, { upload })
|
|
117
|
+
const bodyText = i.body?.text ?? i.title ?? ''
|
|
118
|
+
const footerText = typeof i.footer === 'string' ? i.footer : (i.footer?.text ?? '')
|
|
119
|
+
const headerTitle = typeof i.header === 'string' ? i.header : (i.header?.title ?? '')
|
|
120
|
+
let nativeFlow = null
|
|
121
|
+
if (i.buttons?.length || i.nativeFlowMessage) { const nfm = i.nativeFlowMessage ?? {}; nativeFlow = proto.Message.InteractiveMessage.NativeFlowMessage.create({ buttons: i.buttons ?? nfm.buttons ?? [], messageParamsJson: nfm.messageParamsJson ?? '' }) }
|
|
122
|
+
const headerMedia = {}
|
|
123
|
+
if (media?.imageMessage) headerMedia.imageMessage = media.imageMessage
|
|
124
|
+
if (media?.videoMessage) headerMedia.videoMessage = media.videoMessage
|
|
125
|
+
if (media?.documentMessage) headerMedia.documentMessage = media.documentMessage
|
|
126
|
+
const interactive = proto.Message.InteractiveMessage.create({ body: proto.Message.InteractiveMessage.Body.create({ text: bodyText }), footer: proto.Message.InteractiveMessage.Footer.create({ text: footerText }), header: proto.Message.InteractiveMessage.Header.create({ title: headerTitle, hasMediaAttachment: !!media, ...headerMedia }), ...(nativeFlow ? { nativeFlowMessage: nativeFlow } : {}) })
|
|
127
|
+
if (i.contextInfo) interactive.contextInfo = i.contextInfo
|
|
128
|
+
const msg = await await generateWAMessageFromContent(jid, { interactiveMessage: interactive }, { userJid, quoted: opts.quoted, waUploadToServer: this.client.waUploadToServer })
|
|
129
|
+
const bizNode = getBizBinaryNode(msg.message)
|
|
130
|
+
await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
|
|
131
|
+
return msg
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Carousel ─────────────────────────────────────────────────────────────────
|
|
136
|
+
export class Carousel extends BaseBuilder {
|
|
137
|
+
#cards = []; #caption = ''; #footer = ''; #cardType = 'HSCROLL_CARDS'
|
|
138
|
+
static #PLACEHOLDER = 'https://placehold.co/600x400/000000/ffffff?text=No+Image'
|
|
139
|
+
|
|
140
|
+
static dispatchKeys = ['carouselMessage']
|
|
141
|
+
static shortcuts = {
|
|
142
|
+
sendCarouselMessage: sock => (jid, data, quoted) => { const d = norm(data); const c = new Carousel(sock); if (d.caption) c.setCaption(d.caption); if (d.footer) c.setFooter(d.footer); (d.cards ?? []).forEach(card => c.card(card)); return c.send(jid, q(quoted)) },
|
|
143
|
+
sendCarouselProtoMessage: sock => (jid, data, quoted) => Carousel.shortcuts.sendCarouselMessage(sock)(jid, data, quoted),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
constructor(client) { super(client) }
|
|
147
|
+
|
|
148
|
+
from(data, opts = {}) {
|
|
149
|
+
const d = norm(data, opts)
|
|
150
|
+
if (d.caption) this.setCaption(d.caption)
|
|
151
|
+
if (d.footer) this.setFooter(d.footer)
|
|
152
|
+
if (d.cardType) this.setCardType(d.cardType)
|
|
153
|
+
; (d.cards ?? []).forEach(card => this.card(card))
|
|
154
|
+
return this
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setCaption(t) { this.#caption = t; return this }
|
|
158
|
+
setFooter(t) { this.#footer = t; return this }
|
|
159
|
+
setCardType(t) { this.#cardType = t; return this }
|
|
160
|
+
card(b) { this.#cards.push(b instanceof Button ? b.toCard() : b); return this }
|
|
161
|
+
|
|
162
|
+
async send(jid, opts = {}) {
|
|
163
|
+
if (this.#cards.length < 2) throw new Error('Carousel requires at least 2 cards')
|
|
164
|
+
if (this.#cards.length > 10) throw new Error('Carousel max 10 cards')
|
|
165
|
+
const cards = this.#cards.map(c => ({ ...c, header: (c.header?.imageMessage || c.header?.hasMediaAttachment) ? c.header : { ...c.header, hasMediaAttachment: true, imageMessage: { url: Carousel.#PLACEHOLDER } } }))
|
|
166
|
+
const msg = await await generateWAMessageFromContent(jid, { interactiveMessage: proto.Message.InteractiveMessage.create({ header: proto.Message.InteractiveMessage.Header.create({ hasMediaAttachment: false }), body: proto.Message.InteractiveMessage.Body.create({ text: this.#caption }), footer: proto.Message.InteractiveMessage.Footer.create({ text: this.#footer }), carouselMessage: proto.Message.InteractiveMessage.CarouselMessage.create({ cards: cards.map(c => ({ header: proto.Message.InteractiveMessage.Header.create({ title: c.header?.title ?? '', hasMediaAttachment: c.header?.hasMediaAttachment ?? false, ...c.header }), body: proto.Message.InteractiveMessage.Body.create({ text: c.body?.text ?? '' }), footer: proto.Message.InteractiveMessage.Footer.create({ text: c.footer?.text ?? '' }), nativeFlowMessage: proto.Message.InteractiveMessage.NativeFlowMessage.create({ buttons: c.nativeFlowMessage?.buttons ?? [] }) })), messageVersion: 1, carouselCardType: this.#cardType === 'ALBUM_IMAGE' ? 2 : 1 }) }) }, { userJid: this.client.user?.id, waUploadToServer: this.client.waUploadToServer })
|
|
167
|
+
const bizNode = getBizBinaryNode(msg.message)
|
|
168
|
+
await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
|
|
169
|
+
return msg
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Album ────────────────────────────────────────────────────────────────────
|
|
174
|
+
export class Album extends BaseBuilder {
|
|
175
|
+
#items = []; #delay = 1500
|
|
176
|
+
|
|
177
|
+
static dispatchKeys = ['albumMessage']
|
|
178
|
+
static shortcuts = {
|
|
179
|
+
sendAlbumMessage: sock => (jid, items, quoted, opts = {}) => sock.sendMessage(jid, { albumMessage: items }, { ...q(quoted), ...opts }),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
image(urlOrBuffer, caption = '') { this.#items.push({ image: typeof urlOrBuffer === 'string' ? { url: urlOrBuffer } : urlOrBuffer, caption }); return this }
|
|
183
|
+
video(urlOrBuffer, caption = '') { this.#items.push({ video: typeof urlOrBuffer === 'string' ? { url: urlOrBuffer } : urlOrBuffer, caption }); return this }
|
|
184
|
+
add(items) { items.forEach(item => this.#items.push(item)); return this }
|
|
185
|
+
setDelay(ms) { this.#delay = ms; return this }
|
|
186
|
+
|
|
187
|
+
async send(jid, opts = {}) {
|
|
188
|
+
const items = this.data.albumMessage ? (Array.isArray(this.data.albumMessage) ? this.data.albumMessage : [this.data.albumMessage]) : this.#items
|
|
189
|
+
if (items.length < 2) throw new Error('Album requires at least 2 items')
|
|
190
|
+
const userJid = this.client.user?.id
|
|
191
|
+
const album = await await generateWAMessageFromContent(jid, { messageContextInfo: proto.MessageContextInfo.create({ messageSecret: crypto.randomBytes(32) }), albumMessage: proto.Message.AlbumMessage.create({ expectedImageCount: items.filter(a => a.image).length, expectedVideoCount: items.filter(a => a.video).length }) }, { userJid, messageId: generateMessageIDV2() })
|
|
192
|
+
await this.client.relayMessage(jid, album.message, { messageId: album.key.id })
|
|
193
|
+
for (const item of items) {
|
|
194
|
+
const img = await generateWAMessage(jid, item, { upload: this.client.waUploadToServer, userJid })
|
|
195
|
+
img.message.messageContextInfo = proto.MessageContextInfo.create({ messageSecret: crypto.randomBytes(32), messageAssociation: proto.MessageAssociation.create({ associationType: 1, parentMessageKey: album.key }) })
|
|
196
|
+
await this.client.relayMessage(jid, img.message, { messageId: img.key.id })
|
|
197
|
+
await delay(opts.albumDelay ?? this.#delay)
|
|
198
|
+
}
|
|
199
|
+
return album
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Poll ─────────────────────────────────────────────────────────────────────
|
|
204
|
+
export class Poll extends BaseBuilder {
|
|
205
|
+
#poll = { name: '', values: [], selectableOptionsCount: 1, toAnnouncementGroup: false, hideVoter: false }
|
|
206
|
+
|
|
207
|
+
static shortcuts = {
|
|
208
|
+
sendPoll: sock => (jid, name, values, multiSelect = false, opts = {}) => sock.sendMessage(jid, { poll: { name, values, selectableOptionsCount: multiSelect ? 0 : 1 } }, opts),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
name(n) { this.#poll.name = n; return this }
|
|
212
|
+
options(o) { this.#poll.values = o; return this }
|
|
213
|
+
addOption(o) { this.#poll.values.push(o); return this }
|
|
214
|
+
multiSelect(max = 0) { this.#poll.selectableOptionsCount = max; return this }
|
|
215
|
+
quiz(ans) { this.#poll.pollType = 1; this.#poll.correctAnswer = ans; return this }
|
|
216
|
+
setEndTime(t) { this.#poll.endTime = t; return this }
|
|
217
|
+
|
|
218
|
+
async send(jid, opts = {}) {
|
|
219
|
+
if (!this.#poll.name) throw new Error('Poll name is required')
|
|
220
|
+
if (this.#poll.values.length < 2) throw new Error('Poll requires at least 2 options')
|
|
221
|
+
return this.client.sendMessage(jid, { poll: this.#poll }, opts)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Payment ──────────────────────────────────────────────────────────────────
|
|
226
|
+
export class Payment extends BaseBuilder {
|
|
227
|
+
static dispatchKeys = ['requestPaymentMessage']
|
|
228
|
+
static shortcuts = {
|
|
229
|
+
sendPaymentMessage: sock => (jid, data, quoted) => new Payment(sock).from(data).send(jid, q(quoted)),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
setAmount(a) { this.data.amount = a; return this }
|
|
233
|
+
setCurrency(c) { this.data.currency = c; return this }
|
|
234
|
+
setExpiry(t) { this.data.expiry = t; return this }
|
|
235
|
+
setNote(n) { this.data.note = n; return this }
|
|
236
|
+
setFrom(j) { this.data.from = j; return this }
|
|
237
|
+
|
|
238
|
+
async send(jid, opts = {}) {
|
|
239
|
+
const d = this.data
|
|
240
|
+
const ctx = opts.quoted ? { stanzaId: opts.quoted.key?.id, participant: opts.quoted.key?.participant, quotedMessage: opts.quoted.message } : {}
|
|
241
|
+
const notes = d.note ? { extendedTextMessage: { text: d.note, contextInfo: ctx } } : {}
|
|
242
|
+
const msg = await await generateWAMessageFromContent(jid, { requestPaymentMessage: proto.Message.RequestPaymentMessage.fromObject({ expiryTimestamp: d.expiry ?? 0, amount1000: d.amount ?? 0, currencyCodeIso4217: d.currency ?? 'NGN', requestFrom: d.from ?? '0@s.whatsapp.net', noteMessage: notes, background: d.background ?? { id: 'DEFAULT', placeholderArgb: 0xfff0f0f0 } }) }, { userJid: this.client.user?.id, waUploadToServer: this.client.waUploadToServer })
|
|
243
|
+
await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
|
|
244
|
+
return msg
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─── Order ────────────────────────────────────────────────────────────────────
|
|
249
|
+
export class Order extends BaseBuilder {
|
|
250
|
+
static dispatchKeys = ['orderMessage']
|
|
251
|
+
static shortcuts = {
|
|
252
|
+
sendOrderMessage: sock => (jid, data, quoted) => new Order(sock).from(data).send(jid, q(quoted)),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async send(jid, opts = {}) {
|
|
256
|
+
const o = this.data
|
|
257
|
+
const userJid = this.client.user?.id
|
|
258
|
+
let thumb = null
|
|
259
|
+
if (o.thumbnail) {
|
|
260
|
+
if (Buffer.isBuffer(o.thumbnail)) { thumb = o.thumbnail }
|
|
261
|
+
else if (typeof o.thumbnail === 'string') { try { const { default: axios } = await import('axios'); const res = await axios.get(o.thumbnail, { responseType: 'arraybuffer' }); thumb = Buffer.from(res.data) } catch { } }
|
|
262
|
+
}
|
|
263
|
+
const cleanJid = id => { if (!id) return null; const [user] = id.split(':'); return user.includes('@') ? user : `${user}@s.whatsapp.net` }
|
|
264
|
+
const seller = cleanJid(o.sellerJid) ?? cleanJid(userJid) ?? cleanJid(jid) ?? '0@s.whatsapp.net'
|
|
265
|
+
const msg = await await generateWAMessageFromContent(jid, { orderMessage: proto.Message.OrderMessage.create({ orderId: o.orderId ?? `zervonex${Date.now()}`, thumbnail: thumb, itemCount: o.itemCount ?? 0, status: 2, surface: 1, message: o.message, orderTitle: o.orderTitle, sellerJid: seller, token: o.token ?? 'zervonex_TOKEN', totalAmount1000: o.totalAmount1000 ?? 0, totalCurrencyCode: o.totalCurrencyCode ?? 'NGN', messageVersion: 2 }) }, { userJid, quoted: opts.quoted, waUploadToServer: this.client.waUploadToServer })
|
|
266
|
+
await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
|
|
267
|
+
return msg
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── Event ────────────────────────────────────────────────────────────────────
|
|
272
|
+
export class Event extends BaseBuilder {
|
|
273
|
+
static dispatchKeys = ['eventMessage']
|
|
274
|
+
static shortcuts = {
|
|
275
|
+
sendEventMessage: sock => (jid, data, quoted) => new Event(sock).from(data).send(jid, q(quoted)),
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
setName(n) { this.data.name = n; return this }
|
|
279
|
+
setDescription(d) { this.data.description = d; return this }
|
|
280
|
+
setLocation(l) { this.data.location = l; return this }
|
|
281
|
+
setJoinLink(l) { this.data.joinLink = l; return this }
|
|
282
|
+
setStartTime(t) { this.data.startTime = t; return this }
|
|
283
|
+
setEndTime(t) { this.data.endTime = t; return this }
|
|
284
|
+
setCanceled(v = true) { this.data.isCanceled = v; return this }
|
|
285
|
+
setReminder(sec) { this.data.hasReminder = true; this.data.reminderOffsetSec = sec; return this }
|
|
286
|
+
setScheduledCall(v = true) { this.data.isScheduleCall = v; return this }
|
|
287
|
+
|
|
288
|
+
async send(jid, opts = {}) {
|
|
289
|
+
const e = this.data
|
|
290
|
+
const parseTime = (val, def) => typeof val === 'string' ? parseInt(val) : (val ?? def)
|
|
291
|
+
const msg = await await generateWAMessageFromContent(jid, { messageContextInfo: proto.MessageContextInfo.create({ deviceListMetadata: {}, deviceListMetadataVersion: 2, messageSecret: crypto.randomBytes(32), supportPayload: JSON.stringify({ version: 2, is_ai_message: true, should_show_system_message: true, ticket_id: crypto.randomBytes(16).toString('hex') }) }), eventMessage: proto.Message.EventMessage.create({ contextInfo: proto.ContextInfo.create({ mentionedJid: [jid], participant: jid, remoteJid: 'status@broadcast', forwardedNewsletterMessageInfo: proto.ContextInfo.ForwardedNewsletterMessageInfo.create({ newsletterName: 'zervonex Events', newsletterJid: '120363422827915475@newsletter', serverMessageId: 1 }) }), isCanceled: e.isCanceled ?? false, name: e.name, description: e.description, location: e.location ?? { degreesLatitude: 0, degreesLongitude: 0, name: 'Location' }, joinLink: e.joinLink ?? '', startTime: parseTime(e.startTime, Date.now()), endTime: parseTime(e.endTime, Date.now() + 3600000), extraGuestsAllowed: e.extraGuestsAllowed !== false, ...(e.hasReminder ? { hasReminder: true, reminderOffsetSec: e.reminderOffsetSec ?? 3600 } : {}), ...(e.isScheduleCall !== undefined ? { isScheduleCall: e.isScheduleCall } : {}) }) }, { userJid: this.client.user?.id, quoted: opts.quoted, waUploadToServer: this.client.waUploadToServer })
|
|
292
|
+
await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
|
|
293
|
+
return msg
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ─── StickerPack ──────────────────────────────────────────────────────────────
|
|
298
|
+
export class StickerPack extends BaseBuilder {
|
|
299
|
+
static dispatchKeys = ['stickerPack', 'stickerPackMessage']
|
|
300
|
+
static shortcuts = {
|
|
301
|
+
stickerPackMessage: sock => (jid, data, opts = {}) => new StickerPack(sock).from(data, { name: opts.packName, publisher: opts.packPublisher }).send(jid, q(opts.quoted)),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
setStickers(s) { this.data.stickers = s; return this }
|
|
305
|
+
|
|
306
|
+
async send(jid, opts = {}) {
|
|
307
|
+
const userJid = this.client.user?.id
|
|
308
|
+
const { quoted, ...passOpts } = opts
|
|
309
|
+
const result = await prepareStickerPackMessage(this.data, { upload: this.client.waUploadToServer, logger: this.client.logger, ...passOpts })
|
|
310
|
+
if (result.isBatched) {
|
|
311
|
+
let last
|
|
312
|
+
for (let i = 0; i < result.stickerPackMessage.length; i++) {
|
|
313
|
+
const msg = await await generateWAMessageFromContent(jid, { stickerPackMessage: result.stickerPackMessage[i] }, { userJid, quoted, waUploadToServer: this.client.waUploadToServer })
|
|
314
|
+
await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
|
|
315
|
+
last = msg
|
|
316
|
+
if (i < result.stickerPackMessage.length - 1) await delay(2000)
|
|
317
|
+
}
|
|
318
|
+
return last
|
|
319
|
+
}
|
|
320
|
+
const msg = await await generateWAMessageFromContent(jid, { stickerPackMessage: result.stickerPackMessage }, { userJid, quoted, waUploadToServer: this.client.waUploadToServer })
|
|
321
|
+
await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
|
|
322
|
+
return msg
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// ─── VerticalAlbum ────────────────────────────────────────────────────────────
|
|
326
|
+
export class VerticalAlbum extends BaseBuilder {
|
|
327
|
+
static dispatchKeys = ['verticalAlbumMessage']
|
|
328
|
+
static shortcuts = {
|
|
329
|
+
sendVerticalAlbumMessage: sock => (jid, data, opts = {}) => new VerticalAlbum(sock).from(data, opts).send(jid, opts)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async send(jid, opts = {}) {
|
|
333
|
+
const items = this.data.items ?? (Array.isArray(this.data.verticalAlbumMessage) ? this.data.verticalAlbumMessage : [])
|
|
334
|
+
if (!Array.isArray(items) || items.length < 2) throw new Error('VerticalAlbum requires at least 2 items')
|
|
335
|
+
const expiration = String(Date.now() + 30 * 24 * 60 * 60 * 1000)
|
|
336
|
+
const primitives = items.map((item, i) => {
|
|
337
|
+
const kind = item?.type || (item?.video ? 'video' : 'image')
|
|
338
|
+
if (kind === 'video') {
|
|
339
|
+
const v = item.video || item.data || item
|
|
340
|
+
const url = typeof v === 'string' ? v : (v?.url ?? v?.videoUrl ?? '')
|
|
341
|
+
if (!url) throw new Error(`VerticalAlbum item ${i}: video requires a URL`)
|
|
342
|
+
return { media: { url, mime_type: v?.mimeType ?? v?.mime_type ?? 'video/mp4', file_length: v?.fileLength ?? v?.file_length ?? 0, duration: v?.duration ?? 0 }, imagine_type: 'ANIMATE', status: { status: 'READY' }, __typename: 'GenAIImaginePrimitive', ...(v?.thumbnail ? { thumbnail: { raw_media: v.thumbnail } } : {}) }
|
|
343
|
+
}
|
|
344
|
+
const img = item?.image || item?.data || item
|
|
345
|
+
const url = typeof img === 'string' ? img : (img?.imageHighResUrl ?? img?.highResUrl ?? img?.fullUrl ?? img?.imagePreviewUrl ?? img?.previewUrl ?? img?.url ?? '')
|
|
346
|
+
if (!url) throw new Error(`VerticalAlbum item ${i}: image requires a URL`)
|
|
347
|
+
const w = Number(img?.width ?? 600), h = Number(img?.height ?? 400)
|
|
348
|
+
const media = (u, mw, mh) => ({ url: u, url_fallback: u, mime_type: img?.mimeType ?? img?.mime_type ?? 'image/png', expiration_timestamp_ms: expiration, width: mw, height: mh })
|
|
349
|
+
return { preview_image: media(url, w, h), full_image: media(img?.highResUrl ?? url, w, h), dark_mode_preview_image: media(img?.darkModePreviewUrl ?? url, w, h), dark_mode_full_image: media(img?.darkModeHighResUrl ?? url, w, h), asset_query_status: 'FETCHED', display_mode: 'FULL_WIDTH', full_width: true, __typename: 'GenAIImagePrimitive' }
|
|
350
|
+
})
|
|
351
|
+
const unified = { response_id: crypto.randomUUID(), sections: [{ view_model: { primitives, display_mode: 'FULL_WIDTH', full_width: true, __typename: 'GenAIVStackLayoutViewModel' } }] }
|
|
352
|
+
const msg = await await generateWAMessageFromContent(jid, {
|
|
353
|
+
messageContextInfo: { botMetadata: { pluginMetadata: {}, botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA } },
|
|
354
|
+
botForwardedMessage: { message: { richResponseMessage: proto.Message.AIRichResponseMessage?.create?.({ messageType: 1, submessages: [], unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) }, contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' }, forwardOrigin: 4 } }) ?? { messageType: 1, submessages: [], unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) }, contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' }, forwardOrigin: 4 } } } }
|
|
355
|
+
}, { userJid: this.client.user?.id, waUploadToServer: this.client.waUploadToServer })
|
|
356
|
+
await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
|
|
357
|
+
return msg
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ─── GridImage ────────────────────────────────────────────────────────────────
|
|
362
|
+
export class GridImage extends BaseBuilder {
|
|
363
|
+
static dispatchKeys = ['gridImageMessage']
|
|
364
|
+
static shortcuts = {
|
|
365
|
+
sendGridImageMessage: sock => (jid, data, opts = {}) => new GridImage(sock).from(data, opts).send(jid, opts)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async send(jid, opts = {}) {
|
|
369
|
+
const imageUrls = this.data.imageUrls ?? this.data.images ?? (Array.isArray(this.data.gridImageMessage) ? this.data.gridImageMessage : [])
|
|
370
|
+
if (!Array.isArray(imageUrls) || imageUrls.length < 2) throw new Error('GridImage requires at least 2 images')
|
|
371
|
+
const expiration = String(Date.now() + 30 * 24 * 60 * 60 * 1000)
|
|
372
|
+
// GRID_IMAGE submessage type (6) — proper protocol path, not just unified sections
|
|
373
|
+
const submessages = [{ messageType: 6, gridImageMetadata: { imageUrls } }]
|
|
374
|
+
const sections = imageUrls.map(img => {
|
|
375
|
+
const image = typeof img === 'string' ? { url: img } : img
|
|
376
|
+
const url = image.imageHighResUrl ?? image.highResUrl ?? image.fullUrl ?? image.imagePreviewUrl ?? image.previewUrl ?? image.url ?? ''
|
|
377
|
+
const w = Number(image.width ?? 600), h = Number(image.height ?? 400)
|
|
378
|
+
const media = (u, mw, mh) => ({ url: u, url_fallback: u, mime_type: image.mimeType ?? image.mime_type ?? 'image/png', expiration_timestamp_ms: expiration, width: mw, height: mh })
|
|
379
|
+
return { preview_image: media(url, w, h), full_image: media(image.highResUrl ?? url, w, h), dark_mode_preview_image: media(image.darkModePreviewUrl ?? url, w, h), dark_mode_full_image: media(image.darkModeHighResUrl ?? url, w, h), asset_query_status: 'FETCHED', __typename: 'GenAIImagePrimitive' }
|
|
380
|
+
})
|
|
381
|
+
const unified = { response_id: crypto.randomUUID(), sections: [{ view_model: { primitives: sections, __typename: 'GenAIGridLayoutViewModel' } }] }
|
|
382
|
+
const msg = await await generateWAMessageFromContent(jid, {
|
|
383
|
+
messageContextInfo: { botMetadata: { pluginMetadata: {}, botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA } },
|
|
384
|
+
botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages, unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) }, contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' }, forwardOrigin: 4 } } } }
|
|
385
|
+
}, { userJid: this.client.user?.id, waUploadToServer: this.client.waUploadToServer })
|
|
386
|
+
await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
|
|
387
|
+
return msg
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ─── VideoGrid ────────────────────────────────────────────────────────────────
|
|
392
|
+
export class VideoGrid extends BaseBuilder {
|
|
393
|
+
static dispatchKeys = ['videoGridMessage']
|
|
394
|
+
static shortcuts = {
|
|
395
|
+
sendVideoGridMessage: sock => (jid, data, opts = {}) => new VideoGrid(sock).from(data, opts).send(jid, opts)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async send(jid, opts = {}) {
|
|
399
|
+
const videos = this.data.videos ?? (Array.isArray(this.data.videoGridMessage) ? this.data.videoGridMessage : [])
|
|
400
|
+
if (!Array.isArray(videos) || videos.length < 2) throw new Error('VideoGrid requires at least 2 videos')
|
|
401
|
+
const primitives = videos.map((v, i) => {
|
|
402
|
+
const url = typeof v === 'string' ? v : (v?.url ?? v?.videoUrl ?? v?.reelsUrl ?? '')
|
|
403
|
+
if (!url) throw new Error(`VideoGrid item ${i}: requires a URL`)
|
|
404
|
+
const item = typeof v === 'string' ? {} : v
|
|
405
|
+
return { post_id: item.postId != null ? String(item.postId) : undefined, reels_url: url, reels_title: item.title, thumbnail_url: item.thumbnailUrl ?? item.thumbnail, creator: item.creator, avatar_url: item.avatarUrl, content_hash: item.contentHash, likes_count: item.likes, comments_count: item.comments, shares_count: item.shares, is_verified: item.isVerified, source_app: item.sourceApp, video_delivery_response: { progressive_urls: (item.progressiveUrls ?? []).map(u => ({ progressive_url: u, __typename: 'GenAIProgressiveUrlResponse' })), dash_manifests: (item.dashManifests ?? []).map(m => ({ manifest_xml: m, __typename: 'GenAIDashManifestResponse' })), __typename: 'GenAIVideoDeliveryResponse' }, __typename: 'GenAIVideoPrimitive' }
|
|
406
|
+
})
|
|
407
|
+
const unified = { response_id: crypto.randomUUID(), sections: [{ view_model: { primitives, __typename: 'GenAIGridLayoutViewModel' } }] }
|
|
408
|
+
const msg = await await generateWAMessageFromContent(jid, {
|
|
409
|
+
messageContextInfo: { botMetadata: { pluginMetadata: {}, botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA } },
|
|
410
|
+
botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages: [], unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) }, contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' }, forwardOrigin: 4 } } } }
|
|
411
|
+
}, { userJid: this.client.user?.id, waUploadToServer: this.client.waUploadToServer })
|
|
412
|
+
await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
|
|
413
|
+
return msg
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ─── A2UI ─────────────────────────────────────────────────────────────────────
|
|
418
|
+
export class A2UI extends BaseBuilder {
|
|
419
|
+
static dispatchKeys = ['a2uiMessage']
|
|
420
|
+
static shortcuts = {
|
|
421
|
+
sendA2UIMessage: sock => (jid, data, opts = {}) => new A2UI(sock).from(data, opts).send(jid, opts),
|
|
422
|
+
sendA2UICommandMenu: sock => (jid, data, opts = {}) => new A2UI(sock).from(data, { __commandMenu: true, ...opts }).send(jid, opts)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async send(jid, opts = {}) {
|
|
426
|
+
const d = this.data
|
|
427
|
+
let content
|
|
428
|
+
if (d.__commandMenu) {
|
|
429
|
+
// command menu preset — builds bloksWidget + nativeFlowMessage together
|
|
430
|
+
const rows = (d.rows ?? []).map((r, i) => ({ title: String(r?.[0] ?? r?.title ?? `Command ${i + 1}`), description: String(r?.[1] ?? r?.description ?? ''), id: String(r?.[2] ?? r?.id ?? r?.[0] ?? `command_${i + 1}`) }))
|
|
431
|
+
const buttons = (d.buttons ?? []).map((b, i) => ({ id: `a2ui_button_${i + 1}`, label: String(b?.text ?? b?.label ?? `Button ${i + 1}`), url: String(b?.url ?? '') }))
|
|
432
|
+
const componentIds = ['title', 'divider', 'tableCard', 'footer', ...(buttons.length ? ['buttonRow'] : [])]
|
|
433
|
+
const components = [
|
|
434
|
+
{ id: 'root', component: 'Column', align: 'center', children: componentIds },
|
|
435
|
+
{ id: 'title', component: 'Text', text: String(d.title ?? 'Menu'), variant: 'h2' },
|
|
436
|
+
{ id: 'divider', component: 'Divider' },
|
|
437
|
+
{ id: 'tableCard', component: 'Card', child: 'tableColumn' },
|
|
438
|
+
{ id: 'tableColumn', component: 'Column', children: ['headerRow', ...rows.flatMap((_, i) => [`divider_${i}`, `row_${i}`])] },
|
|
439
|
+
{ id: 'headerRow', component: 'Row', children: ['headerCommand', 'headerFunction'] },
|
|
440
|
+
{ id: 'headerCommand', component: 'Text', text: 'Command', variant: 'h5' },
|
|
441
|
+
{ id: 'headerFunction', component: 'Text', text: 'Function', variant: 'h5' },
|
|
442
|
+
...rows.flatMap((r, i) => [{ id: `divider_${i}`, component: 'Divider' }, { id: `row_${i}`, component: 'Row', children: [`command_${i}`, `description_${i}`] }, { id: `command_${i}`, component: 'Text', text: r.title }, { id: `description_${i}`, component: 'Text', text: r.description }]),
|
|
443
|
+
{ id: 'footer', component: 'Text', text: d.footer ?? 'Powered by zervonex', variant: 'caption' }
|
|
444
|
+
]
|
|
445
|
+
if (buttons.length) {
|
|
446
|
+
components.push({ id: 'buttonRow', component: 'Row', justify: 'spaceEvenly', children: buttons.flatMap((b, i) => [`buttonLabel_${i}`, `button_${i}`]) })
|
|
447
|
+
buttons.forEach((b, i) => { components.push({ id: `buttonLabel_${i}`, component: 'Text', text: b.label }); components.push({ id: `button_${i}`, component: 'Button', child: `buttonLabel_${i}`, variant: 'primary', action: { call: 'openUrl', args: { url: b.url } } }) })
|
|
448
|
+
}
|
|
449
|
+
const surfaceId = `zervonex-a2ui-${crypto.randomUUID()}`
|
|
450
|
+
content = { interactiveMessage: { ...(d.imageMessage ? { header: { imageMessage: d.imageMessage, hasMediaAttachment: true } } : {}), bloksWidget: { type: 'im_a2ui', data: JSON.stringify([{ version: 'v0.9.1', createSurface: { surfaceId, root: 'root', catalogId: 'https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json' } }, { version: 'v0.9.1', updateComponents: { surfaceId, components } }, { version: 'v0.9.1', updateDataModel: { surfaceId, value: { title: String(d.title ?? 'Menu'), footer: d.footer ?? 'Powered by zervonex', rows } } }]), fallback: String(d.fallback ?? d.title ?? '') }, nativeFlowMessage: { messageParamsJson: JSON.stringify({ limited_time_offer: { text: String(d.title ?? ''), url: buttons[0]?.url ?? '', copy_code: String(d.title ?? ''), expiration_time: Date.now() * 1000 } }), buttons: [{ name: 'single_select', buttonParamsJson: JSON.stringify({ title: String(d.title ?? ''), sections: [{ title: 'Available Commands', rows }] }) }] } } }
|
|
451
|
+
} else {
|
|
452
|
+
// generic A2UI envelope
|
|
453
|
+
const { imageMessage, bloksWidget = {}, nativeFlowMessage, fallback = '', ...rest } = d
|
|
454
|
+
content = { interactiveMessage: { ...rest, ...(imageMessage ? { header: { imageMessage, hasMediaAttachment: true } } : {}), bloksWidget: { type: bloksWidget.type ?? 'im_a2ui', data: typeof bloksWidget.data === 'string' ? bloksWidget.data : JSON.stringify(bloksWidget.data ?? {}), ...(bloksWidget.fallback || fallback ? { fallback: String(bloksWidget.fallback ?? fallback) } : {}) }, ...(nativeFlowMessage ? { nativeFlowMessage: { ...nativeFlowMessage, ...(nativeFlowMessage.buttons ? { buttons: nativeFlowMessage.buttons } : {}) } } : {}) } }
|
|
455
|
+
}
|
|
456
|
+
const msg = await await generateWAMessageFromContent(jid, content, { userJid: this.client.user?.id, waUploadToServer: this.client.waUploadToServer })
|
|
457
|
+
const bizNode = getBizBinaryNode(msg.message)
|
|
458
|
+
await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
|
|
459
|
+
return msg
|
|
460
|
+
}
|
|
449
461
|
}
|