@vanzxy/baileys 1.6.2 β 1.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @vanzxy/baileys might be problematic. Click here for more details.
- package/NOTICE.md +50 -0
- package/lib/Utils/A2UI.js +217 -0
- package/lib/Utils/MessageBuilder.js +332 -46
- package/lib/Utils/MessageBuilder_d.ts +45 -0
- package/lib/Utils/PersistentStore.js +592 -0
- package/lib/Utils/PersistentStore_d.ts +60 -0
- package/lib/Utils/anti-delete.d.ts +68 -0
- package/lib/Utils/anti-delete.js +185 -0
- package/lib/Utils/auto-reply.d.ts +47 -0
- package/lib/Utils/auto-reply.js +155 -0
- package/lib/Utils/button-helper-utils.js +314 -0
- package/lib/Utils/button-sender.js +817 -0
- package/lib/Utils/chat-history-helpers.d.ts +21 -0
- package/lib/Utils/chat-history-helpers.js +71 -0
- package/lib/Utils/index.d.ts +11 -0
- package/lib/Utils/index.js +16 -0
- package/lib/Utils/media-messages.d.ts +18 -0
- package/lib/Utils/media-messages.js +71 -0
- package/lib/Utils/media-set.d.ts +13 -0
- package/lib/Utils/media-set.js +165 -0
- package/lib/Utils/message-kind.js +139 -0
- package/lib/Utils/message-search.d.ts +44 -0
- package/lib/Utils/message-search.js +174 -0
- package/lib/Utils/scheduling.d.ts +42 -0
- package/lib/Utils/scheduling.js +140 -0
- package/lib/Utils/status.d.ts +50 -0
- package/lib/Utils/status.js +108 -0
- package/lib/Utils/stickerpack.d.ts +51 -0
- package/lib/Utils/stickerpack.js +276 -0
- package/lib/Utils/templates.d.ts +76 -0
- package/lib/Utils/templates.js +151 -0
- package/lib/Utils/use-sqlite-auth-state.js +28 -1
- package/lib/Utils/vcard.d.ts +58 -0
- package/lib/Utils/vcard.js +94 -0
- package/lib/VoIP/audio-feeder.d.ts +15 -0
- package/lib/VoIP/audio-feeder.js +132 -0
- package/lib/VoIP/index.js +277 -0
- package/lib/VoIP/relay-transport.d.ts +43 -0
- package/lib/VoIP/relay-transport.js +559 -0
- package/lib/VoIP/signaling.js +624 -0
- package/lib/VoIP/types.d.ts +69 -0
- package/lib/VoIP/types.js +17 -0
- package/lib/VoIP/wasm-engine.d.ts +103 -0
- package/lib/VoIP/wasm-engine.js +1214 -0
- package/lib/VoIP/worker-bootstrap.js +1042 -0
- package/lib/WABinary/generic-utils.js +8 -0
- package/lib/assets/wasm/loader.js +5 -0
- package/lib/assets/wasm/whatsapp.wasm +0 -0
- package/lib/assets/wasm/worker-modules.js +273 -0
- package/lib/index.js +4 -0
- package/package.json +22 -1
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
// Evernight AI β lib/Utils/button-sender.js / Creator : Vanzxyπ
|
|
2
|
+
// Ported from @queenanya/baileys (Bail-master fork) src/addons/button-sender.ts
|
|
3
|
+
// Original credit inside that file: "Ported from @ryuu-reinzz/button-helper v2.2.5"
|
|
4
|
+
// Converted TS -> JS ESM for @vanzxy/baileys.
|
|
5
|
+
//
|
|
6
|
+
// Provides runtime helpers to send WhatsApp interactive / native-flow button
|
|
7
|
+
// messages via relayMessage directly, bypassing sendMessage's validation path
|
|
8
|
+
// that does not recognise interactiveMessage payloads.
|
|
9
|
+
//
|
|
10
|
+
// Visibility guarantee β all button types (quick_reply, cta_url, cta_copy,
|
|
11
|
+
// cta_call, list, template, carousel/cards, combined, single_select) render
|
|
12
|
+
// correctly on WhatsApp Messenger & WhatsApp Business, Android & iOS.
|
|
13
|
+
//
|
|
14
|
+
// What this file provides:
|
|
15
|
+
// 1. buildInteractiveButtons() β legacy β native_flow normaliser
|
|
16
|
+
// 2. validateAuthoringButtons() β permissive pre-send validation
|
|
17
|
+
// 3. validateSendButtonsPayload() β strict sendButtons payload check
|
|
18
|
+
// 4. validateSendInteractiveMessagePayload() β strict interactive payload check
|
|
19
|
+
// 5. validateInteractiveMessageContent() β post-convert check
|
|
20
|
+
// 6. convertToInteractiveMessage() β high-level authoring β proto shape
|
|
21
|
+
// 7. sendInteractiveMessage() β low-level power function
|
|
22
|
+
// 8. sendInteractiveMessageV2() β extended (thumbnail / externalAdReply)
|
|
23
|
+
// 9. sendButtons() β convenience quick-reply wrapper
|
|
24
|
+
// 10. InteractiveValidationError β structured error class
|
|
25
|
+
//
|
|
26
|
+
// Usage:
|
|
27
|
+
// import { sendButtons, sendInteractiveMessage } from '@vanzxy/baileys'
|
|
28
|
+
//
|
|
29
|
+
// await sendButtons(sock, jid, {
|
|
30
|
+
// text: 'Choose an option',
|
|
31
|
+
// footer: 'Bot footer',
|
|
32
|
+
// buttons: [
|
|
33
|
+
// { id: 'yes', text: 'Yes' },
|
|
34
|
+
// { id: 'no', text: 'No' },
|
|
35
|
+
// ],
|
|
36
|
+
// })
|
|
37
|
+
|
|
38
|
+
import { generateMessageIDV2 } from './generics.js';
|
|
39
|
+
import { generateWAMessageFromContent, normalizeMessageContent } from './messages.js';
|
|
40
|
+
import { isJidGroup } from '../WABinary/index.js';
|
|
41
|
+
import { getButtonArgs, getButtonType } from './button-helper-utils.js';
|
|
42
|
+
|
|
43
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
44
|
+
// Custom Error
|
|
45
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Thrown when button payload authoring validation fails.
|
|
49
|
+
* Provides structured errors/warnings + a canonical example so callers can
|
|
50
|
+
* surface actionable feedback to end-users or logs.
|
|
51
|
+
*/
|
|
52
|
+
export class InteractiveValidationError extends Error {
|
|
53
|
+
constructor(message, { context, errors = [], warnings = [], example } = {}) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = 'InteractiveValidationError';
|
|
56
|
+
this.context = context;
|
|
57
|
+
this.errors = errors;
|
|
58
|
+
this.warnings = warnings;
|
|
59
|
+
this.example = example;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
toJSON() {
|
|
63
|
+
return {
|
|
64
|
+
name: this.name,
|
|
65
|
+
message: this.message,
|
|
66
|
+
context: this.context,
|
|
67
|
+
errors: this.errors,
|
|
68
|
+
warnings: this.warnings,
|
|
69
|
+
example: this.example
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
formatDetailed() {
|
|
74
|
+
const lines = [`[${this.name}] ${this.message}${this.context ? ' (' + this.context + ')' : ''}`];
|
|
75
|
+
if (this.errors.length) {
|
|
76
|
+
lines.push('Errors:');
|
|
77
|
+
this.errors.forEach((e) => lines.push(' - ' + e));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (this.warnings.length) {
|
|
81
|
+
lines.push('Warnings:');
|
|
82
|
+
this.warnings.forEach((w) => lines.push(' - ' + w));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (this.example) {
|
|
86
|
+
lines.push('Example payload:', JSON.stringify(this.example, null, 2));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
94
|
+
// Example payloads (embedded inside thrown errors for developer guidance)
|
|
95
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
96
|
+
|
|
97
|
+
const EXAMPLE_PAYLOADS = {
|
|
98
|
+
sendButtons: {
|
|
99
|
+
text: 'Choose an option',
|
|
100
|
+
buttons: [
|
|
101
|
+
{ id: 'opt1', text: 'Option 1' },
|
|
102
|
+
{ id: 'opt2', text: 'Option 2' },
|
|
103
|
+
{
|
|
104
|
+
name: 'cta_url',
|
|
105
|
+
buttonParamsJson: JSON.stringify({
|
|
106
|
+
display_text: 'Visit Site',
|
|
107
|
+
url: 'https://example.com'
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
footer: 'Footer text'
|
|
112
|
+
},
|
|
113
|
+
sendInteractiveMessage: {
|
|
114
|
+
text: 'Pick an action',
|
|
115
|
+
interactiveButtons: [
|
|
116
|
+
{
|
|
117
|
+
name: 'quick_reply',
|
|
118
|
+
buttonParamsJson: JSON.stringify({ display_text: 'Hello', id: 'hello' })
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'cta_copy',
|
|
122
|
+
buttonParamsJson: JSON.stringify({ display_text: 'Copy Code', copy_code: 'ABC123' })
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
footer: 'Footer'
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
130
|
+
// Allowed button name sets
|
|
131
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
132
|
+
|
|
133
|
+
/** Allowed complex button names in sendButtons */
|
|
134
|
+
const SEND_BUTTONS_ALLOWED_COMPLEX = new Set(['cta_url', 'cta_copy', 'cta_call']);
|
|
135
|
+
|
|
136
|
+
/** Full set of button names allowed in sendInteractiveMessage */
|
|
137
|
+
const INTERACTIVE_ALLOWED_NAMES = new Set([
|
|
138
|
+
'quick_reply',
|
|
139
|
+
'cta_url',
|
|
140
|
+
'cta_copy',
|
|
141
|
+
'cta_call',
|
|
142
|
+
'cta_catalog',
|
|
143
|
+
'cta_reminder',
|
|
144
|
+
'cta_cancel_reminder',
|
|
145
|
+
'address_message',
|
|
146
|
+
'send_location',
|
|
147
|
+
'open_webview',
|
|
148
|
+
'mpm',
|
|
149
|
+
'wa_payment_transaction_details',
|
|
150
|
+
'automated_greeting_message_view_catalog',
|
|
151
|
+
'galaxy_message',
|
|
152
|
+
'single_select'
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
/** Minimum required fields per button name */
|
|
156
|
+
const REQUIRED_FIELDS_MAP = {
|
|
157
|
+
cta_url: ['display_text', 'url'],
|
|
158
|
+
cta_copy: ['display_text', 'copy_code'],
|
|
159
|
+
cta_call: ['display_text', 'phone_number'],
|
|
160
|
+
cta_catalog: ['business_phone_number'],
|
|
161
|
+
cta_reminder: ['display_text'],
|
|
162
|
+
cta_cancel_reminder: ['display_text'],
|
|
163
|
+
address_message: ['display_text'],
|
|
164
|
+
send_location: ['display_text'],
|
|
165
|
+
open_webview: ['title', 'link'],
|
|
166
|
+
mpm: ['product_id'],
|
|
167
|
+
wa_payment_transaction_details: ['transaction_id'],
|
|
168
|
+
automated_greeting_message_view_catalog: ['business_phone_number', 'catalog_product_id'],
|
|
169
|
+
galaxy_message: ['flow_token', 'flow_id'],
|
|
170
|
+
single_select: ['title', 'sections'],
|
|
171
|
+
quick_reply: ['display_text', 'id']
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
175
|
+
// Internal helper
|
|
176
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
177
|
+
|
|
178
|
+
function parseButtonParamsInternal(name, buttonParamsJson, errors, _warnings, index) {
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(buttonParamsJson);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
errors.push(`button[${index}] (${name}) invalid JSON: ${e.message}`);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const required = REQUIRED_FIELDS_MAP[name] ?? [];
|
|
188
|
+
for (const field of required) {
|
|
189
|
+
if (!(field in parsed)) {
|
|
190
|
+
errors.push(`button[${index}] (${name}) missing required field '${field}'`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (name === 'open_webview' && parsed.link) {
|
|
195
|
+
const link = parsed.link;
|
|
196
|
+
if (typeof link !== 'object' || !link.url) {
|
|
197
|
+
errors.push(`button[${index}] (open_webview) link.url required`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (name === 'single_select') {
|
|
202
|
+
if (!Array.isArray(parsed.sections) || parsed.sections.length === 0) {
|
|
203
|
+
errors.push(`button[${index}] (single_select) sections must be a non-empty array`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return parsed;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
211
|
+
// Public: normalise + validate
|
|
212
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Normalise various legacy / upstream button shapes into the native-flow format
|
|
216
|
+
* { name, buttonParamsJson }.
|
|
217
|
+
*
|
|
218
|
+
* Accepted input shapes:
|
|
219
|
+
* 1. Already native_flow : { name: string, buttonParamsJson: string }
|
|
220
|
+
* 2. Simple legacy : { id?: string, text?: string, displayText?: string }
|
|
221
|
+
* 3. Old Baileys : { buttonId: string, buttonText: { displayText: string } }
|
|
222
|
+
* 4. Unknown : passed through verbatim
|
|
223
|
+
*/
|
|
224
|
+
export function buildInteractiveButtons(buttons = []) {
|
|
225
|
+
return buttons.map((btn, i) => {
|
|
226
|
+
// 1. Already native shape
|
|
227
|
+
if (btn.name && btn.buttonParamsJson) return btn;
|
|
228
|
+
|
|
229
|
+
// 2. Legacy quick-reply
|
|
230
|
+
if (btn.id || btn.text || btn.displayText) {
|
|
231
|
+
return {
|
|
232
|
+
name: 'quick_reply',
|
|
233
|
+
buttonParamsJson: JSON.stringify({
|
|
234
|
+
display_text: btn.text ?? btn.displayText ?? `Button ${i + 1}`,
|
|
235
|
+
id: btn.id ?? `quick_${i + 1}`
|
|
236
|
+
})
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 3. Old Baileys shape
|
|
241
|
+
if (btn.buttonId && btn.buttonText?.displayText) {
|
|
242
|
+
return {
|
|
243
|
+
name: 'quick_reply',
|
|
244
|
+
buttonParamsJson: JSON.stringify({
|
|
245
|
+
display_text: btn.buttonText.displayText,
|
|
246
|
+
id: btn.buttonId
|
|
247
|
+
})
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 4. Unknown β pass through
|
|
252
|
+
return btn;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Validate raw button objects before conversion.
|
|
258
|
+
* Permissive: only blocks clearly malformed input.
|
|
259
|
+
*/
|
|
260
|
+
export function validateAuthoringButtons(buttons) {
|
|
261
|
+
const errors = [];
|
|
262
|
+
const warnings = [];
|
|
263
|
+
|
|
264
|
+
if (buttons === null) return { errors: [], warnings: [], valid: true, cleaned: [] };
|
|
265
|
+
if (!Array.isArray(buttons)) {
|
|
266
|
+
errors.push('buttons must be an array');
|
|
267
|
+
return { errors, warnings, valid: false, cleaned: [] };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const SOFT_CAP = 25;
|
|
271
|
+
if (buttons.length === 0) {
|
|
272
|
+
warnings.push('buttons array is empty');
|
|
273
|
+
} else if (buttons.length > SOFT_CAP) {
|
|
274
|
+
warnings.push(`buttons count (${buttons.length}) exceeds soft cap of ${SOFT_CAP}; may be rejected by client`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const cleaned = buttons.map((btn, idx) => {
|
|
278
|
+
if (btn === null || typeof btn !== 'object') {
|
|
279
|
+
errors.push(`button[${idx}] is not an object`);
|
|
280
|
+
return btn;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (btn.name && btn.buttonParamsJson) {
|
|
284
|
+
if (typeof btn.buttonParamsJson !== 'string') {
|
|
285
|
+
errors.push(`button[${idx}] buttonParamsJson must be string`);
|
|
286
|
+
} else {
|
|
287
|
+
try {
|
|
288
|
+
JSON.parse(btn.buttonParamsJson);
|
|
289
|
+
} catch (e) {
|
|
290
|
+
errors.push(`button[${idx}] buttonParamsJson is not valid JSON: ${e.message}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return btn;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (btn.id || btn.text || btn.displayText) return btn;
|
|
298
|
+
if (btn.buttonId && btn.buttonText?.displayText) return btn;
|
|
299
|
+
warnings.push(`button[${idx}] unrecognized shape; passing through unchanged`);
|
|
300
|
+
return btn;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return { errors, warnings, valid: errors.length === 0, cleaned };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Strict validator for sendButtons() payload.
|
|
308
|
+
*/
|
|
309
|
+
export function validateSendButtonsPayload(data) {
|
|
310
|
+
const errors = [];
|
|
311
|
+
const warnings = [];
|
|
312
|
+
|
|
313
|
+
if (!data || typeof data !== 'object') {
|
|
314
|
+
return { valid: false, errors: ['payload must be an object'], warnings };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!data.text || typeof data.text !== 'string') {
|
|
318
|
+
errors.push('text is mandatory and must be a string');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!Array.isArray(data.buttons) || data.buttons.length === 0) {
|
|
322
|
+
errors.push('buttons is mandatory and must be a non-empty array');
|
|
323
|
+
} else {
|
|
324
|
+
data.buttons.forEach((btn, i) => {
|
|
325
|
+
if (!btn || typeof btn !== 'object') {
|
|
326
|
+
errors.push(`button[${i}] must be an object`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (btn.id && btn.text) {
|
|
331
|
+
if (typeof btn.id !== 'string' || typeof btn.text !== 'string') {
|
|
332
|
+
errors.push(`button[${i}] legacy quick reply id/text must be strings`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (btn.name && btn.buttonParamsJson) {
|
|
339
|
+
if (!SEND_BUTTONS_ALLOWED_COMPLEX.has(btn.name)) {
|
|
340
|
+
errors.push(
|
|
341
|
+
`button[${i}] name '${btn.name}' not allowed in sendButtons (allowed: ${[...SEND_BUTTONS_ALLOWED_COMPLEX].join(', ')})`
|
|
342
|
+
);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (typeof btn.buttonParamsJson !== 'string') {
|
|
347
|
+
errors.push(`button[${i}] buttonParamsJson must be string`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
parseButtonParamsInternal(btn.name, btn.buttonParamsJson, errors, warnings, i);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
errors.push(
|
|
356
|
+
`button[${i}] invalid shape β expected {id, text} or {name, buttonParamsJson} with name in [${[...SEND_BUTTONS_ALLOWED_COMPLEX].join(', ')}]`
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Strict validator for sendInteractiveMessage() authoring payload.
|
|
366
|
+
*/
|
|
367
|
+
export function validateSendInteractiveMessagePayload(data) {
|
|
368
|
+
const errors = [];
|
|
369
|
+
const warnings = [];
|
|
370
|
+
|
|
371
|
+
if (!data || typeof data !== 'object') {
|
|
372
|
+
return { valid: false, errors: ['payload must be an object'], warnings };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!data.text || typeof data.text !== 'string') {
|
|
376
|
+
errors.push('text is mandatory and must be a string');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!Array.isArray(data.interactiveButtons) || data.interactiveButtons.length === 0) {
|
|
380
|
+
errors.push('interactiveButtons is mandatory and must be a non-empty array');
|
|
381
|
+
} else {
|
|
382
|
+
data.interactiveButtons.forEach((btn, i) => {
|
|
383
|
+
if (!btn || typeof btn !== 'object') {
|
|
384
|
+
errors.push(`interactiveButtons[${i}] must be an object`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!btn.name || typeof btn.name !== 'string') {
|
|
389
|
+
errors.push(`interactiveButtons[${i}] missing name`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!INTERACTIVE_ALLOWED_NAMES.has(btn.name)) {
|
|
394
|
+
errors.push(`interactiveButtons[${i}] name '${btn.name}' not allowed`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!btn.buttonParamsJson || typeof btn.buttonParamsJson !== 'string') {
|
|
399
|
+
errors.push(`interactiveButtons[${i}] buttonParamsJson must be a non-empty string`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
parseButtonParamsInternal(btn.name, btn.buttonParamsJson, errors, warnings, i);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Validate already-converted interactiveMessage content (just before WAMessage creation).
|
|
412
|
+
*/
|
|
413
|
+
export function validateInteractiveMessageContent(content) {
|
|
414
|
+
const errors = [];
|
|
415
|
+
const warnings = [];
|
|
416
|
+
|
|
417
|
+
if (!content || typeof content !== 'object') {
|
|
418
|
+
return { errors: ['content must be an object'], warnings, valid: false };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const interactive = content.interactiveMessage;
|
|
422
|
+
|
|
423
|
+
// Non-interactive messages are fine β nothing to validate
|
|
424
|
+
if (!interactive) return { errors, warnings, valid: true };
|
|
425
|
+
|
|
426
|
+
const nativeFlow = interactive.nativeFlowMessage;
|
|
427
|
+
if (!nativeFlow) {
|
|
428
|
+
errors.push('interactiveMessage.nativeFlowMessage missing');
|
|
429
|
+
return { errors, warnings, valid: false };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!Array.isArray(nativeFlow.buttons)) {
|
|
433
|
+
errors.push('nativeFlowMessage.buttons must be an array');
|
|
434
|
+
return { errors, warnings, valid: false };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (nativeFlow.buttons.length === 0) {
|
|
438
|
+
warnings.push('nativeFlowMessage.buttons is empty');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
nativeFlow.buttons.forEach((btn, i) => {
|
|
442
|
+
if (!btn || typeof btn !== 'object') {
|
|
443
|
+
errors.push(`buttons[${i}] is not an object`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!btn.buttonParamsJson) {
|
|
448
|
+
warnings.push(`buttons[${i}] missing buttonParamsJson (may fail to render)`);
|
|
449
|
+
} else if (typeof btn.buttonParamsJson !== 'string') {
|
|
450
|
+
errors.push(`buttons[${i}] buttonParamsJson must be string`);
|
|
451
|
+
} else {
|
|
452
|
+
try {
|
|
453
|
+
JSON.parse(btn.buttonParamsJson);
|
|
454
|
+
} catch (e) {
|
|
455
|
+
warnings.push(`buttons[${i}] buttonParamsJson invalid JSON (${e.message})`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!btn.name) {
|
|
460
|
+
warnings.push(`buttons[${i}] missing name; defaulting to quick_reply`);
|
|
461
|
+
btn.name = 'quick_reply';
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
return { errors, warnings, valid: errors.length === 0 };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
469
|
+
// Content converter
|
|
470
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Convert the high-level authoring shape:
|
|
474
|
+
* { text, footer?, title?, subtitle?, interactiveButtons: [...] }
|
|
475
|
+
* into the exact structure Baileys / WAProto expects:
|
|
476
|
+
* { interactiveMessage: { nativeFlowMessage: { buttons: [...] }, body?, header?, footer? } }
|
|
477
|
+
*
|
|
478
|
+
* Authoring-only fields are stripped so they don't leak into
|
|
479
|
+
* generateWAMessageFromContent.
|
|
480
|
+
*/
|
|
481
|
+
export function convertToInteractiveMessage(content) {
|
|
482
|
+
const btns = content.interactiveButtons;
|
|
483
|
+
if (btns && btns.length > 0) {
|
|
484
|
+
const interactiveMessage = {
|
|
485
|
+
nativeFlowMessage: {
|
|
486
|
+
buttons: btns.map((btn) => ({
|
|
487
|
+
name: btn.name ?? 'quick_reply',
|
|
488
|
+
buttonParamsJson: btn.buttonParamsJson
|
|
489
|
+
})),
|
|
490
|
+
messageParamsJson: ''
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
if (content.title || content.subtitle) {
|
|
495
|
+
interactiveMessage.header = { title: content.title ?? content.subtitle ?? '' };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (content.text) {
|
|
499
|
+
interactiveMessage.body = { text: content.text };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (content.footer) {
|
|
503
|
+
interactiveMessage.footer = { text: content.footer };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Strip authoring-only keys to avoid leaking into proto serialisation
|
|
507
|
+
const newContent = { ...content };
|
|
508
|
+
delete newContent.interactiveButtons;
|
|
509
|
+
delete newContent.title;
|
|
510
|
+
delete newContent.subtitle;
|
|
511
|
+
delete newContent.text;
|
|
512
|
+
delete newContent.footer;
|
|
513
|
+
|
|
514
|
+
return { ...newContent, interactiveMessage };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return content;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
521
|
+
// Core send helpers
|
|
522
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Low-level power function β sends any interactive message by:
|
|
526
|
+
*
|
|
527
|
+
* 1. Validate authoring payload (when interactiveButtons is provided).
|
|
528
|
+
* 2. Convert to interactiveMessage / nativeFlowMessage proto shape.
|
|
529
|
+
* 3. Build WAMessage via generateWAMessageFromContent (skips sendMessage
|
|
530
|
+
* validation that rejects interactiveMessage).
|
|
531
|
+
* 4. Derive & inject required binary nodes so buttons render on all platforms:
|
|
532
|
+
* - biz node { tag:'biz', attrs:{} }
|
|
533
|
+
* - interactive child { type:'native_flow', v:'1' }
|
|
534
|
+
* - native_flow child { v:'9', name:'mixed' }
|
|
535
|
+
* - bot node { tag:'bot', attrs:{ biz_bot:'1' } } (private chats only)
|
|
536
|
+
* 5. Relay via sock.relayMessage.
|
|
537
|
+
*
|
|
538
|
+
* @param {import('../Types/Socket.js').WASocket} sock Active @vanzxy/baileys socket instance.
|
|
539
|
+
* @param {string} jid Destination chat JID.
|
|
540
|
+
* @param {Record<string, unknown>} content Authoring payload (may include interactiveButtons).
|
|
541
|
+
* @param {object} [options] Pass-through relay options (additionalNodes, etc.).
|
|
542
|
+
*/
|
|
543
|
+
export async function sendInteractiveMessage(sock, jid, content, options = {}) {
|
|
544
|
+
if (!sock) {
|
|
545
|
+
throw new InteractiveValidationError('Socket is required', { context: 'sendInteractiveMessage' });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Step 1 β authoring validation
|
|
549
|
+
if (Array.isArray(content.interactiveButtons)) {
|
|
550
|
+
const strict = validateSendInteractiveMessagePayload(content);
|
|
551
|
+
if (!strict.valid) {
|
|
552
|
+
throw new InteractiveValidationError('Interactive authoring payload invalid', {
|
|
553
|
+
context: 'sendInteractiveMessage.validateSendInteractiveMessagePayload',
|
|
554
|
+
errors: strict.errors,
|
|
555
|
+
warnings: strict.warnings,
|
|
556
|
+
example: EXAMPLE_PAYLOADS.sendInteractiveMessage
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (strict.warnings.length) {
|
|
561
|
+
sock.logger?.warn?.(strict.warnings, '[button-sender] sendInteractiveMessage warnings');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Step 2 β convert to proto shape
|
|
566
|
+
const convertedContent = convertToInteractiveMessage(content);
|
|
567
|
+
const { errors: cErr, warnings: cWarn, valid: cValid } = validateInteractiveMessageContent(convertedContent);
|
|
568
|
+
if (!cValid) {
|
|
569
|
+
throw new InteractiveValidationError('Converted interactive content invalid', {
|
|
570
|
+
context: 'sendInteractiveMessage.validateInteractiveMessageContent',
|
|
571
|
+
errors: cErr,
|
|
572
|
+
warnings: cWarn,
|
|
573
|
+
example: convertToInteractiveMessage(EXAMPLE_PAYLOADS.sendInteractiveMessage)
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (cWarn.length) sock.logger?.warn?.(cWarn, '[button-sender] Interactive content warnings');
|
|
578
|
+
|
|
579
|
+
// Step 3 β build WAMessage
|
|
580
|
+
const userJid = sock.authState?.creds?.me?.id ?? sock.user?.id ?? '';
|
|
581
|
+
const fullMsg = generateWAMessageFromContent(jid, convertedContent, {
|
|
582
|
+
userJid,
|
|
583
|
+
messageId: generateMessageIDV2(userJid),
|
|
584
|
+
timestamp: new Date()
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Step 4 β derive binary nodes
|
|
588
|
+
// normalizeMessageContent unwraps view-once/ephemerals so getButtonType
|
|
589
|
+
// inspects the real inner message type correctly.
|
|
590
|
+
const normalizedContent = normalizeMessageContent(fullMsg.message);
|
|
591
|
+
const buttonType = normalizedContent ? getButtonType(normalizedContent) : undefined;
|
|
592
|
+
|
|
593
|
+
const additionalNodes = [...(options.additionalNodes ?? [])];
|
|
594
|
+
|
|
595
|
+
if (buttonType && normalizedContent) {
|
|
596
|
+
// biz > interactive > native_flow(v:9, mixed) β exact sendButton node structure
|
|
597
|
+
// Works on: Android, iOS, WA Messenger, WA Business
|
|
598
|
+
const bizNode = getButtonArgs(normalizedContent);
|
|
599
|
+
additionalNodes.push(bizNode);
|
|
600
|
+
|
|
601
|
+
// Private chats also need the bot node
|
|
602
|
+
if (!isJidGroup(jid)) {
|
|
603
|
+
additionalNodes.push({ tag: 'bot', attrs: { biz_bot: '1' } });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Step 5 β relay
|
|
608
|
+
await sock.relayMessage(jid, fullMsg.message, {
|
|
609
|
+
messageId: fullMsg.key.id,
|
|
610
|
+
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
611
|
+
additionalAttributes: options.additionalAttributes ?? {},
|
|
612
|
+
statusJidList: options.statusJidList,
|
|
613
|
+
additionalNodes
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Step 6 (optional) β emit to local event stream
|
|
617
|
+
// Only for private chats to avoid duplicate processing in groups.
|
|
618
|
+
if (sock.config?.emitOwnEvents && !isJidGroup(jid)) {
|
|
619
|
+
process.nextTick(() => {
|
|
620
|
+
if (sock.processingMutex?.mutex && sock.upsertMessage) {
|
|
621
|
+
void sock.processingMutex.mutex(() => sock.upsertMessage(fullMsg, 'append'));
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return fullMsg;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Extended send variant with thumbnail / externalAdReply patching.
|
|
631
|
+
*
|
|
632
|
+
* Dummy document priority:
|
|
633
|
+
* 1. content.filePath β load file from local disk path
|
|
634
|
+
* 2. content.fileUrl β fetch file buffer from a URL
|
|
635
|
+
* 3. content.thumbnailUrl β fetch jpegThumbnail buffer AND use as dummy
|
|
636
|
+
* 4. fallback β plain text dummy buffer
|
|
637
|
+
* mimetype is taken from content.mimetype if provided, else auto-detected.
|
|
638
|
+
*
|
|
639
|
+
* externalAdReply fields:
|
|
640
|
+
* - containsAutoReply : true
|
|
641
|
+
* - mediaUrl / thumbnailUrl
|
|
642
|
+
* - renderLargerThumbnail : true
|
|
643
|
+
* - jpegThumbnail buffer (WA trusts this most)
|
|
644
|
+
*
|
|
645
|
+
* axios is a soft peer dependency β errors are non-fatal (logged only).
|
|
646
|
+
*
|
|
647
|
+
* @example β thumbnail from URL
|
|
648
|
+
* await sendInteractiveMessageV2(sock, jid, {
|
|
649
|
+
* text: 'Hello',
|
|
650
|
+
* thumbnailUrl: 'https://example.com/thumb.jpg',
|
|
651
|
+
* interactiveButtons: [{ name: 'quick_reply', buttonParamsJson: '{"display_text":"Hi","id":"hi"}' }],
|
|
652
|
+
* })
|
|
653
|
+
*
|
|
654
|
+
* @example β dummy file from local path
|
|
655
|
+
* await sendInteractiveMessageV2(sock, jid, {
|
|
656
|
+
* text: 'Hello',
|
|
657
|
+
* filePath: '/tmp/myfile.pdf',
|
|
658
|
+
* mimetype: 'application/pdf',
|
|
659
|
+
* interactiveButtons: [...],
|
|
660
|
+
* })
|
|
661
|
+
*/
|
|
662
|
+
export async function sendInteractiveMessageV2(sock, jid, content, options = {}) {
|
|
663
|
+
if (!sock) {
|
|
664
|
+
throw new InteractiveValidationError('Socket is required', { context: 'sendInteractiveMessageV2' });
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const hasThumb = !!content.thumbnailUrl;
|
|
668
|
+
const hasFilePath = !!content.filePath;
|
|
669
|
+
const hasFileUrl = !!content.fileUrl;
|
|
670
|
+
const shouldForce = options.forceExternalAdReply === true;
|
|
671
|
+
|
|
672
|
+
// ββ Helper: fetch buffer from any URL (axios soft peer dep) βββββββββββββββ
|
|
673
|
+
async function bufferFromUrl(url) {
|
|
674
|
+
try {
|
|
675
|
+
// axios is an optional peer dependency
|
|
676
|
+
const { default: axios } = await import('axios');
|
|
677
|
+
const res = await axios.get(url, { responseType: 'arraybuffer' });
|
|
678
|
+
return Buffer.from(res.data);
|
|
679
|
+
} catch (e) {
|
|
680
|
+
sock.logger?.warn?.({ err: e }, '[button-sender] Failed to fetch buffer from URL');
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ββ Patch 1 β build dummy document so externalAdReply renders thumbnail βββ
|
|
686
|
+
// Only inject when no media already present in the payload.
|
|
687
|
+
if ((hasThumb || hasFilePath || hasFileUrl || shouldForce) && !content.document && !content.image && !content.video) {
|
|
688
|
+
try {
|
|
689
|
+
let fileBuffer;
|
|
690
|
+
let fileName = 'file.pdf';
|
|
691
|
+
let mimeType = 'application/pdf';
|
|
692
|
+
|
|
693
|
+
if (hasFilePath) {
|
|
694
|
+
const { readFileSync } = await import('fs');
|
|
695
|
+
fileBuffer = readFileSync(content.filePath);
|
|
696
|
+
fileName = content.filePath.split('/').pop() ?? fileName;
|
|
697
|
+
mimeType = content.mimetype ?? 'application/octet-stream';
|
|
698
|
+
} else if (hasFileUrl) {
|
|
699
|
+
const buf = await bufferFromUrl(content.fileUrl);
|
|
700
|
+
fileBuffer = buf ?? Buffer.from('dummy', 'utf-8');
|
|
701
|
+
fileName = content.fileUrl.split('/').pop() ?? fileName;
|
|
702
|
+
mimeType = content.mimetype ?? 'application/octet-stream';
|
|
703
|
+
} else {
|
|
704
|
+
fileBuffer = Buffer.from('dummy', 'utf-8');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
content.document = fileBuffer;
|
|
708
|
+
content.fileName = content.fileName ?? fileName;
|
|
709
|
+
content.mimetype = content.mimetype ?? mimeType;
|
|
710
|
+
} catch (e) {
|
|
711
|
+
sock.logger?.warn?.({ err: e }, '[button-sender] Failed to build dummy document');
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ββ Fetch jpegThumbnail buffer from thumbnailUrl βββββββββββββββββ
|
|
716
|
+
// WA trusts jpegThumbnail buffer more than a bare URL string.
|
|
717
|
+
let jpegThumb = null;
|
|
718
|
+
if (hasThumb) jpegThumb = await bufferFromUrl(content.thumbnailUrl);
|
|
719
|
+
|
|
720
|
+
// ββ Patch 2 β build externalAdReply contextInfo β
|
|
721
|
+
if (hasThumb || hasFilePath || hasFileUrl || shouldForce) {
|
|
722
|
+
const thumbUrl = content.thumbnailUrl ?? options.thumbnailUrl ?? '';
|
|
723
|
+
const existingCtx = content.contextInfo ?? {};
|
|
724
|
+
const existingEar = existingCtx.externalAdReply ?? {};
|
|
725
|
+
|
|
726
|
+
content.contextInfo = {
|
|
727
|
+
...existingCtx,
|
|
728
|
+
externalAdReply: {
|
|
729
|
+
...existingEar,
|
|
730
|
+
mediaType: 1,
|
|
731
|
+
containsAutoReply: true,
|
|
732
|
+
title: existingEar.title ?? `Β© ${globalThis.ownername ?? 'Evernight AI'}`,
|
|
733
|
+
body: existingEar.body ?? 'Virtual Assistant',
|
|
734
|
+
sourceUrl: existingEar.sourceUrl ?? 'https://example.com',
|
|
735
|
+
mediaUrl: thumbUrl,
|
|
736
|
+
thumbnailUrl: thumbUrl,
|
|
737
|
+
renderLargerThumbnail: true,
|
|
738
|
+
...(jpegThumb ? { jpegThumbnail: jpegThumb } : {})
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return sendInteractiveMessage(sock, jid, content, options);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
747
|
+
// Public convenience wrapper
|
|
748
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Convenience wrapper for the most common quick-reply / CTA button use case.
|
|
752
|
+
*
|
|
753
|
+
* @example β quick-reply
|
|
754
|
+
* await sendButtons(sock, jid, {
|
|
755
|
+
* text: 'Are you sure?',
|
|
756
|
+
* footer: 'Bot v1',
|
|
757
|
+
* buttons: [
|
|
758
|
+
* { id: 'yes', text: 'Yes' },
|
|
759
|
+
* { id: 'no', text: 'No' },
|
|
760
|
+
* ],
|
|
761
|
+
* })
|
|
762
|
+
*
|
|
763
|
+
* @example β mixed quick-reply + CTA URL
|
|
764
|
+
* await sendButtons(sock, jid, {
|
|
765
|
+
* text: 'Check our site',
|
|
766
|
+
* buttons: [
|
|
767
|
+
* { id: 'info', text: 'More Info' },
|
|
768
|
+
* { name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: 'Open', url: 'https://example.com' }) },
|
|
769
|
+
* ],
|
|
770
|
+
* })
|
|
771
|
+
*/
|
|
772
|
+
export async function sendButtons(sock, jid, data = { text: '', buttons: [] }, options = {}) {
|
|
773
|
+
if (!sock) {
|
|
774
|
+
throw new InteractiveValidationError('Socket is required', { context: 'sendButtons' });
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const { text = '', footer = '', title, subtitle, buttons = [] } = data;
|
|
778
|
+
|
|
779
|
+
// Strict payload validation
|
|
780
|
+
const strict = validateSendButtonsPayload({ text, buttons, title, subtitle, footer });
|
|
781
|
+
if (!strict.valid) {
|
|
782
|
+
throw new InteractiveValidationError('Buttons payload invalid', {
|
|
783
|
+
context: 'sendButtons.validateSendButtonsPayload',
|
|
784
|
+
errors: strict.errors,
|
|
785
|
+
warnings: strict.warnings,
|
|
786
|
+
example: EXAMPLE_PAYLOADS.sendButtons
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (strict.warnings.length) {
|
|
791
|
+
sock.logger?.warn?.(strict.warnings, '[button-sender] sendButtons warnings');
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Validate authoring buttons
|
|
795
|
+
const { errors, warnings, cleaned } = validateAuthoringButtons(buttons);
|
|
796
|
+
if (errors.length) {
|
|
797
|
+
throw new InteractiveValidationError('Authoring button objects invalid', {
|
|
798
|
+
context: 'sendButtons.validateAuthoringButtons',
|
|
799
|
+
errors,
|
|
800
|
+
warnings,
|
|
801
|
+
example: EXAMPLE_PAYLOADS.sendButtons.buttons
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (warnings.length) {
|
|
806
|
+
sock.logger?.warn?.(warnings, '[button-sender] Button validation warnings');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Normalise to native_flow format
|
|
810
|
+
const interactiveButtons = buildInteractiveButtons(cleaned);
|
|
811
|
+
|
|
812
|
+
const payload = { text, footer, interactiveButtons };
|
|
813
|
+
if (title) payload.title = title;
|
|
814
|
+
if (subtitle) payload.subtitle = subtitle;
|
|
815
|
+
|
|
816
|
+
return sendInteractiveMessage(sock, jid, payload, options);
|
|
817
|
+
}
|