@zervonex/baileys 3.0.2 → 3.0.4

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.
@@ -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
- #client; #cards = []; #caption = ''; #footer = ''; #cardType = 'HSCROLL_CARDS'
138
-
139
- static dispatchKeys = ['carouselMessage']
140
- static shortcuts = {
141
- 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)) },
142
- sendCarouselProtoMessage: sock => (jid, data, quoted) => Carousel.shortcuts.sendCarouselMessage(sock)(jid, data, quoted),
143
- }
144
-
145
- constructor(client) { if (!client) throw new Error('Socket client is required'); this.#client = client }
146
- setCaption(t) { this.#caption = t; return this }
147
- setFooter(t) { this.#footer = t; return this }
148
- setCardType(t) { this.#cardType = t; return this }
149
- card(b) { this.#cards.push(b instanceof Button ? b.toCard() : b); return this }
150
-
151
- async send(jid, opts = {}) {
152
- if (!this.#cards.length) throw new Error('Carousel requires at least 1 card')
153
- if (this.#cards.length > 10) throw new Error('Carousel max 10 cards')
154
- const msg = 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: this.#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 })
155
- const bizNode = getBizBinaryNode(msg.message)
156
- await this.#client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
157
- return msg
158
- }
159
- }
160
-
161
- // ─── Album ────────────────────────────────────────────────────────────────────
162
- export class Album extends BaseBuilder {
163
- #items = []; #delay = 1500
164
-
165
- static dispatchKeys = ['albumMessage']
166
- static shortcuts = {
167
- sendAlbumMessage: sock => (jid, items, quoted, opts = {}) => sock.sendMessage(jid, { albumMessage: items }, { ...q(quoted), ...opts }),
168
- }
169
-
170
- image(urlOrBuffer, caption = '') { this.#items.push({ image: typeof urlOrBuffer === 'string' ? { url: urlOrBuffer } : urlOrBuffer, caption }); return this }
171
- video(urlOrBuffer, caption = '') { this.#items.push({ video: typeof urlOrBuffer === 'string' ? { url: urlOrBuffer } : urlOrBuffer, caption }); return this }
172
- add(items) { items.forEach(item => this.#items.push(item)); return this }
173
- setDelay(ms) { this.#delay = ms; return this }
174
-
175
- async send(jid, opts = {}) {
176
- const items = this.data.albumMessage ? (Array.isArray(this.data.albumMessage) ? this.data.albumMessage : [this.data.albumMessage]) : this.#items
177
- if (items.length < 2) throw new Error('Album requires at least 2 items')
178
- const userJid = this.client.user?.id
179
- const album = 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() })
180
- await this.client.relayMessage(jid, album.message, { messageId: album.key.id })
181
- for (const item of items) {
182
- const img = await generateWAMessage(jid, item, { upload: this.client.waUploadToServer, userJid })
183
- img.message.messageContextInfo = proto.MessageContextInfo.create({ messageSecret: crypto.randomBytes(32), messageAssociation: proto.MessageAssociation.create({ associationType: 1, parentMessageKey: album.key }) })
184
- await this.client.relayMessage(jid, img.message, { messageId: img.key.id })
185
- await delay(opts.albumDelay ?? this.#delay)
186
- }
187
- return album
188
- }
189
- }
190
-
191
- // ─── Poll ─────────────────────────────────────────────────────────────────────
192
- export class Poll extends BaseBuilder {
193
- #poll = { name: '', values: [], selectableOptionsCount: 1, toAnnouncementGroup: false, hideVoter: false }
194
-
195
- static shortcuts = {
196
- sendPoll: sock => (jid, name, values, multiSelect = false, opts = {}) => sock.sendMessage(jid, { poll: { name, values, selectableOptionsCount: multiSelect ? 0 : 1 } }, opts),
197
- }
198
-
199
- name(n) { this.#poll.name = n; return this }
200
- options(o) { this.#poll.values = o; return this }
201
- addOption(o) { this.#poll.values.push(o); return this }
202
- multiSelect(max = 0) { this.#poll.selectableOptionsCount = max; return this }
203
- quiz(ans) { this.#poll.pollType = 1; this.#poll.correctAnswer = ans; return this }
204
- setEndTime(t) { this.#poll.endTime = t; return this }
205
-
206
- async send(jid, opts = {}) {
207
- if (!this.#poll.name) throw new Error('Poll name is required')
208
- if (this.#poll.values.length < 2) throw new Error('Poll requires at least 2 options')
209
- return this.client.sendMessage(jid, { poll: this.#poll }, opts)
210
- }
211
- }
212
-
213
- // ─── Payment ──────────────────────────────────────────────────────────────────
214
- export class Payment extends BaseBuilder {
215
- static dispatchKeys = ['requestPaymentMessage']
216
- static shortcuts = {
217
- sendPaymentMessage: sock => (jid, data, quoted) => new Payment(sock).from(data).send(jid, q(quoted)),
218
- }
219
-
220
- setAmount(a) { this.data.amount = a; return this }
221
- setCurrency(c) { this.data.currency = c; return this }
222
- setExpiry(t) { this.data.expiry = t; return this }
223
- setNote(n) { this.data.note = n; return this }
224
- setFrom(j) { this.data.from = j; return this }
225
-
226
- async send(jid, opts = {}) {
227
- const d = this.data
228
- const ctx = opts.quoted ? { stanzaId: opts.quoted.key?.id, participant: opts.quoted.key?.participant, quotedMessage: opts.quoted.message } : {}
229
- const notes = d.note ? { extendedTextMessage: { text: d.note, contextInfo: ctx } } : {}
230
- const msg = 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 })
231
- await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
232
- return msg
233
- }
234
- }
235
-
236
- // ─── Order ────────────────────────────────────────────────────────────────────
237
- export class Order extends BaseBuilder {
238
- static dispatchKeys = ['orderMessage']
239
- static shortcuts = {
240
- sendOrderMessage: sock => (jid, data, quoted) => new Order(sock).from(data).send(jid, q(quoted)),
241
- }
242
-
243
- async send(jid, opts = {}) {
244
- const o = this.data
245
- const userJid = this.client.user?.id
246
- let thumb = null
247
- if (o.thumbnail) {
248
- if (Buffer.isBuffer(o.thumbnail)) { thumb = o.thumbnail }
249
- 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 { } }
250
- }
251
- const cleanJid = id => { if (!id) return null; const [user] = id.split(':'); return user.includes('@') ? user : `${user}@s.whatsapp.net` }
252
- const seller = cleanJid(o.sellerJid) ?? cleanJid(userJid) ?? cleanJid(jid) ?? '0@s.whatsapp.net'
253
- const msg = 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 })
254
- await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
255
- return msg
256
- }
257
- }
258
-
259
- // ─── Event ────────────────────────────────────────────────────────────────────
260
- export class Event extends BaseBuilder {
261
- static dispatchKeys = ['eventMessage']
262
- static shortcuts = {
263
- sendEventMessage: sock => (jid, data, quoted) => new Event(sock).from(data).send(jid, q(quoted)),
264
- }
265
-
266
- setName(n) { this.data.name = n; return this }
267
- setDescription(d) { this.data.description = d; return this }
268
- setLocation(l) { this.data.location = l; return this }
269
- setJoinLink(l) { this.data.joinLink = l; return this }
270
- setStartTime(t) { this.data.startTime = t; return this }
271
- setEndTime(t) { this.data.endTime = t; return this }
272
- setCanceled(v = true) { this.data.isCanceled = v; return this }
273
- setReminder(sec) { this.data.hasReminder = true; this.data.reminderOffsetSec = sec; return this }
274
- setScheduledCall(v = true) { this.data.isScheduleCall = v; return this }
275
-
276
- async send(jid, opts = {}) {
277
- const e = this.data
278
- const parseTime = (val, def) => typeof val === 'string' ? parseInt(val) : (val ?? def)
279
- const msg = 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 })
280
- await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
281
- return msg
282
- }
283
- }
284
-
285
- // ─── StickerPack ──────────────────────────────────────────────────────────────
286
- export class StickerPack extends BaseBuilder {
287
- static dispatchKeys = ['stickerPack', 'stickerPackMessage']
288
- static shortcuts = {
289
- stickerPackMessage: sock => (jid, data, opts = {}) => new StickerPack(sock).from(data, { name: opts.packName, publisher: opts.packPublisher }).send(jid, q(opts.quoted)),
290
- }
291
-
292
- setStickers(s) { this.data.stickers = s; return this }
293
-
294
- async send(jid, opts = {}) {
295
- const userJid = this.client.user?.id
296
- const { quoted, ...passOpts } = opts
297
- const result = await prepareStickerPackMessage(this.data, { upload: this.client.waUploadToServer, logger: this.client.logger, ...passOpts })
298
- if (result.isBatched) {
299
- let last
300
- for (let i = 0; i < result.stickerPackMessage.length; i++) {
301
- const msg = await generateWAMessageFromContent(jid, { stickerPackMessage: result.stickerPackMessage[i] }, { userJid, quoted })
302
- await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
303
- last = msg
304
- if (i < result.stickerPackMessage.length - 1) await delay(2000)
305
- }
306
- return last
307
- }
308
- const msg = await generateWAMessageFromContent(jid, { stickerPackMessage: result.stickerPackMessage }, { userJid, quoted })
309
- await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
310
- return msg
311
- }
312
- }
313
- // ─── VerticalAlbum ────────────────────────────────────────────────────────────
314
- export class VerticalAlbum extends BaseBuilder {
315
- static dispatchKeys = ['verticalAlbumMessage']
316
- static shortcuts = {
317
- sendVerticalAlbumMessage: sock => (jid, data, opts = {}) => new VerticalAlbum(sock).from(data, opts).send(jid, opts)
318
- }
319
-
320
- async send(jid, opts = {}) {
321
- const items = this.data.items ?? (Array.isArray(this.data.verticalAlbumMessage) ? this.data.verticalAlbumMessage : [])
322
- if (!Array.isArray(items) || items.length < 2) throw new Error('VerticalAlbum requires at least 2 items')
323
- const expiration = String(Date.now() + 30 * 24 * 60 * 60 * 1000)
324
- const primitives = items.map((item, i) => {
325
- const kind = item?.type || (item?.video ? 'video' : 'image')
326
- if (kind === 'video') {
327
- const v = item.video || item.data || item
328
- const url = typeof v === 'string' ? v : (v?.url ?? v?.videoUrl ?? '')
329
- if (!url) throw new Error(`VerticalAlbum item ${i}: video requires a URL`)
330
- 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 } } : {}) }
331
- }
332
- const img = item?.image || item?.data || item
333
- const url = typeof img === 'string' ? img : (img?.imageHighResUrl ?? img?.highResUrl ?? img?.fullUrl ?? img?.imagePreviewUrl ?? img?.previewUrl ?? img?.url ?? '')
334
- if (!url) throw new Error(`VerticalAlbum item ${i}: image requires a URL`)
335
- const w = Number(img?.width ?? 600), h = Number(img?.height ?? 400)
336
- 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 })
337
- 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' }
338
- })
339
- const unified = { response_id: crypto.randomUUID(), sections: [{ view_model: { primitives, display_mode: 'FULL_WIDTH', full_width: true, __typename: 'GenAIVStackLayoutViewModel' } }] }
340
- const msg = await generateWAMessageFromContent(jid, {
341
- messageContextInfo: { botMetadata: { pluginMetadata: {}, botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA } },
342
- 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 } } } }
343
- }, { userJid: this.client.user?.id })
344
- await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
345
- return msg
346
- }
347
- }
348
-
349
- // ─── GridImage ────────────────────────────────────────────────────────────────
350
- export class GridImage extends BaseBuilder {
351
- static dispatchKeys = ['gridImageMessage']
352
- static shortcuts = {
353
- sendGridImageMessage: sock => (jid, data, opts = {}) => new GridImage(sock).from(data, opts).send(jid, opts)
354
- }
355
-
356
- async send(jid, opts = {}) {
357
- const imageUrls = this.data.imageUrls ?? this.data.images ?? (Array.isArray(this.data.gridImageMessage) ? this.data.gridImageMessage : [])
358
- if (!Array.isArray(imageUrls) || imageUrls.length < 2) throw new Error('GridImage requires at least 2 images')
359
- const expiration = String(Date.now() + 30 * 24 * 60 * 60 * 1000)
360
- // GRID_IMAGE submessage type (6) — proper protocol path, not just unified sections
361
- const submessages = [{ messageType: 6, gridImageMetadata: { imageUrls } }]
362
- const sections = imageUrls.map(img => {
363
- const image = typeof img === 'string' ? { url: img } : img
364
- const url = image.imageHighResUrl ?? image.highResUrl ?? image.fullUrl ?? image.imagePreviewUrl ?? image.previewUrl ?? image.url ?? ''
365
- const w = Number(image.width ?? 600), h = Number(image.height ?? 400)
366
- 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 })
367
- 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' }
368
- })
369
- const unified = { response_id: crypto.randomUUID(), sections: [{ view_model: { primitives: sections, __typename: 'GenAIGridLayoutViewModel' } }] }
370
- const msg = await generateWAMessageFromContent(jid, {
371
- messageContextInfo: { botMetadata: { pluginMetadata: {}, botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA } },
372
- botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages, unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) }, contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' }, forwardOrigin: 4 } } } }
373
- }, { userJid: this.client.user?.id })
374
- await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
375
- return msg
376
- }
377
- }
378
-
379
- // ─── VideoGrid ────────────────────────────────────────────────────────────────
380
- export class VideoGrid extends BaseBuilder {
381
- static dispatchKeys = ['videoGridMessage']
382
- static shortcuts = {
383
- sendVideoGridMessage: sock => (jid, data, opts = {}) => new VideoGrid(sock).from(data, opts).send(jid, opts)
384
- }
385
-
386
- async send(jid, opts = {}) {
387
- const videos = this.data.videos ?? (Array.isArray(this.data.videoGridMessage) ? this.data.videoGridMessage : [])
388
- if (!Array.isArray(videos) || videos.length < 2) throw new Error('VideoGrid requires at least 2 videos')
389
- const primitives = videos.map((v, i) => {
390
- const url = typeof v === 'string' ? v : (v?.url ?? v?.videoUrl ?? v?.reelsUrl ?? '')
391
- if (!url) throw new Error(`VideoGrid item ${i}: requires a URL`)
392
- const item = typeof v === 'string' ? {} : v
393
- 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' }
394
- })
395
- const unified = { response_id: crypto.randomUUID(), sections: [{ view_model: { primitives, __typename: 'GenAIGridLayoutViewModel' } }] }
396
- const msg = await generateWAMessageFromContent(jid, {
397
- messageContextInfo: { botMetadata: { pluginMetadata: {}, botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA } },
398
- botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages: [], unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) }, contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' }, forwardOrigin: 4 } } } }
399
- }, { userJid: this.client.user?.id })
400
- await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
401
- return msg
402
- }
403
- }
404
-
405
- // ─── A2UI ─────────────────────────────────────────────────────────────────────
406
- export class A2UI extends BaseBuilder {
407
- static dispatchKeys = ['a2uiMessage']
408
- static shortcuts = {
409
- sendA2UIMessage: sock => (jid, data, opts = {}) => new A2UI(sock).from(data, opts).send(jid, opts),
410
- sendA2UICommandMenu: sock => (jid, data, opts = {}) => new A2UI(sock).from(data, { __commandMenu: true, ...opts }).send(jid, opts)
411
- }
412
-
413
- async send(jid, opts = {}) {
414
- const d = this.data
415
- let content
416
- if (d.__commandMenu) {
417
- // command menu preset — builds bloksWidget + nativeFlowMessage together
418
- 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}`) }))
419
- 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 ?? '') }))
420
- const componentIds = ['title', 'divider', 'tableCard', 'footer', ...(buttons.length ? ['buttonRow'] : [])]
421
- const components = [
422
- { id: 'root', component: 'Column', align: 'center', children: componentIds },
423
- { id: 'title', component: 'Text', text: String(d.title ?? 'Menu'), variant: 'h2' },
424
- { id: 'divider', component: 'Divider' },
425
- { id: 'tableCard', component: 'Card', child: 'tableColumn' },
426
- { id: 'tableColumn', component: 'Column', children: ['headerRow', ...rows.flatMap((_, i) => [`divider_${i}`, `row_${i}`])] },
427
- { id: 'headerRow', component: 'Row', children: ['headerCommand', 'headerFunction'] },
428
- { id: 'headerCommand', component: 'Text', text: 'Command', variant: 'h5' },
429
- { id: 'headerFunction', component: 'Text', text: 'Function', variant: 'h5' },
430
- ...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 }]),
431
- { id: 'footer', component: 'Text', text: d.footer ?? 'Powered by zervonex', variant: 'caption' }
432
- ]
433
- if (buttons.length) {
434
- components.push({ id: 'buttonRow', component: 'Row', justify: 'spaceEvenly', children: buttons.flatMap((b, i) => [`buttonLabel_${i}`, `button_${i}`]) })
435
- 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 } } }) })
436
- }
437
- const surfaceId = `zervonex-a2ui-${crypto.randomUUID()}`
438
- 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 }] }) }] } } }
439
- } else {
440
- // generic A2UI envelope
441
- const { imageMessage, bloksWidget = {}, nativeFlowMessage, fallback = '', ...rest } = d
442
- 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 } : {}) } } : {}) } }
443
- }
444
- const msg = await generateWAMessageFromContent(jid, content, { userJid: this.client.user?.id })
445
- const bizNode = getBizBinaryNode(msg.message)
446
- await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
447
- return msg
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
  }