@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.
- package/LICENSE +20 -18
- package/README.md +172 -13
- package/WAProto/WAProtoCompile.js +20 -7
- package/lib/Builders/AIRich.js +76 -45
- package/lib/Builders/base.js +14 -3
- package/lib/Builders/builders.js +140 -48
- package/lib/Defaults/index.js +4 -0
- package/lib/Socket/spam-report.js +104 -25
- package/lib/Socket/zervonex-handler.js +9 -14
- package/lib/Store/make-cache-manager-store.js +2 -3
- package/lib/Store/make-in-memory-store.js +1 -0
- package/lib/Utils/auth-utils.js +19 -2
- package/lib/Utils/bot-capabilities.js +1 -0
- package/lib/Utils/crypto.js +2 -1
- package/lib/Utils/extras.js +1 -1
- package/lib/Utils/generics.js +1 -1
- package/lib/Utils/index.js +1 -0
- package/lib/Utils/message-composer.js +8 -5
- package/lib/Utils/messages.js +9 -3
- package/lib/Utils/native-flow.js +26 -0
- package/lib/Utils/spam-report-utils.js +100 -69
- package/lib/Utils/use-keyv-auth-state.js +169 -0
- package/lib/Utils/use-multi-file-auth-state.js +3 -194
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/lib/Builders/base.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/lib/Builders/builders.js
CHANGED
|
@@ -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:
|
|
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 =>
|
|
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 =>
|
|
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
|
|
243
|
-
|
|
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
|
|
266
|
-
|
|
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.
|
|
283
|
-
setEndTime(t) { this.data.
|
|
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
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
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
|
package/lib/Defaults/index.js
CHANGED
|
@@ -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,
|
|
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,
|
|
6
|
+
const { query, generateMessageTag, signalRepository, logger } = sock
|
|
7
|
+
const store = config.reportingInfoStore ?? createReportingInfoStore()
|
|
7
8
|
|
|
8
|
-
if (!
|
|
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
|
|
19
|
-
const reportJid = await
|
|
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
|
|
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:
|
|
30
|
-
text: m.text
|
|
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 }, '
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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))
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
}
|