@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.

Files changed (51) hide show
  1. package/NOTICE.md +50 -0
  2. package/lib/Utils/A2UI.js +217 -0
  3. package/lib/Utils/MessageBuilder.js +332 -46
  4. package/lib/Utils/MessageBuilder_d.ts +45 -0
  5. package/lib/Utils/PersistentStore.js +592 -0
  6. package/lib/Utils/PersistentStore_d.ts +60 -0
  7. package/lib/Utils/anti-delete.d.ts +68 -0
  8. package/lib/Utils/anti-delete.js +185 -0
  9. package/lib/Utils/auto-reply.d.ts +47 -0
  10. package/lib/Utils/auto-reply.js +155 -0
  11. package/lib/Utils/button-helper-utils.js +314 -0
  12. package/lib/Utils/button-sender.js +817 -0
  13. package/lib/Utils/chat-history-helpers.d.ts +21 -0
  14. package/lib/Utils/chat-history-helpers.js +71 -0
  15. package/lib/Utils/index.d.ts +11 -0
  16. package/lib/Utils/index.js +16 -0
  17. package/lib/Utils/media-messages.d.ts +18 -0
  18. package/lib/Utils/media-messages.js +71 -0
  19. package/lib/Utils/media-set.d.ts +13 -0
  20. package/lib/Utils/media-set.js +165 -0
  21. package/lib/Utils/message-kind.js +139 -0
  22. package/lib/Utils/message-search.d.ts +44 -0
  23. package/lib/Utils/message-search.js +174 -0
  24. package/lib/Utils/scheduling.d.ts +42 -0
  25. package/lib/Utils/scheduling.js +140 -0
  26. package/lib/Utils/status.d.ts +50 -0
  27. package/lib/Utils/status.js +108 -0
  28. package/lib/Utils/stickerpack.d.ts +51 -0
  29. package/lib/Utils/stickerpack.js +276 -0
  30. package/lib/Utils/templates.d.ts +76 -0
  31. package/lib/Utils/templates.js +151 -0
  32. package/lib/Utils/use-sqlite-auth-state.js +28 -1
  33. package/lib/Utils/vcard.d.ts +58 -0
  34. package/lib/Utils/vcard.js +94 -0
  35. package/lib/VoIP/audio-feeder.d.ts +15 -0
  36. package/lib/VoIP/audio-feeder.js +132 -0
  37. package/lib/VoIP/index.js +277 -0
  38. package/lib/VoIP/relay-transport.d.ts +43 -0
  39. package/lib/VoIP/relay-transport.js +559 -0
  40. package/lib/VoIP/signaling.js +624 -0
  41. package/lib/VoIP/types.d.ts +69 -0
  42. package/lib/VoIP/types.js +17 -0
  43. package/lib/VoIP/wasm-engine.d.ts +103 -0
  44. package/lib/VoIP/wasm-engine.js +1214 -0
  45. package/lib/VoIP/worker-bootstrap.js +1042 -0
  46. package/lib/WABinary/generic-utils.js +8 -0
  47. package/lib/assets/wasm/loader.js +5 -0
  48. package/lib/assets/wasm/whatsapp.wasm +0 -0
  49. package/lib/assets/wasm/worker-modules.js +273 -0
  50. package/lib/index.js +4 -0
  51. 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
+ }