whats-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ /**
2
+ * whats-mcp — Label tools (WhatsApp Business) (3 tools).
3
+ *
4
+ * manage_label, manage_chat_label, manage_message_label
5
+ */
6
+
7
+ "use strict";
8
+
9
+ const { phoneToJid, okResult, errResult } = require("../helpers");
10
+
11
+ module.exports = [
12
+ // 1. manage_label
13
+ {
14
+ definition: {
15
+ name: "manage_label",
16
+ description:
17
+ "Create, edit, or delete a WhatsApp Business label." +
18
+ " Labels are used to organize chats and messages." +
19
+ " Note: Only available for WhatsApp Business accounts.",
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ action: {
24
+ type: "string",
25
+ enum: ["create", "edit", "delete", "list"],
26
+ description: "Action to perform.",
27
+ },
28
+ label_id: {
29
+ type: "string",
30
+ description: "Label ID (required for edit/delete).",
31
+ },
32
+ name: {
33
+ type: "string",
34
+ description: "Label name (required for create/edit).",
35
+ },
36
+ color: {
37
+ type: "integer",
38
+ description: "Label color index (0-19). Optional for create/edit.",
39
+ },
40
+ },
41
+ required: ["action"],
42
+ },
43
+ },
44
+ handler: async ({ action, label_id, name, color }, { sock }) => {
45
+ if (action === "list") {
46
+ try {
47
+ const labels = await sock.getLabels();
48
+ return okResult({
49
+ count: labels.length,
50
+ labels: labels.map((l) => ({
51
+ id: l.id,
52
+ name: l.name,
53
+ color: l.color,
54
+ predefined: l.predefinedId !== undefined,
55
+ })),
56
+ });
57
+ } catch (err) {
58
+ return errResult("Could not fetch labels. Are you using a WhatsApp Business account? " + err.message);
59
+ }
60
+ }
61
+
62
+ if (action === "create") {
63
+ if (!name) return errResult("Label name is required for create.");
64
+ const result = await sock.addLabel({ name, color: color ?? 0 });
65
+ return okResult({ status: "created", label: result });
66
+ }
67
+
68
+ if (action === "edit") {
69
+ if (!label_id) return errResult("label_id is required for edit.");
70
+ const updates = {};
71
+ if (name !== undefined) updates.name = name;
72
+ if (color !== undefined) updates.color = color;
73
+ await sock.editLabel(label_id, updates);
74
+ return okResult({ status: "edited", label_id });
75
+ }
76
+
77
+ if (action === "delete") {
78
+ if (!label_id) return errResult("label_id is required for delete.");
79
+ await sock.deleteLabel(label_id);
80
+ return okResult({ status: "deleted", label_id });
81
+ }
82
+
83
+ return errResult(`Unknown action: ${action}`);
84
+ },
85
+ },
86
+
87
+ // 2. manage_chat_label
88
+ {
89
+ definition: {
90
+ name: "manage_chat_label",
91
+ description: "Add or remove a label from a chat (WhatsApp Business).",
92
+ inputSchema: {
93
+ type: "object",
94
+ properties: {
95
+ action: {
96
+ type: "string",
97
+ enum: ["add", "remove"],
98
+ description: "Add or remove the label.",
99
+ },
100
+ jid: {
101
+ type: "string",
102
+ description: "Chat JID or phone number.",
103
+ },
104
+ label_id: {
105
+ type: "string",
106
+ description: "Label ID to add/remove.",
107
+ },
108
+ },
109
+ required: ["action", "jid", "label_id"],
110
+ },
111
+ },
112
+ handler: async ({ action, jid, label_id }, { sock }) => {
113
+ const chatJid = phoneToJid(jid);
114
+ if (action === "add") {
115
+ await sock.addChatLabel(chatJid, label_id);
116
+ return okResult({ status: "label_added", jid: chatJid, label_id });
117
+ }
118
+ if (action === "remove") {
119
+ await sock.removeChatLabel(chatJid, label_id);
120
+ return okResult({ status: "label_removed", jid: chatJid, label_id });
121
+ }
122
+ return errResult(`Unknown action: ${action}`);
123
+ },
124
+ },
125
+
126
+ // 3. manage_message_label
127
+ {
128
+ definition: {
129
+ name: "manage_message_label",
130
+ description: "Add or remove a label from a specific message (WhatsApp Business).",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ action: {
135
+ type: "string",
136
+ enum: ["add", "remove"],
137
+ description: "Add or remove the label.",
138
+ },
139
+ jid: {
140
+ type: "string",
141
+ description: "Chat JID.",
142
+ },
143
+ message_id: {
144
+ type: "string",
145
+ description: "Message ID to label.",
146
+ },
147
+ label_id: {
148
+ type: "string",
149
+ description: "Label ID.",
150
+ },
151
+ },
152
+ required: ["action", "jid", "message_id", "label_id"],
153
+ },
154
+ },
155
+ handler: async ({ action, jid, message_id, label_id }, { sock }) => {
156
+ const chatJid = phoneToJid(jid);
157
+ if (action === "add") {
158
+ await sock.addMessageLabel(chatJid, message_id, label_id);
159
+ return okResult({ status: "label_added", jid: chatJid, message_id, label_id });
160
+ }
161
+ if (action === "remove") {
162
+ await sock.removeMessageLabel(chatJid, message_id, label_id);
163
+ return okResult({ status: "label_removed", jid: chatJid, message_id, label_id });
164
+ }
165
+ return errResult(`Unknown action: ${action}`);
166
+ },
167
+ },
168
+ ];
@@ -0,0 +1,510 @@
1
+ /**
2
+ * whats-mcp — Messaging tools (14 tools).
3
+ *
4
+ * send_text, send_image, send_video, send_audio, send_document,
5
+ * send_sticker, send_location, send_contact, send_reaction, send_poll,
6
+ * edit_message, delete_message, forward_message, batch_send_text
7
+ */
8
+
9
+ "use strict";
10
+
11
+ const {
12
+ phoneToJid, resolveMedia, okResult, errResult,
13
+ } = require("../helpers");
14
+
15
+ // ── Helper: resolve quoted message for replies ──────────────────────────────
16
+
17
+ function _buildSendOpts(args, store) {
18
+ const opts = {};
19
+ if (args.quoted_id) {
20
+ const quoted = store.getMessage(args.quoted_id);
21
+ if (quoted) opts.quoted = quoted;
22
+ }
23
+ if (args.mentions && Array.isArray(args.mentions)) {
24
+ // Mentions should be JIDs
25
+ opts.mentions = args.mentions;
26
+ }
27
+ return opts;
28
+ }
29
+
30
+ function _fmtSent(result, jid) {
31
+ return okResult({
32
+ status: "sent",
33
+ jid,
34
+ message_id: result?.key?.id || null,
35
+ timestamp: result?.messageTimestamp
36
+ ? Number(result.messageTimestamp)
37
+ : Math.floor(Date.now() / 1000),
38
+ });
39
+ }
40
+
41
+ // ── Tool definitions ─────────────────────────────────────────────────────────
42
+
43
+ module.exports = [
44
+ // 1. send_text
45
+ {
46
+ definition: {
47
+ name: "send_text",
48
+ description:
49
+ "Send a text message to a contact or group." +
50
+ " Supports @mentions and replying to a specific message via quoted_id." +
51
+ " The jid can be a phone number (e.g. 33612345678) or full JID.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ jid: { type: "string", description: "Recipient: phone number or full JID (e.g. 33612345678 or 33612345678@s.whatsapp.net or 120363xxx@g.us)" },
56
+ text: { type: "string", description: "Message text to send. Supports WhatsApp formatting: *bold*, _italic_, ~strikethrough~, ```code```." },
57
+ quoted_id: { type: "string", description: "Optional: message ID to reply/quote." },
58
+ mentions: { type: "array", items: { type: "string" }, description: "Optional: array of JIDs to @mention in the message." },
59
+ },
60
+ required: ["jid", "text"],
61
+ },
62
+ },
63
+ handler: async ({ jid, text, quoted_id, mentions }, { sock, store }) => {
64
+ const to = phoneToJid(jid);
65
+ const content = { text };
66
+ const opts = _buildSendOpts({ quoted_id, mentions }, store);
67
+ if (mentions) content.mentions = mentions;
68
+ const result = await sock.sendMessage(to, content, opts);
69
+ return _fmtSent(result, to);
70
+ },
71
+ },
72
+
73
+ // 2. send_image
74
+ {
75
+ definition: {
76
+ name: "send_image",
77
+ description:
78
+ "Send an image to a contact or group." +
79
+ " Media source can be a URL (https://...), base64 data, or local file path.",
80
+ inputSchema: {
81
+ type: "object",
82
+ properties: {
83
+ jid: { type: "string", description: "Recipient JID or phone number." },
84
+ source: { type: "string", description: "Image source: URL, base64 string, or local file path." },
85
+ caption: { type: "string", description: "Optional caption for the image." },
86
+ quoted_id: { type: "string", description: "Optional: message ID to reply/quote." },
87
+ },
88
+ required: ["jid", "source"],
89
+ },
90
+ },
91
+ handler: async ({ jid, source, caption, quoted_id }, { sock, store }) => {
92
+ const to = phoneToJid(jid);
93
+ const content = { image: resolveMedia(source) };
94
+ if (caption) content.caption = caption;
95
+ const opts = _buildSendOpts({ quoted_id }, store);
96
+ const result = await sock.sendMessage(to, content, opts);
97
+ return _fmtSent(result, to);
98
+ },
99
+ },
100
+
101
+ // 3. send_video
102
+ {
103
+ definition: {
104
+ name: "send_video",
105
+ description:
106
+ "Send a video to a contact or group." +
107
+ " Set gif_playback=true for a GIF, or ptv=true for a video note (circle).",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ jid: { type: "string", description: "Recipient JID or phone number." },
112
+ source: { type: "string", description: "Video source: URL, base64, or local path." },
113
+ caption: { type: "string", description: "Optional caption." },
114
+ gif_playback: { type: "boolean", description: "Send as GIF (auto-playing, no sound). Default false." },
115
+ ptv: { type: "boolean", description: "Send as video note / circle message. Default false." },
116
+ quoted_id: { type: "string", description: "Optional: message ID to reply/quote." },
117
+ },
118
+ required: ["jid", "source"],
119
+ },
120
+ },
121
+ handler: async ({ jid, source, caption, gif_playback, ptv, quoted_id }, { sock, store }) => {
122
+ const to = phoneToJid(jid);
123
+ const content = { video: resolveMedia(source) };
124
+ if (caption) content.caption = caption;
125
+ if (gif_playback) content.gifPlayback = true;
126
+ if (ptv) content.ptv = true;
127
+ const opts = _buildSendOpts({ quoted_id }, store);
128
+ const result = await sock.sendMessage(to, content, opts);
129
+ return _fmtSent(result, to);
130
+ },
131
+ },
132
+
133
+ // 4. send_audio
134
+ {
135
+ definition: {
136
+ name: "send_audio",
137
+ description:
138
+ "Send an audio file or voice note." +
139
+ " Set ptt=true to send as a voice note (push-to-talk style).",
140
+ inputSchema: {
141
+ type: "object",
142
+ properties: {
143
+ jid: { type: "string", description: "Recipient JID or phone number." },
144
+ source: { type: "string", description: "Audio source: URL, base64, or local path." },
145
+ ptt: { type: "boolean", description: "Send as voice note (push-to-talk). Default false." },
146
+ quoted_id: { type: "string", description: "Optional: message ID to reply/quote." },
147
+ },
148
+ required: ["jid", "source"],
149
+ },
150
+ },
151
+ handler: async ({ jid, source, ptt, quoted_id }, { sock, store }) => {
152
+ const to = phoneToJid(jid);
153
+ const content = { audio: resolveMedia(source) };
154
+ if (ptt) content.ptt = true;
155
+ const opts = _buildSendOpts({ quoted_id }, store);
156
+ const result = await sock.sendMessage(to, content, opts);
157
+ return _fmtSent(result, to);
158
+ },
159
+ },
160
+
161
+ // 5. send_document
162
+ {
163
+ definition: {
164
+ name: "send_document",
165
+ description:
166
+ "Send a document/file to a contact or group." +
167
+ " Supports any file type — specify mimetype and filename.",
168
+ inputSchema: {
169
+ type: "object",
170
+ properties: {
171
+ jid: { type: "string", description: "Recipient JID or phone number." },
172
+ source: { type: "string", description: "Document source: URL, base64, or local path." },
173
+ filename: { type: "string", description: "Display filename (e.g. 'report.pdf')." },
174
+ mimetype: { type: "string", description: "MIME type (e.g. 'application/pdf'). Auto-detected if omitted." },
175
+ caption: { type: "string", description: "Optional caption." },
176
+ quoted_id: { type: "string", description: "Optional: message ID to reply/quote." },
177
+ },
178
+ required: ["jid", "source"],
179
+ },
180
+ },
181
+ handler: async ({ jid, source, filename, mimetype, caption, quoted_id }, { sock, store }) => {
182
+ const to = phoneToJid(jid);
183
+ const content = {
184
+ document: resolveMedia(source),
185
+ mimetype: mimetype || "application/octet-stream",
186
+ };
187
+ if (filename) content.fileName = filename;
188
+ if (caption) content.caption = caption;
189
+ const opts = _buildSendOpts({ quoted_id }, store);
190
+ const result = await sock.sendMessage(to, content, opts);
191
+ return _fmtSent(result, to);
192
+ },
193
+ },
194
+
195
+ // 6. send_sticker
196
+ {
197
+ definition: {
198
+ name: "send_sticker",
199
+ description: "Send a sticker (WebP format recommended).",
200
+ inputSchema: {
201
+ type: "object",
202
+ properties: {
203
+ jid: { type: "string", description: "Recipient JID or phone number." },
204
+ source: { type: "string", description: "Sticker image source: URL, base64, or local path (WebP format)." },
205
+ quoted_id: { type: "string", description: "Optional: message ID to reply/quote." },
206
+ },
207
+ required: ["jid", "source"],
208
+ },
209
+ },
210
+ handler: async ({ jid, source, quoted_id }, { sock, store }) => {
211
+ const to = phoneToJid(jid);
212
+ const content = { sticker: resolveMedia(source) };
213
+ const opts = _buildSendOpts({ quoted_id }, store);
214
+ const result = await sock.sendMessage(to, content, opts);
215
+ return _fmtSent(result, to);
216
+ },
217
+ },
218
+
219
+ // 7. send_location
220
+ {
221
+ definition: {
222
+ name: "send_location",
223
+ description: "Send a GPS location pin.",
224
+ inputSchema: {
225
+ type: "object",
226
+ properties: {
227
+ jid: { type: "string", description: "Recipient JID or phone number." },
228
+ latitude: { type: "number", description: "Latitude (decimal degrees)." },
229
+ longitude: { type: "number", description: "Longitude (decimal degrees)." },
230
+ name: { type: "string", description: "Optional location name." },
231
+ address: { type: "string", description: "Optional address text." },
232
+ quoted_id: { type: "string", description: "Optional: message ID to reply/quote." },
233
+ },
234
+ required: ["jid", "latitude", "longitude"],
235
+ },
236
+ },
237
+ handler: async ({ jid, latitude, longitude, name, address, quoted_id }, { sock, store }) => {
238
+ const to = phoneToJid(jid);
239
+ const content = {
240
+ location: {
241
+ degreesLatitude: latitude,
242
+ degreesLongitude: longitude,
243
+ },
244
+ };
245
+ if (name) content.location.name = name;
246
+ if (address) content.location.address = address;
247
+ const opts = _buildSendOpts({ quoted_id }, store);
248
+ const result = await sock.sendMessage(to, content, opts);
249
+ return _fmtSent(result, to);
250
+ },
251
+ },
252
+
253
+ // 8. send_contact
254
+ {
255
+ definition: {
256
+ name: "send_contact",
257
+ description: "Send one or more contact cards (vCards).",
258
+ inputSchema: {
259
+ type: "object",
260
+ properties: {
261
+ jid: { type: "string", description: "Recipient JID or phone number." },
262
+ contacts: {
263
+ type: "array",
264
+ description: "Array of contacts to send.",
265
+ items: {
266
+ type: "object",
267
+ properties: {
268
+ name: { type: "string", description: "Contact display name." },
269
+ phone: { type: "string", description: "Contact phone number." },
270
+ },
271
+ required: ["name", "phone"],
272
+ },
273
+ },
274
+ quoted_id: { type: "string", description: "Optional: message ID to reply/quote." },
275
+ },
276
+ required: ["jid", "contacts"],
277
+ },
278
+ },
279
+ handler: async ({ jid, contacts, quoted_id }, { sock, store }) => {
280
+ const to = phoneToJid(jid);
281
+ const vCards = contacts.map((c) => {
282
+ const phone = c.phone.replace(/[^0-9+]/g, "");
283
+ return (
284
+ "BEGIN:VCARD\n" +
285
+ "VERSION:3.0\n" +
286
+ `FN:${c.name}\n` +
287
+ `TEL;type=CELL;type=VOICE;waid=${phone.replace("+", "")}:${phone}\n` +
288
+ "END:VCARD"
289
+ );
290
+ });
291
+ const content = {
292
+ contacts: {
293
+ displayName: contacts.length === 1 ? contacts[0].name : `${contacts.length} contacts`,
294
+ contacts: vCards.map((vcard) => ({ vcard })),
295
+ },
296
+ };
297
+ const opts = _buildSendOpts({ quoted_id }, store);
298
+ const result = await sock.sendMessage(to, content, opts);
299
+ return _fmtSent(result, to);
300
+ },
301
+ },
302
+
303
+ // 9. send_reaction
304
+ {
305
+ definition: {
306
+ name: "send_reaction",
307
+ description:
308
+ "React to a message with an emoji." +
309
+ " Send an empty emoji string to remove the reaction.",
310
+ inputSchema: {
311
+ type: "object",
312
+ properties: {
313
+ jid: { type: "string", description: "Chat JID where the message is." },
314
+ message_id: { type: "string", description: "ID of the message to react to." },
315
+ emoji: { type: "string", description: "Emoji reaction (e.g. '👍', '❤️'). Empty string to remove." },
316
+ from_me: { type: "boolean", description: "Whether the target message was sent by you. Default false." },
317
+ },
318
+ required: ["jid", "message_id", "emoji"],
319
+ },
320
+ },
321
+ handler: async ({ jid, message_id, emoji, from_me }, { sock }) => {
322
+ const to = phoneToJid(jid);
323
+ const content = {
324
+ react: {
325
+ text: emoji,
326
+ key: {
327
+ remoteJid: to,
328
+ id: message_id,
329
+ fromMe: from_me ?? false,
330
+ },
331
+ },
332
+ };
333
+ await sock.sendMessage(to, content);
334
+ return okResult({
335
+ status: emoji ? "reacted" : "reaction_removed",
336
+ jid: to,
337
+ message_id,
338
+ emoji: emoji || null,
339
+ });
340
+ },
341
+ },
342
+
343
+ // 10. send_poll
344
+ {
345
+ definition: {
346
+ name: "send_poll",
347
+ description:
348
+ "Create a poll in a chat." +
349
+ " By default single-select; set selectable_count > 1 for multi-select.",
350
+ inputSchema: {
351
+ type: "object",
352
+ properties: {
353
+ jid: { type: "string", description: "Recipient JID or phone number." },
354
+ question: { type: "string", description: "Poll question text." },
355
+ options: { type: "array", items: { type: "string" }, description: "Array of poll option strings (2-12)." },
356
+ selectable_count: { type: "integer", description: "How many options can be selected (default 1 = single-select)." },
357
+ },
358
+ required: ["jid", "question", "options"],
359
+ },
360
+ },
361
+ handler: async ({ jid, question, options, selectable_count }, { sock }) => {
362
+ if (!options || options.length < 2) {
363
+ return errResult("A poll requires at least 2 options.");
364
+ }
365
+ const to = phoneToJid(jid);
366
+ const content = {
367
+ poll: {
368
+ name: question,
369
+ values: options,
370
+ selectableCount: selectable_count ?? 1,
371
+ },
372
+ };
373
+ const result = await sock.sendMessage(to, content);
374
+ return _fmtSent(result, to);
375
+ },
376
+ },
377
+
378
+ // 11. edit_message
379
+ {
380
+ definition: {
381
+ name: "edit_message",
382
+ description:
383
+ "Edit a previously sent message (text only)." +
384
+ " You can only edit messages you sent.",
385
+ inputSchema: {
386
+ type: "object",
387
+ properties: {
388
+ jid: { type: "string", description: "Chat JID where the message is." },
389
+ message_id: { type: "string", description: "ID of the message to edit." },
390
+ new_text: { type: "string", description: "New text content." },
391
+ },
392
+ required: ["jid", "message_id", "new_text"],
393
+ },
394
+ },
395
+ handler: async ({ jid, message_id, new_text }, { sock }) => {
396
+ const to = phoneToJid(jid);
397
+ const content = {
398
+ text: new_text,
399
+ edit: { remoteJid: to, id: message_id, fromMe: true },
400
+ };
401
+ const result = await sock.sendMessage(to, content);
402
+ return okResult({ status: "edited", jid: to, message_id });
403
+ },
404
+ },
405
+
406
+ // 12. delete_message
407
+ {
408
+ definition: {
409
+ name: "delete_message",
410
+ description:
411
+ "Delete (revoke) a message." +
412
+ " You can delete your own messages for everyone, or in groups admins can delete anyone's messages.",
413
+ inputSchema: {
414
+ type: "object",
415
+ properties: {
416
+ jid: { type: "string", description: "Chat JID." },
417
+ message_id: { type: "string", description: "ID of the message to delete." },
418
+ from_me: { type: "boolean", description: "Whether you sent the message. Default true." },
419
+ participant: { type: "string", description: "In groups: JID of the message sender (required if from_me=false)." },
420
+ },
421
+ required: ["jid", "message_id"],
422
+ },
423
+ },
424
+ handler: async ({ jid, message_id, from_me, participant }, { sock }) => {
425
+ const to = phoneToJid(jid);
426
+ const key = {
427
+ remoteJid: to,
428
+ id: message_id,
429
+ fromMe: from_me ?? true,
430
+ };
431
+ if (participant) key.participant = participant;
432
+ await sock.sendMessage(to, { delete: key });
433
+ return okResult({ status: "deleted", jid: to, message_id });
434
+ },
435
+ },
436
+
437
+ // 13. forward_message
438
+ {
439
+ definition: {
440
+ name: "forward_message",
441
+ description: "Forward an existing message to another chat.",
442
+ inputSchema: {
443
+ type: "object",
444
+ properties: {
445
+ to_jid: { type: "string", description: "Destination JID to forward to." },
446
+ message_id: { type: "string", description: "ID of the message to forward." },
447
+ },
448
+ required: ["to_jid", "message_id"],
449
+ },
450
+ },
451
+ handler: async ({ to_jid, message_id }, { sock, store }) => {
452
+ const msg = store.getMessage(message_id);
453
+ if (!msg) {
454
+ return errResult(`Message ${message_id} not found in store. It must be a recent message.`);
455
+ }
456
+ const to = phoneToJid(to_jid);
457
+ const result = await sock.sendMessage(to, { forward: msg, force: true });
458
+ return _fmtSent(result, to);
459
+ },
460
+ },
461
+
462
+ // 14. batch_send_text
463
+ {
464
+ definition: {
465
+ name: "batch_send_text",
466
+ description:
467
+ "Send the same text message to multiple recipients." +
468
+ " Returns a summary of successes and failures.",
469
+ inputSchema: {
470
+ type: "object",
471
+ properties: {
472
+ jids: {
473
+ type: "array",
474
+ items: { type: "string" },
475
+ description: "Array of recipient JIDs or phone numbers.",
476
+ },
477
+ text: { type: "string", description: "Message text to send to all recipients." },
478
+ delay_ms: {
479
+ type: "integer",
480
+ description: "Delay in ms between sends to avoid rate-limiting. Default 1000.",
481
+ },
482
+ },
483
+ required: ["jids", "text"],
484
+ },
485
+ },
486
+ handler: async ({ jids, text, delay_ms }, { sock }) => {
487
+ if (!jids || jids.length === 0) {
488
+ return errResult("At least one recipient is required.");
489
+ }
490
+ const delay = delay_ms ?? 1000;
491
+ const results = [];
492
+ for (const jid of jids) {
493
+ const to = phoneToJid(jid);
494
+ try {
495
+ const r = await sock.sendMessage(to, { text });
496
+ results.push({ jid: to, status: "sent", message_id: r?.key?.id || null });
497
+ } catch (err) {
498
+ results.push({ jid: to, status: "failed", error: err.message });
499
+ }
500
+ const idx = jids.indexOf(jid);
501
+ if (delay > 0 && idx < jids.length - 1) {
502
+ await new Promise((r) => setTimeout(r, delay));
503
+ }
504
+ }
505
+ const sent = results.filter((r) => r.status === "sent").length;
506
+ const failed = results.filter((r) => r.status === "failed").length;
507
+ return okResult({ total: jids.length, sent, failed, results });
508
+ },
509
+ },
510
+ ];