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