@zervonex/baileys 3.0.8 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import crypto from 'crypto'
2
2
  import { proto } from '../../WAProto/index.js'
3
- import { generateWAMessage, generateWAMessageFromContent, getUrlInfo, prepareWAMessageMedia } from '../Utils/index.js'
3
+ import { generateWAMessage, generateWAMessageFromContent, getUrlInfo, prepareWAMessageMedia, resolveNativeFlowButtons } from '../Utils/index.js'
4
4
 
5
5
  export const getRandomHex = () => '#' + Array.from(crypto.getRandomValues(new Uint8Array(3)), b => b.toString(16).padStart(2, '0')).join('')
6
6
 
@@ -100,7 +100,7 @@ export async function handlePollResult(sock, jid, p) {
100
100
  pollResultSnapshotMessage: proto.Message.PollResultSnapshotMessage.create({
101
101
  name: p.name,
102
102
  pollVotes: (p.pollVotes ?? []).map(v => proto.Message.PollResultSnapshotMessage.PollVote.create({ optionName: v.optionName, optionVoteCount: String(v.optionVoteCount ?? 0) })),
103
- contextInfo: proto.ContextInfo.create({ isForwarded: true, forwardingScore: 1, forwardedNewsletterMessageInfo: proto.ContextInfo.ForwardedNewsletterMessageInfo.create({ newsletterName: p.newsletter?.newsletterName ?? 'Newsletter', newsletterJid: p.newsletter?.newsletterJid ?? '120363399602691477@newsletter', serverMessageId: 1000, contentType: 'UPDATE' }) })
103
+ contextInfo: proto.ContextInfo.create({ isForwarded: true, forwardingScore: 1 })
104
104
  })
105
105
  }, { userJid: sock.user?.id })
106
106
  await sock.relayMessage(jid, msg.message, { messageId: msg.key.id })
@@ -109,6 +109,16 @@ export async function handlePollResult(sock, jid, p) {
109
109
 
110
110
  export const q = quoted => quoted ? { quoted } : {}
111
111
 
112
+ export async function handleNativeFlow(sock, jid, content, opts = {}) {
113
+ const rawButtons = content.nativeFlow
114
+ const resolved = resolveNativeFlowButtons(rawButtons)
115
+ const offerParams = content.offerText ? { limited_time_offer: { text: content.offerText, url: content.offerUrl || '', copy_code: content.offerCode || '', expiration_time: content.offerExpiration ? Math.floor(content.offerExpiration / 1000) : 0 } } : {}
116
+ const messageParams = { ...offerParams }
117
+ if (content.optionText) messageParams.bottom_sheet = { in_thread_buttons_limit: 1, divider_indices: Array.from({ length: resolved.length }, (_, i) => i), list_title: content.optionTitle || content.optionText, button_title: content.optionText }
118
+ if (content.interactiveAsTemplate) return sock.sendMessage(jid, { ...content, nativeFlow: undefined, interactiveButtons: undefined, templateButtons: resolved.map(b => { const p = JSON.parse(b.buttonParamsJson ?? '{}'); return b.name === 'cta_url' ? { text: p.display_text, url: p.url } : b.name === 'cta_call' ? { text: p.display_text, call: p.phone_number } : { text: p.display_text, id: p.id } }) }, opts)
119
+ return sock.sendMessage(jid, { ...content, nativeFlow: undefined, interactiveButtons: resolved, ...(Object.keys(messageParams).length ? { messageParamsJson: JSON.stringify(messageParams) } : {}) }, opts)
120
+ }
121
+
112
122
  export const PRIMITIVE_SHORTCUTS = {
113
123
  sendText: sock => (jid, text, opts = {}) => sock.sendMessage(jid, { text }, opts),
114
124
  sendImage: sock => (jid, image, caption = '', opts = {}) => sock.sendMessage(jid, { image, caption }, opts),
@@ -126,5 +136,6 @@ export const HANDLER_SHORTCUTS = {
126
136
  sendStatusMentionMessage: (sock, config) => (jid, data) => handleStatusMention(sock, jid, norm(data), config),
127
137
  sendStatusMentions: (sock, config) => (jid, data) => handleStatusMention(sock, jid, norm(data), config),
128
138
  sendPollResultMessage: sock => (jid, data) => handlePollResult(sock, jid, norm(data)),
129
- sendGroupStatusMessage: (sock, config) => (jid, content, opts = {}) => handleGroupStatus(sock, jid, norm(content), config)
139
+ sendGroupStatusMessage: (sock, config) => (jid, content, opts = {}) => handleGroupStatus(sock, jid, norm(content), config),
140
+ sendNativeFlow: sock => (jid, content, opts = {}) => handleNativeFlow(sock, jid, norm(content), opts)
130
141
  }
@@ -1,7 +1,7 @@
1
1
  import crypto from 'crypto'
2
2
  import { proto } from '../../WAProto/index.js'
3
3
  import { BOT_RENDERING_CONFIG_METADATA } from '../Defaults/index.js'
4
- import { generateWAMessage, generateWAMessageFromContent, generateMessageIDV2, prepareStickerPackMessage, prepareWAMessageMedia } from '../Utils/index.js'
4
+ import { generateWAMessage, generateWAMessageFromContent, generateMessageIDV2, prepareStickerPackMessage, prepareWAMessageMedia, resolveNativeFlowButtons } from '../Utils/index.js'
5
5
  import { getBizBinaryNode } from '../WABinary/index.js'
6
6
  import { BaseBuilder, norm, q } from './base.js'
7
7
 
@@ -43,9 +43,7 @@ export class Button {
43
43
  setLimitedTimeOffer(exp) { this.#params.limited_time_offer = { expiration_time_ms: exp }; return this }
44
44
  setBottomSheet(opts = {}) { this.#params.bottom_sheet = opts; return this }
45
45
  setTapTargetConfiguration(opts = {}) { this.#params.tap_target_configuration = opts; return this }
46
-
47
46
  #addNative(name, params) { this.#mode = 'native'; this.#buttons.push({ name, buttonParamsJson: JSON.stringify(params) }); return this }
48
-
49
47
  reply(text, id) { return this.#addNative('quick_reply', { display_text: text, id }) }
50
48
  url(text, url, merchantUrl) { return this.#addNative('cta_url', { display_text: text, url, merchant_url: merchantUrl ?? url }) }
51
49
  call(text, phone) { return this.#addNative('cta_call', { display_text: text, phone_number: phone }) }
@@ -118,7 +116,7 @@ export class Interactive extends BaseBuilder {
118
116
  const footerText = typeof i.footer === 'string' ? i.footer : (i.footer?.text ?? '')
119
117
  const headerTitle = typeof i.header === 'string' ? i.header : (i.header?.title ?? '')
120
118
  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 ?? '' }) }
119
+ if (i.buttons?.length || i.nativeFlowMessage) { const nfm = i.nativeFlowMessage ?? {}; const rawButtons = i.buttons ?? nfm.buttons ?? []; nativeFlow = proto.Message.InteractiveMessage.NativeFlowMessage.create({ buttons: resolveNativeFlowButtons(rawButtons), messageParamsJson: nfm.messageParamsJson ?? '' }) }
122
120
  const headerMedia = {}
123
121
  if (media?.imageMessage) headerMedia.imageMessage = media.imageMessage
124
122
  if (media?.videoMessage) headerMedia.videoMessage = media.videoMessage
@@ -139,7 +137,7 @@ export class Carousel extends BaseBuilder {
139
137
 
140
138
  static dispatchKeys = ['carouselMessage']
141
139
  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)) },
140
+ 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 => { const resolved = card.nativeFlow ? { ...card, nativeFlow: undefined, nativeFlowMessage: { buttons: resolveNativeFlowButtons(Array.isArray(card.nativeFlow) ? card.nativeFlow : [card.nativeFlow]) } } : card; c.card(resolved) }); return c.send(jid, q(quoted)) },
143
141
  sendCarouselProtoMessage: sock => (jid, data, quoted) => Carousel.shortcuts.sendCarouselMessage(sock)(jid, data, quoted),
144
142
  }
145
143
 
@@ -150,20 +148,20 @@ export class Carousel extends BaseBuilder {
150
148
  if (d.caption) this.setCaption(d.caption)
151
149
  if (d.footer) this.setFooter(d.footer)
152
150
  if (d.cardType) this.setCardType(d.cardType)
153
- ; (d.cards ?? []).forEach(card => this.card(card))
151
+ ; (d.cards ?? []).forEach(card => { const resolved = card.nativeFlow ? { ...card, nativeFlow: undefined, nativeFlowMessage: { buttons: resolveNativeFlowButtons(Array.isArray(card.nativeFlow) ? card.nativeFlow : [card.nativeFlow]) } } : card; this.card(resolved) })
154
152
  return this
155
153
  }
156
154
 
157
155
  setCaption(t) { this.#caption = t; return this }
158
156
  setFooter(t) { this.#footer = t; return this }
159
157
  setCardType(t) { this.#cardType = t; return this }
160
- card(b) { this.#cards.push(b instanceof Button ? b.toCard() : b); return this }
158
+ card(b) { this.#cards.push(b instanceof Button ? b.toCard() : (b.text || b.url || b.id ? new Button(this.client).reply(b.text ?? '', b.id ?? '').toCard() : b)); return this }
161
159
 
162
160
  async send(jid, opts = {}) {
163
161
  if (this.#cards.length < 2) throw new Error('Carousel requires at least 2 cards')
164
162
  if (this.#cards.length > 10) throw new Error('Carousel max 10 cards')
165
163
  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 })
164
+ 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: resolveNativeFlowButtons(c.nativeFlowMessage?.buttons ?? []) }) })), messageVersion: 1, carouselCardType: this.#cardType === 'ALBUM_IMAGE' ? 2 : 1 }) }) }, { userJid: this.client.user?.id, waUploadToServer: this.client.waUploadToServer })
167
165
  const bizNode = getBizBinaryNode(msg.message)
168
166
  await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
169
167
  return msg
@@ -234,13 +232,17 @@ export class Payment extends BaseBuilder {
234
232
  setExpiry(t) { this.data.expiry = t; return this }
235
233
  setNote(n) { this.data.note = n; return this }
236
234
  setFrom(j) { this.data.from = j; return this }
235
+ setBackground(b) { this.data.background = b; return this }
237
236
 
238
237
  async send(jid, opts = {}) {
239
238
  const d = this.data
239
+ const currency = d.currency ?? 'NGN'
240
+ const amount = d.amount ?? 0
240
241
  const ctx = opts.quoted ? { stanzaId: opts.quoted.key?.id, participant: opts.quoted.key?.participant, quotedMessage: opts.quoted.message } : {}
241
242
  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 })
243
+ const msg = await generateWAMessageFromContent(jid, { requestPaymentMessage: proto.Message.RequestPaymentMessage.fromObject({ expiryTimestamp: d.expiry ?? 0, amount1000: amount, currencyCodeIso4217: currency, requestFrom: d.from ?? '0@s.whatsapp.net', noteMessage: notes, amount: proto.Money.create({ value: amount, offset: 1000, currencyCode: currency }), ...(d.background ? { background: proto.PaymentBackground.create({ id: d.background.id ?? 'DEFAULT', placeholderArgb: d.background.placeholderArgb ?? 0xfff0f0f0, textArgb: d.background.textArgb ?? 0, subtextArgb: d.background.subtextArgb ?? 0, type: proto.PaymentBackground.Type.DEFAULT }) } : {}) }) }, { userJid: this.client.user?.id, waUploadToServer: this.client.waUploadToServer })
244
+ const bizNode = getBizBinaryNode(msg.message)
245
+ await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
244
246
  return msg
245
247
  }
246
248
  }
@@ -262,8 +264,9 @@ export class Order extends BaseBuilder {
262
264
  }
263
265
  const cleanJid = id => { if (!id) return null; const [user] = id.split(':'); return user.includes('@') ? user : `${user}@s.whatsapp.net` }
264
266
  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
+ const msg = await generateWAMessageFromContent(jid, { orderMessage: proto.Message.OrderMessage.create({ orderId: o.orderId ?? `zervonex${Date.now()}`, thumbnail: thumb, itemCount: o.itemCount ?? 0, status: o.status ?? proto.Message.OrderMessage.OrderStatus.INQUIRY, surface: o.surface ?? proto.Message.OrderMessage.OrderSurface.CATALOG, message: o.message, orderTitle: o.orderTitle, sellerJid: seller, token: o.token ?? 'zervonex_TOKEN', totalAmount1000: o.totalAmount1000 ?? 0, totalCurrencyCode: o.totalCurrencyCode ?? 'NGN', messageVersion: o.messageVersion ?? 2, ...(o.catalogType ? { catalogType: o.catalogType } : {}), ...(o.contextInfo ? { contextInfo: o.contextInfo } : {}) }) }, { userJid, quoted: opts.quoted, waUploadToServer: this.client.waUploadToServer })
268
+ const bizNode = getBizBinaryNode(msg.message)
269
+ await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
267
270
  return msg
268
271
  }
269
272
  }
@@ -271,25 +274,27 @@ export class Order extends BaseBuilder {
271
274
  // ─── Event ────────────────────────────────────────────────────────────────────
272
275
  export class Event extends BaseBuilder {
273
276
  static dispatchKeys = ['eventMessage']
274
- static shortcuts = {
275
- sendEventMessage: sock => (jid, data, quoted) => new Event(sock).from(data).send(jid, q(quoted)),
276
- }
277
+ static shortcuts = { sendEventMessage: sock => (jid, data, quoted) => new Event(sock).from(data).send(jid, q(quoted)) }
277
278
 
278
279
  setName(n) { this.data.name = n; return this }
279
280
  setDescription(d) { this.data.description = d; return this }
280
281
  setLocation(l) { this.data.location = l; return this }
281
282
  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 }
283
+ setStartTime(t) { this.data.startDate = t instanceof Date ? t : new Date(t); return this }
284
+ setEndTime(t) { this.data.endDate = t instanceof Date ? t : new Date(t); return this }
284
285
  setCanceled(v = true) { this.data.isCanceled = v; return this }
285
286
  setReminder(sec) { this.data.hasReminder = true; this.data.reminderOffsetSec = sec; return this }
286
287
  setScheduledCall(v = true) { this.data.isScheduleCall = v; return this }
288
+ setMessageSecret(s) { this.data.messageSecret = s; return this }
287
289
 
288
290
  async send(jid, opts = {}) {
289
291
  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 })
292
+ if (!e.name) throw new Error('Event name is required')
293
+ const startDate = e.startDate instanceof Date ? e.startDate : new Date(e.startDate ?? Date.now())
294
+ const endDate = e.endDate ? (e.endDate instanceof Date ? e.endDate : new Date(e.endDate)) : null
295
+ const msg = await generateWAMessageFromContent(jid, { messageContextInfo: proto.MessageContextInfo.create({ messageSecret: e.messageSecret || crypto.randomBytes(32) }), eventMessage: proto.Message.EventMessage.create({ isCanceled: e.isCanceled ?? false, name: e.name, description: e.description ?? '', location: e.location ?? { degreesLatitude: 0, degreesLongitude: 0, name: 'Location' }, joinLink: e.joinLink ?? '', startTime: Math.floor(startDate.getTime() / 1000), endTime: endDate ? Math.floor(endDate.getTime() / 1000) : undefined, extraGuestsAllowed: e.extraGuestsAllowed !== false, isScheduleCall: e.isScheduleCall ?? false, hasReminder: e.hasReminder ?? false, reminderOffsetSec: e.reminderOffsetSec ?? 0 }) }, { userJid: this.client.user?.id, quoted: opts.quoted, waUploadToServer: this.client.waUploadToServer })
296
+ const bizNode = getBizBinaryNode(msg.message)
297
+ await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
293
298
  return msg
294
299
  }
295
300
  }
@@ -353,7 +358,9 @@ export class VerticalAlbum extends BaseBuilder {
353
358
  messageContextInfo: { botMetadata: { pluginMetadata: {}, botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA } },
354
359
  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
360
  }, { userJid: this.client.user?.id, waUploadToServer: this.client.waUploadToServer })
356
- await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
361
+ const bizNode = getBizBinaryNode(msg.message)
362
+ const relayFn = this.client.relayRichMessage ?? this.client.relayMessage
363
+ await relayFn(jid, msg, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
357
364
  return msg
358
365
  }
359
366
  }
@@ -383,7 +390,9 @@ export class GridImage extends BaseBuilder {
383
390
  messageContextInfo: { botMetadata: { pluginMetadata: {}, botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA } },
384
391
  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
392
  }, { userJid: this.client.user?.id, waUploadToServer: this.client.waUploadToServer })
386
- await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
393
+ const bizNode = getBizBinaryNode(msg.message)
394
+ const relayFn = this.client.relayRichMessage ?? this.client.relayMessage
395
+ await relayFn(jid, msg, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
387
396
  return msg
388
397
  }
389
398
  }
@@ -409,51 +418,134 @@ export class VideoGrid extends BaseBuilder {
409
418
  messageContextInfo: { botMetadata: { pluginMetadata: {}, botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA } },
410
419
  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
420
  }, { userJid: this.client.user?.id, waUploadToServer: this.client.waUploadToServer })
412
- await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id })
421
+ const bizNode = getBizBinaryNode(msg.message)
422
+ const relayFn = this.client.relayRichMessage ?? this.client.relayMessage
423
+ await relayFn(jid, msg, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
413
424
  return msg
414
425
  }
415
426
  }
416
427
 
417
428
  // ─── A2UI ─────────────────────────────────────────────────────────────────────
418
429
  export class A2UI extends BaseBuilder {
430
+ static VERSION = 'v0.9'
431
+ static get CATALOG() { return `https://a2ui.org/specification/${this.VERSION}/catalogs/basic/catalog.json` }
419
432
  static dispatchKeys = ['a2uiMessage']
420
433
  static shortcuts = {
421
434
  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)
435
+ sendA2UICommandMenu: sock => (jid, data, opts = {}) => new A2UI(sock).from(data, { __commandMenu: true, ...opts }).send(jid, opts),
436
+ sendA2UI: sock => (jid, components, opts = {}) => new A2UI(sock).addComponents(components).send(jid, opts)
423
437
  }
424
-
438
+ static #v = { imgVariant: ['icon', 'avatar', 'smallFeature', 'mediumFeature', 'largeFeature', 'header'], imgFit: ['contain', 'cover', 'fill', 'none', 'scaleDown'], textVariant: ['h1', 'h2', 'h3', 'h4', 'h5', 'caption', 'body'], btnVariant: ['default', 'primary', 'borderless'], colJustify: ['start', 'center', 'end', 'spaceBetween', 'spaceAround', 'spaceEvenly', 'stretch'], rowJustify: ['center', 'end', 'spaceAround', 'spaceBetween', 'spaceEvenly', 'start', 'stretch'], align: ['start', 'center', 'end', 'stretch'], listDir: ['vertical', 'horizontal'], tfVariant: ['longText', 'number', 'shortText', 'obscured'], cpVariant: ['mutuallyExclusive', 'multipleSelection'], cpDisplay: ['checkbox', 'chips'], divAxis: ['horizontal', 'vertical'], icons: ['accountCircle', 'add', 'arrowBack', 'arrowForward', 'attachFile', 'calendarToday', 'call', 'camera', 'check', 'close', 'delete', 'download', 'edit', 'event', 'error', 'fastForward', 'favorite', 'favoriteOff', 'folder', 'help', 'home', 'info', 'locationOn', 'lock', 'lockOpen', 'mail', 'menu', 'moreVert', 'moreHoriz', 'notificationsOff', 'notifications', 'pause', 'payment', 'person', 'phone', 'photo', 'play', 'print', 'refresh', 'rewind', 'search', 'send', 'settings', 'share', 'shoppingCart', 'skipNext', 'skipPrevious', 'star', 'starHalf', 'starOff', 'stop', 'upload', 'visibility', 'visibilityOff', 'volumeDown', 'volumeMute', 'volumeOff', 'volumeUp', 'warning'] }
439
+ static #pick(val, list, def) { return list.includes(val) ? val : def }
440
+ static #id(id, name) { if (!id) throw new TypeError(`${name} requires an id`); return id }
441
+ static text(id, text, { variant = 'body' } = {}) { return { id: A2UI.#id(id, 'text'), component: 'Text', text: String(text), variant: A2UI.#pick(variant, A2UI.#v.textVariant, 'body') } }
442
+ static image(id, input, { variant = 'mediumFeature', fit = 'fill' } = {}) { if (!input) throw new TypeError('image requires a url, buffer, or file path'); return { id: A2UI.#id(id, 'image'), component: 'Image', url: '', _raw: input, variant: A2UI.#pick(variant, A2UI.#v.imgVariant, 'mediumFeature'), fit: A2UI.#pick(fit, A2UI.#v.imgFit, 'fill') } }
443
+ static icon(id, name) { if (!name) throw new TypeError('icon requires a name'); if (typeof name === 'string' && !A2UI.#v.icons.includes(name)) throw new TypeError(`invalid icon: ${name}`); return { id: A2UI.#id(id, 'icon'), component: 'Icon', name } }
444
+ static video(id, url) { if (!url) throw new TypeError('video requires a url'); return { id: A2UI.#id(id, 'video'), component: 'Video', url: String(url) } }
445
+ static audio(id, url, { description = '' } = {}) { if (!url) throw new TypeError('audio requires a url'); return { id: A2UI.#id(id, 'audio'), component: 'AudioPlayer', url: String(url), ...(description ? { description: String(description) } : {}) } }
446
+ static column(id, children = [], { justify = 'start', align = 'stretch' } = {}) { if (!children.length) throw new TypeError('column requires at least one child'); return { id: A2UI.#id(id, 'column'), component: 'Column', children: children.map(String), justify: A2UI.#pick(justify, A2UI.#v.colJustify, 'start'), align: A2UI.#pick(align, A2UI.#v.align, 'stretch') } }
447
+ static row(id, children = [], { justify = 'start', align = 'stretch' } = {}) { if (!children.length) throw new TypeError('row requires at least one child'); return { id: A2UI.#id(id, 'row'), component: 'Row', children: children.map(String), justify: A2UI.#pick(justify, A2UI.#v.rowJustify, 'start'), align: A2UI.#pick(align, A2UI.#v.align, 'stretch') } }
448
+ static list(id, children = [], { direction = 'vertical', align = 'stretch' } = {}) { if (!children.length) throw new TypeError('list requires at least one child'); return { id: A2UI.#id(id, 'list'), component: 'List', children: children.map(String), direction: A2UI.#pick(direction, A2UI.#v.listDir, 'vertical'), align: A2UI.#pick(align, A2UI.#v.align, 'stretch') } }
449
+ static card(id, child) { if (!child) throw new TypeError('card requires a child id'); return { id: A2UI.#id(id, 'card'), component: 'Card', child: String(child) } }
450
+ static tabs(id, tabs = []) { if (!tabs.length) throw new TypeError('tabs requires at least one tab'); return { id: A2UI.#id(id, 'tabs'), component: 'Tabs', tabs: tabs.map((t, i) => ({ title: String(t.title ?? t.label ?? `Tab ${i + 1}`), child: String(t.child) })) } }
451
+ static modal(id, trigger, content) { if (!trigger) throw new TypeError('modal requires a trigger id'); if (!content) throw new TypeError('modal requires a content id'); return { id: A2UI.#id(id, 'modal'), component: 'Modal', trigger: String(trigger), content: String(content) } }
452
+ static divider(id, { axis = 'horizontal' } = {}) { return { id: A2UI.#id(id, 'divider'), component: 'Divider', axis: A2UI.#pick(axis, A2UI.#v.divAxis, 'horizontal') } }
453
+ static button(id, child, action, { variant = 'default' } = {}) { if (!child) throw new TypeError('button requires a child id'); if (!action) throw new TypeError('button requires an action'); return { id: A2UI.#id(id, 'button'), component: 'Button', child: String(child), action, variant: A2UI.#pick(variant, A2UI.#v.btnVariant, 'default') } }
454
+ static textField(id, label, { value = '', variant = 'shortText', validationRegexp = '' } = {}) { if (!label) throw new TypeError('textField requires a label'); return { id: A2UI.#id(id, 'textField'), component: 'TextField', label: String(label), value: String(value), variant: A2UI.#pick(variant, A2UI.#v.tfVariant, 'shortText'), ...(validationRegexp ? { validationRegexp: String(validationRegexp) } : {}) } }
455
+ static checkBox(id, label, value = false) { if (!label) throw new TypeError('checkBox requires a label'); return { id: A2UI.#id(id, 'checkBox'), component: 'CheckBox', label: String(label), value: Boolean(value) } }
456
+ static choicePicker(id, options = [], value = [], { label = '', variant = 'mutuallyExclusive', displayStyle = 'checkbox', filterable = false } = {}) { if (!options.length) throw new TypeError('choicePicker requires at least one option'); return { id: A2UI.#id(id, 'choicePicker'), component: 'ChoicePicker', options: options.map(o => typeof o === 'string' ? { label: o, value: o } : { label: String(o.label), value: String(o.value) }), value: (Array.isArray(value) ? value : [value]).map(String), ...(label ? { label: String(label) } : {}), variant: A2UI.#pick(variant, A2UI.#v.cpVariant, 'mutuallyExclusive'), displayStyle: A2UI.#pick(displayStyle, A2UI.#v.cpDisplay, 'checkbox'), filterable: Boolean(filterable) } }
457
+ static slider(id, value, max, { label = '', min = 0 } = {}) { if (max == null) throw new TypeError('slider requires a max'); return { id: A2UI.#id(id, 'slider'), component: 'Slider', value: Number(value), max: Number(max), min: Number(min), ...(label ? { label: String(label) } : {}) } }
458
+ static dateTimeInput(id, value = '', { label = '', enableDate = false, enableTime = false, min = '', max = '' } = {}) { return { id: A2UI.#id(id, 'dateTimeInput'), component: 'DateTimeInput', value: String(value), enableDate: Boolean(enableDate), enableTime: Boolean(enableTime), ...(label ? { label: String(label) } : {}), ...(min ? { min: String(min) } : {}), ...(max ? { max: String(max) } : {}) } }
459
+ static surface(components, { surfaceId, catalogId = A2UI.CATALOG, version = A2UI.VERSION } = {}) { const id = surfaceId ?? `zervonex-a2ui-${crypto.randomUUID()}`; return Buffer.from(JSON.stringify({ version, createSurface: { surfaceId: id, catalogId, sendDataModel: false, components } }), 'utf8') }
460
+ static async #resolveImage(input, upload) {
461
+ if (Buffer.isBuffer(input)) { const p = await prepareWAMessageMedia({ image: input }, { upload }); return { url: p.imageMessage?.url ?? '' } }
462
+ if (typeof input === 'string' && !input.startsWith('http')) { const { readFile } = await import('node:fs/promises'); const p = await prepareWAMessageMedia({ image: await readFile(input) }, { upload }); return { url: p.imageMessage?.url ?? '' } }
463
+ if (typeof input === 'string') return { url: input }
464
+ if (input?.url) return { url: input.url }
465
+ if (input?.image) { const p = await prepareWAMessageMedia({ image: input.image }, { upload }); return { url: p.imageMessage?.url ?? '' } }
466
+ throw new TypeError('invalid image input')
467
+ }
468
+ #components = []; #buttons = []; #surfaceId = `zervonex-a2ui-${crypto.randomUUID()}`; #fallback = ''; #imageMessage = null
469
+ async #resolveComponents() {
470
+ const upload = this.client.waUploadToServer
471
+ return Promise.all(this.#components.map(async c => {
472
+ if (c.component !== 'Image' || !c._raw) return c
473
+ const { _raw, ...rest } = c
474
+ const { url } = await A2UI.#resolveImage(_raw, upload)
475
+ return { ...rest, url }
476
+ }))
477
+ }
478
+ async #buildHeader(upload) {
479
+ if (!this.#imageMessage) return { header: { hasMediaAttachment: false } }
480
+ const input = this.#imageMessage
481
+ if (Buffer.isBuffer(input)) { const p = await prepareWAMessageMedia({ image: input }, { upload }); return { header: { ...p, hasMediaAttachment: true } } }
482
+ if (input?.image) { const p = await prepareWAMessageMedia({ image: input.image }, { upload }); return { header: { ...p, hasMediaAttachment: true } } }
483
+ const url = input?.url ?? (typeof input === 'string' ? input : null)
484
+ if (url) return { header: { imageMessage: { url }, hasMediaAttachment: true } }
485
+ return { header: { hasMediaAttachment: false } }
486
+ }
487
+ addComponent(obj) { if (!obj?.id) throw new TypeError('addComponent requires an object with an id'); this.#components.push(obj); return this }
488
+ addComponents(arr) { if (!Array.isArray(arr)) throw new TypeError('addComponents requires an array'); for (const c of arr) this.addComponent(c); return this }
489
+ addText(id, text, opts = {}) { return this.addComponent(A2UI.text(id, text, opts)) }
490
+ addImage(id, input, opts = {}) { return this.addComponent(A2UI.image(id, input, opts)) }
491
+ addIcon(id, name) { return this.addComponent(A2UI.icon(id, name)) }
492
+ addVideo(id, url) { return this.addComponent(A2UI.video(id, url)) }
493
+ addAudio(id, url, opts = {}) { return this.addComponent(A2UI.audio(id, url, opts)) }
494
+ addColumn(id, children = [], opts = {}) { return this.addComponent(A2UI.column(id, children, opts)) }
495
+ addRow(id, children = [], opts = {}) { return this.addComponent(A2UI.row(id, children, opts)) }
496
+ addList(id, children = [], opts = {}) { return this.addComponent(A2UI.list(id, children, opts)) }
497
+ addCard(id, child) { return this.addComponent(A2UI.card(id, child)) }
498
+ addTabs(id, tabs = []) { return this.addComponent(A2UI.tabs(id, tabs)) }
499
+ addModal(id, trigger, content) { return this.addComponent(A2UI.modal(id, trigger, content)) }
500
+ addDivider(id, opts = {}) { return this.addComponent(A2UI.divider(id, opts)) }
501
+ addButtonComponent(id, child, action, opts = {}) { return this.addComponent(A2UI.button(id, child, action, opts)) }
502
+ addTextField(id, label, opts = {}) { return this.addComponent(A2UI.textField(id, label, opts)) }
503
+ addCheckBox(id, label, value = false) { return this.addComponent(A2UI.checkBox(id, label, value)) }
504
+ addChoicePicker(id, options, value, opts = {}) { return this.addComponent(A2UI.choicePicker(id, options, value, opts)) }
505
+ addSlider(id, value, max, opts = {}) { return this.addComponent(A2UI.slider(id, value, max, opts)) }
506
+ addDateTimeInput(id, value, opts = {}) { return this.addComponent(A2UI.dateTimeInput(id, value, opts)) }
507
+ addButton(nameOrShorthand, params = {}) {
508
+ if (!nameOrShorthand) throw new TypeError('addButton requires a name or shorthand object')
509
+ if (typeof nameOrShorthand === 'object') { const [r] = resolveNativeFlowButtons([nameOrShorthand]); this.#buttons.push(r) }
510
+ else this.#buttons.push({ name: nameOrShorthand, buttonParamsJson: typeof params === 'string' ? params : JSON.stringify(params) })
511
+ return this
512
+ }
513
+ setFallback(text) { this.#fallback = String(text); return this }
514
+ setSurfaceId(id) { this.#surfaceId = String(id); return this }
515
+ setImage(input) { if (!input) throw new TypeError('setImage requires a url, buffer, or file path'); this.#imageMessage = input; return this }
425
516
  async send(jid, opts = {}) {
426
517
  const d = this.data
518
+ if (d.components?.length) { this.addComponents(d.components); if (d.buttons?.length) for (const b of d.buttons) this.#buttons.push(b) }
519
+ const upload = this.client.waUploadToServer
520
+ if (this.#components.length) {
521
+ if (!this.#components.some(c => c.id === 'root')) throw new Error('A2UI components must include one with id "root"')
522
+ const [components, { header }] = await Promise.all([this.#resolveComponents(), this.#buildHeader(upload)])
523
+ const buttons = [...this.#buttons, ...(opts.buttons ?? [])]
524
+ const content = { interactiveMessage: { header, body: { text: d.body ?? d.text ?? '\u200b' }, bloksWidget: { uuid: this.#surfaceId, type: 'im_a2ui', data: A2UI.surface(components, { surfaceId: this.#surfaceId }), fallback: this.#fallback }, ...(buttons.length ? { nativeFlowMessage: { messageParamsJson: JSON.stringify(opts.nativeFlowParams ?? {}), buttons, messageVersion: 1 } } : {}) } }
525
+ const msg = await generateWAMessageFromContent(jid, content, { userJid: this.client.user?.id, waUploadToServer: upload })
526
+ const bizNode = getBizBinaryNode(msg.message)
527
+ await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
528
+ return msg
529
+ }
427
530
  let content
428
531
  if (d.__commandMenu) {
429
- // command menu preset — builds bloksWidget + nativeFlowMessage together
430
532
  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
533
  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
- }
534
+ const components = [{ id: 'root', component: 'Column', align: 'center', children: ['title', 'divider', 'tableCard', 'footer', ...(buttons.length ? ['buttonRow'] : [])] }, { id: 'title', component: 'Text', text: String(d.title ?? 'Menu'), variant: 'h2' }, { id: 'divider', component: 'Divider' }, { id: 'tableCard', component: 'Card', child: 'tableColumn' }, { id: 'tableColumn', component: 'Column', children: ['headerRow', ...rows.flatMap((_, i) => [`divider_${i}`, `row_${i}`])] }, { id: 'headerRow', component: 'Row', children: ['headerCommand', 'headerFunction'] }, { id: 'headerCommand', component: 'Text', text: 'Command', variant: 'h5' }, { id: 'headerFunction', component: 'Text', text: 'Function', variant: 'h5' }, ...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 }]), { id: 'footer', component: 'Text', text: String(d.footer ?? 'Powered by zervonex'), variant: 'caption' }]
535
+ if (buttons.length) { components.push({ id: 'buttonRow', component: 'Row', justify: 'spaceEvenly', children: buttons.flatMap((_, i) => [`buttonLabel_${i}`, `button_${i}`]) }); 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 } } }) }) }
449
536
  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 }] }) }] } } }
537
+ let cmdHeader = { hasMediaAttachment: false }
538
+ if (d.imageMessage) cmdHeader = { imageMessage: d.imageMessage, hasMediaAttachment: true }
539
+ else if (d.image) { const { url } = await A2UI.#resolveImage(d.image, upload); cmdHeader = { imageMessage: { url }, hasMediaAttachment: true } }
540
+ content = { interactiveMessage: { header: cmdHeader, body: { text: d.body ?? d.text ?? '\u200b' }, bloksWidget: { uuid: surfaceId, type: 'im_a2ui', data: A2UI.surface(components, { surfaceId }), 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 }] }) }], messageVersion: 1 } } }
451
541
  } 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 } : {}) } } : {}) } }
542
+ const { imageMessage, image, bloksWidget = {}, nativeFlowMessage, fallback = '', ...rest } = d
543
+ let elseHeader = { hasMediaAttachment: false }
544
+ if (imageMessage) elseHeader = { imageMessage, hasMediaAttachment: true }
545
+ else if (image) { const { url } = await A2UI.#resolveImage(image, upload); elseHeader = { imageMessage: { url }, hasMediaAttachment: true } }
546
+ content = { interactiveMessage: { ...rest, header: elseHeader, body: { text: d.body ?? d.text ?? '\u200b' }, bloksWidget: { uuid: bloksWidget.uuid ?? crypto.randomUUID(), type: bloksWidget.type ?? 'im_a2ui', data: Buffer.isBuffer(bloksWidget.data) ? bloksWidget.data : Buffer.from(typeof bloksWidget.data === 'string' ? bloksWidget.data : JSON.stringify(bloksWidget.data ?? {}), 'utf8'), ...(bloksWidget.fallback || fallback ? { fallback: String(bloksWidget.fallback ?? fallback) } : {}) }, ...(nativeFlowMessage ? { nativeFlowMessage: { ...nativeFlowMessage, messageVersion: nativeFlowMessage.messageVersion ?? 1 } } : {}) } }
455
547
  }
456
- const msg = await await generateWAMessageFromContent(jid, content, { userJid: this.client.user?.id, waUploadToServer: this.client.waUploadToServer })
548
+ const msg = await generateWAMessageFromContent(jid, content, { userJid: this.client.user?.id, waUploadToServer: upload })
457
549
  const bizNode = getBizBinaryNode(msg.message)
458
550
  await this.client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: bizNode ? [bizNode] : [] })
459
551
  return msg
@@ -208,6 +208,10 @@ export const INITIAL_PREKEY_COUNT = 95;
208
208
  export const UPLOAD_TIMEOUT = 30000;
209
209
  export const MIN_UPLOAD_INTERVAL = 5000;
210
210
  export const HISTORY_SYNC_PAUSED_TIMEOUT_MS = 120000;
211
+ export const DEFAULT_PREKEY_RETENTION = 150
212
+ export const DEFAULT_CLEANUP_THRESHOLD = 50
213
+ export const CLEANUP_INTERVAL_MS = 10 * 60 * 1000
214
+ export const DEFAULT_COLLECTION_NAME = 'zervonex'
211
215
  export const DEFAULT_CACHE_TTLS = {
212
216
  SIGNAL_STORE: 5 * 60,
213
217
  MSG_RETRY: 60 * 60,
@@ -1,55 +1,130 @@
1
1
  import { createReportingInfoStore } from '../Store/index.js'
2
- import { SPAM_FLOWS, resolveReportJid, normalizeReportingTag, buildSpamReportIq, buildSimpleReportNode, parseSpamReportResponse } from '../Utils/index.js'
2
+ import { SPAM_FLOWS, REPORT_REASON_CODES, resolveReportContext, buildSpamReportIq, buildSimpleReportNode, parseSpamReportResponse } from '../Utils/index.js'
3
3
  import { isJidGroup, isPnUser, isLidUser, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js'
4
4
 
5
5
  export const makeSpamReportSocket = (sock, config) => {
6
- const { query, signalRepository, updateBlockStatus, logger } = sock
6
+ const { query, generateMessageTag, signalRepository, logger } = sock
7
+ const store = config.reportingInfoStore ?? createReportingInfoStore()
7
8
 
8
- if (!config.reportingInfoStore) config.reportingInfoStore = createReportingInfoStore()
9
- const store = config.reportingInfoStore
9
+ if (!signalRepository?.lidMapping) logger?.warn?.({}, 'makeSpamReportSocket: signalRepository.lidMapping unavailable — LID resolution disabled')
10
10
 
11
11
  const simpleReportQuery = (jid, messageKeys) => query({
12
12
  tag: 'iq',
13
13
  attrs: { type: 'set', xmlns: 'spam', to: S_WHATSAPP_NET },
14
- content: [buildSimpleReportNode(jid, messageKeys)]
14
+ content: [buildSimpleReportNode(jid, messageKeys)],
15
15
  })
16
16
 
17
+ const wReportQuery = async (node, label, meta, waitForAckMs = 5000) => {
18
+ try {
19
+ await query(node, waitForAckMs)
20
+ logger?.info?.(meta, `${label} (ack received)`)
21
+ return true
22
+ } catch (err) {
23
+ const code = err?.output?.statusCode || err?.statusCode
24
+ if (code === 408 || err?.message?.toLowerCase?.().includes('timed out')) {
25
+ logger?.warn?.(meta, `${label} (no ack; request likely sent)`)
26
+ return false
27
+ }
28
+ throw err
29
+ }
30
+ }
31
+
32
+ const resolveToPN = async jid => {
33
+ if (!isLidUser(jid)) return jidNormalizedUser(jid)
34
+ const pn = await signalRepository?.lidMapping?.getPNForLID?.(jid)
35
+ if (!pn) {
36
+ logger?.error?.({ jid }, 'resolveToPN: could not resolve LID to PN')
37
+ throw new Error(`resolveToPN: no PN found for LID ${jid}`)
38
+ }
39
+ logger?.debug?.({ lid: jid, pn }, 'resolveToPN: resolved LID → PN')
40
+ return jidNormalizedUser(pn)
41
+ }
42
+
17
43
  const reportSpam = async (jid, options = {}) => {
18
- const { spamFlow = SPAM_FLOWS.ACCOUNT_INFO_REPORT, messages: manualMessages, maxMessages = 5 } = options
19
- const reportJid = await resolveReportJid(jid, signalRepository?.lidMapping)
44
+ const { spamFlow: manualFlow, messages: manualMessages, maxMessages = 5 } = options
45
+ const { reportJid, spamFlow: autoFlow, isGroup } = await resolveReportContext(jid, signalRepository?.lidMapping)
46
+ const spamFlow = manualFlow ?? autoFlow
20
47
  let messages = manualMessages
21
48
  if (!messages?.length) {
22
49
  messages = store.getForJid(reportJid, maxMessages)
23
- if (!messages.length) messages = store.getForJid(jid, maxMessages)
50
+ if (!messages.length && !isGroup) {
51
+ messages = store.getForJid(jid, maxMessages)
52
+ if (messages.length) logger?.debug?.({ jid, reportJid }, 'reportSpam: fell back to raw JID for store lookup')
53
+ }
54
+ }
55
+ if (!messages?.length) {
56
+ logger?.error?.({ jid, reportJid }, 'reportSpam: no reporting_tag stored — receive at least one message first')
57
+ throw new Error(`reportSpam: no reporting_tag stored for ${jid}`)
24
58
  }
25
- if (!messages?.length) throw new Error(`reportSpam: no reporting_tag stored for ${jid}. Receive at least one message first.`)
26
59
  const normalized = messages.slice(0, maxMessages).map(m => ({
27
60
  stanzaId: m.stanzaId || m.id,
28
- timestamp: Number(m.timestamp || m.sendTimestamp || m.t),
29
- reportingTag: normalizeReportingTag(m.reportingTag || m.tag),
30
- text: m.text ?? m.raw ?? '',
61
+ timestamp: Number(m.timestamp || m.sendTimestamp || m.t) || Math.floor(Date.now() / 1000),
62
+ reportingTag: m.reportingTag || m.tag,
63
+ text: m.text != null ? String(m.text) : m.raw != null ? String(m.raw) : '',
31
64
  pushName: m.pushName || m.reportedPushName || '',
32
65
  messageType: m.messageType || m.type || 'text',
33
- fromJid: m.fromJid || m.from || reportJid
66
+ fromJid: m.fromJid || m.from || reportJid,
34
67
  }))
35
- logger?.debug?.({ jid: reportJid, spamFlow, count: normalized.length }, 'sending spam report IQ')
36
- const result = await query(buildSpamReportIq(reportJid, normalized, spamFlow))
68
+ logger?.debug?.({ jid: reportJid, spamFlow, count: normalized.length }, 'reportSpam: raw sending IQ')
69
+ const result = await query(buildSpamReportIq(reportJid, normalized, spamFlow, logger))
37
70
  return { ...parseSpamReportResponse(result), messageCount: normalized.length }
38
71
  }
39
72
 
40
73
  const reportContact = async (jid, messageKeys = []) => {
41
- const normalized = jidNormalizedUser(jid)
42
- if (!isPnUser(normalized) && !isLidUser(normalized)) throw new Error('reportContact requires a valid contact JID')
43
- const result = await simpleReportQuery(normalized, messageKeys)
44
- await updateBlockStatus(normalized, 'block')
45
- return parseSpamReportResponse(result)
74
+ if (!isPnUser(jid) && !isLidUser(jid)) {
75
+ logger?.error?.({ jid }, 'reportContact: invalid contact JID')
76
+ throw new Error('reportContact requires a valid contact JID')
77
+ }
78
+ const pnJid = await resolveToPN(jid)
79
+ return parseSpamReportResponse(await simpleReportQuery(pnJid, messageKeys))
46
80
  }
47
81
 
48
82
  const reportGroup = async (jid, messageKeys = []) => {
49
- if (!isJidGroup(jid)) throw new Error('reportGroup requires a valid group JID')
50
- const result = await simpleReportQuery(jid, messageKeys)
51
- await query({ tag: 'iq', attrs: { type: 'set', xmlns: 'w:g2', to: '@g.us' }, content: [{ tag: 'leave', attrs: {}, content: [{ tag: 'group', attrs: { id: jid } }] }] })
52
- return parseSpamReportResponse(result)
83
+ if (!isJidGroup(jid)) {
84
+ logger?.error?.({ jid }, 'reportGroup: invalid group JID')
85
+ throw new Error('reportGroup requires a valid group JID')
86
+ }
87
+ const parsed = parseSpamReportResponse(await simpleReportQuery(jid, messageKeys))
88
+ if (parsed.success) {
89
+ try {
90
+ await query({ tag: 'iq', attrs: { type: 'set', xmlns: 'w:g2', to: '@g.us' }, content: [{ tag: 'leave', attrs: {}, content: [{ tag: 'group', attrs: { id: jid } }] }] })
91
+ logger?.info?.({ jid }, 'reportGroup: left group after successful report')
92
+ } catch (err) {
93
+ logger?.warn?.({ jid, err: err?.message }, 'reportGroup: leave failed after successful report')
94
+ }
95
+ }
96
+ return parsed
97
+ }
98
+
99
+ const reportUser = async (jid, reason = 'spam', waitForAckMs = 5000) => {
100
+ const pnJid = await resolveToPN(jid)
101
+ return wReportQuery(
102
+ { tag: 'iq', attrs: { id: generateMessageTag(), xmlns: 'w:report', to: 's.whatsapp.net', type: 'set' }, content: [{ tag: 'user', attrs: { jid: pnJid, reason: String(REPORT_REASON_CODES[reason] ?? 1) } }] },
103
+ 'reported user', { jid: pnJid, reason }, waitForAckMs,
104
+ )
105
+ }
106
+
107
+ const reportMessage = async (jid, messageId, reason = 'spam', participantJid, waitForAckMs = 5000) => {
108
+ const pnJid = await resolveToPN(jid)
109
+ const participant = participantJid ? await resolveToPN(participantJid) : undefined
110
+ const attrs = { id: String(messageId), reason: String(REPORT_REASON_CODES[reason] ?? 1), ...(participant ? { participant } : {}) }
111
+ return wReportQuery(
112
+ { tag: 'iq', attrs: { id: generateMessageTag(), xmlns: 'w:report', to: 's.whatsapp.net', type: 'set' }, content: [{ tag: 'message', attrs }] },
113
+ 'reported message', { jid: pnJid, messageId, reason, participant }, waitForAckMs,
114
+ )
115
+ }
116
+
117
+ const reportAndBlockUser = async (jid, reason = 'spam', options = {}) => {
118
+ const { waitForAckMs = 5000, maxAttempts = 4, delayMs = 1500 } = options
119
+ const pnJid = await resolveToPN(jid)
120
+ let attempts = 0
121
+ while (attempts < maxAttempts) {
122
+ await reportUser(pnJid, reason, waitForAckMs)
123
+ attempts++
124
+ logger?.debug?.({ jid: pnJid, attempts }, 'reportAndBlockUser: attempt sent')
125
+ await new Promise(r => setTimeout(r, delayMs))
126
+ }
127
+ return { attempts }
53
128
  }
54
129
 
55
130
  return {
@@ -57,9 +132,13 @@ export const makeSpamReportSocket = (sock, config) => {
57
132
  reportSpam,
58
133
  reportContact,
59
134
  reportGroup,
135
+ reportUser,
136
+ reportMessage,
137
+ reportAndBlockUser,
60
138
  getStoredReportingInfo: (jid, max = 5) => store.getForJid(jid, max),
61
139
  clearStoredReportingInfo: jid => store.clear(jid),
62
140
  reportingInfoStore: store,
63
- SPAM_FLOWS
141
+ SPAM_FLOWS,
142
+ REPORT_REASON_CODES,
64
143
  }
65
144
  }