@zervonex/baileys 0.0.0-stage → 3.0.1

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