novaapp-sdk 1.0.10 → 1.2.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.
- package/dist/index.d.mts +1627 -75
- package/dist/index.d.ts +1627 -75
- package/dist/index.js +1911 -146
- package/dist/index.mjs +1891 -145
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,14 +20,33 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ActionRowBuilder: () => ActionRowBuilder,
|
|
24
|
+
ButtonBuilder: () => ButtonBuilder,
|
|
25
|
+
ChannelsAPI: () => ChannelsAPI,
|
|
26
|
+
Collection: () => Collection,
|
|
23
27
|
CommandsAPI: () => CommandsAPI,
|
|
28
|
+
Cooldown: () => Cooldown,
|
|
29
|
+
CooldownManager: () => CooldownManager,
|
|
30
|
+
EmbedBuilder: () => EmbedBuilder,
|
|
24
31
|
HttpClient: () => HttpClient,
|
|
32
|
+
InteractionOptions: () => InteractionOptions,
|
|
25
33
|
InteractionsAPI: () => InteractionsAPI,
|
|
34
|
+
Logger: () => Logger,
|
|
26
35
|
MembersAPI: () => MembersAPI,
|
|
36
|
+
MessageBuilder: () => MessageBuilder,
|
|
27
37
|
MessagesAPI: () => MessagesAPI,
|
|
38
|
+
ModalBuilder: () => ModalBuilder,
|
|
28
39
|
NovaClient: () => NovaClient,
|
|
40
|
+
NovaInteraction: () => NovaInteraction,
|
|
41
|
+
NovaMessage: () => NovaMessage,
|
|
29
42
|
PermissionsAPI: () => PermissionsAPI,
|
|
30
|
-
|
|
43
|
+
PollBuilder: () => PollBuilder,
|
|
44
|
+
ReactionsAPI: () => ReactionsAPI,
|
|
45
|
+
SelectMenuBuilder: () => SelectMenuBuilder,
|
|
46
|
+
ServersAPI: () => ServersAPI,
|
|
47
|
+
SlashCommandBuilder: () => SlashCommandBuilder,
|
|
48
|
+
SlashCommandOptionBuilder: () => SlashCommandOptionBuilder,
|
|
49
|
+
TextInputBuilder: () => TextInputBuilder
|
|
31
50
|
});
|
|
32
51
|
module.exports = __toCommonJS(index_exports);
|
|
33
52
|
|
|
@@ -125,6 +144,70 @@ var MessagesAPI = class {
|
|
|
125
144
|
typing(channelId) {
|
|
126
145
|
return this.http.post(`/channels/${channelId}/typing`);
|
|
127
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Fetch a single message by ID.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* const msg = await client.messages.fetchOne('message-id')
|
|
152
|
+
*/
|
|
153
|
+
fetchOne(messageId) {
|
|
154
|
+
return this.http.get(`/messages/${messageId}`);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Pin a message in its channel.
|
|
158
|
+
* The bot must have the `messages.manage` scope.
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* await client.messages.pin('message-id')
|
|
162
|
+
*/
|
|
163
|
+
pin(messageId) {
|
|
164
|
+
return this.http.post(`/messages/${messageId}/pin`);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Unpin a message.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* await client.messages.unpin('message-id')
|
|
171
|
+
*/
|
|
172
|
+
unpin(messageId) {
|
|
173
|
+
return this.http.delete(`/messages/${messageId}/pin`);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Fetch all pinned messages in a channel.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* const pins = await client.messages.fetchPinned('channel-id')
|
|
180
|
+
*/
|
|
181
|
+
fetchPinned(channelId) {
|
|
182
|
+
return this.http.get(`/channels/${channelId}/pins`);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Add a reaction to a message.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* await client.messages.addReaction('message-id', '👍')
|
|
189
|
+
*/
|
|
190
|
+
addReaction(messageId, emoji) {
|
|
191
|
+
return this.http.post(`/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Remove the bot's reaction from a message.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* await client.messages.removeReaction('message-id', '👍')
|
|
198
|
+
*/
|
|
199
|
+
removeReaction(messageId, emoji) {
|
|
200
|
+
return this.http.delete(`/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Fetch all reactions on a message.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* const reactions = await client.messages.fetchReactions('message-id')
|
|
207
|
+
*/
|
|
208
|
+
fetchReactions(messageId) {
|
|
209
|
+
return this.http.get(`/messages/${messageId}/reactions`);
|
|
210
|
+
}
|
|
128
211
|
};
|
|
129
212
|
|
|
130
213
|
// src/api/commands.ts
|
|
@@ -252,6 +335,61 @@ var MembersAPI = class {
|
|
|
252
335
|
ban(serverId, userId, reason) {
|
|
253
336
|
return this.http.post(`/servers/${serverId}/members/${userId}/ban`, { reason });
|
|
254
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Unban a previously banned user.
|
|
340
|
+
* Requires the `members.ban` scope.
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* await client.members.unban('server-id', 'user-id')
|
|
344
|
+
*/
|
|
345
|
+
unban(serverId, userId) {
|
|
346
|
+
return this.http.delete(`/servers/${serverId}/bans/${userId}`);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Fetch the ban list for a server.
|
|
350
|
+
* Requires the `members.ban` scope.
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* const bans = await client.members.listBans('server-id')
|
|
354
|
+
* for (const ban of bans) {
|
|
355
|
+
* console.log(`${ban.username} — ${ban.reason ?? 'No reason'}`)
|
|
356
|
+
* }
|
|
357
|
+
*/
|
|
358
|
+
listBans(serverId) {
|
|
359
|
+
return this.http.get(`/servers/${serverId}/bans`);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Send a direct message to a user (DM).
|
|
363
|
+
* Requires the `messages.write` scope.
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* await client.members.dm('user-id', { content: 'Hello!' })
|
|
367
|
+
* await client.members.dm('user-id', 'Hello from the bot!')
|
|
368
|
+
*/
|
|
369
|
+
dm(userId, options) {
|
|
370
|
+
const body = typeof options === "string" ? { content: options } : options;
|
|
371
|
+
return this.http.post(`/users/${userId}/dm`, body);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Add a role to a member.
|
|
375
|
+
* Requires the `members.roles` scope.
|
|
376
|
+
*
|
|
377
|
+
* @example
|
|
378
|
+
* await client.members.addRole('server-id', 'user-id', 'role-id')
|
|
379
|
+
*/
|
|
380
|
+
addRole(serverId, userId, roleId) {
|
|
381
|
+
return this.http.post(`/servers/${serverId}/members/${userId}/roles/${roleId}`);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Remove a role from a member.
|
|
385
|
+
* Requires the `members.roles` scope.
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* await client.members.removeRole('server-id', 'user-id', 'role-id')
|
|
389
|
+
*/
|
|
390
|
+
removeRole(serverId, userId, roleId) {
|
|
391
|
+
return this.http.delete(`/servers/${serverId}/members/${userId}/roles/${roleId}`);
|
|
392
|
+
}
|
|
255
393
|
};
|
|
256
394
|
|
|
257
395
|
// src/api/servers.ts
|
|
@@ -269,6 +407,17 @@ var ServersAPI = class {
|
|
|
269
407
|
list() {
|
|
270
408
|
return this.http.get("/servers");
|
|
271
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* Fetch all roles in a server.
|
|
412
|
+
* Results are sorted by position (lowest first).
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* const roles = await client.servers.listRoles('server-id')
|
|
416
|
+
* const adminRole = roles.find(r => r.name === 'Admin')
|
|
417
|
+
*/
|
|
418
|
+
listRoles(serverId) {
|
|
419
|
+
return this.http.get(`/servers/${serverId}/roles`);
|
|
420
|
+
}
|
|
272
421
|
};
|
|
273
422
|
|
|
274
423
|
// src/api/interactions.ts
|
|
@@ -435,183 +584,1799 @@ var PermissionsAPI = class {
|
|
|
435
584
|
}
|
|
436
585
|
};
|
|
437
586
|
|
|
438
|
-
// src/
|
|
439
|
-
var
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
"message.deleted": "messageDelete",
|
|
443
|
-
"message.reaction_added": "reactionAdd",
|
|
444
|
-
"message.reaction_removed": "reactionRemove",
|
|
445
|
-
"user.joined_server": "memberAdd",
|
|
446
|
-
"user.left_server": "memberRemove",
|
|
447
|
-
"user.started_typing": "typingStart"
|
|
448
|
-
};
|
|
449
|
-
var NovaClient = class extends import_node_events.EventEmitter {
|
|
450
|
-
constructor(options) {
|
|
451
|
-
super();
|
|
452
|
-
/** The authenticated bot application. Available after `ready` fires. */
|
|
453
|
-
this.botUser = null;
|
|
454
|
-
this.socket = null;
|
|
455
|
-
if (!options.token) {
|
|
456
|
-
throw new Error("[nova-bot-sdk] A bot token is required.");
|
|
457
|
-
}
|
|
458
|
-
if (!options.token.startsWith("nova_bot_")) {
|
|
459
|
-
console.warn('[nova-bot-sdk] Warning: token does not start with "nova_bot_". Are you sure this is a bot token?');
|
|
460
|
-
}
|
|
461
|
-
this.options = {
|
|
462
|
-
token: options.token,
|
|
463
|
-
baseUrl: options.baseUrl ?? "https://novachatapp.com"
|
|
464
|
-
};
|
|
465
|
-
this.http = new HttpClient(this.options.baseUrl, this.options.token);
|
|
466
|
-
this.messages = new MessagesAPI(this.http);
|
|
467
|
-
this.commands = new CommandsAPI(this.http);
|
|
468
|
-
this.members = new MembersAPI(this.http);
|
|
469
|
-
this.servers = new ServersAPI(this.http);
|
|
470
|
-
this.interactions = new InteractionsAPI(this.http, this);
|
|
471
|
-
this.permissions = new PermissionsAPI(this.http);
|
|
472
|
-
this.on("error", () => {
|
|
473
|
-
});
|
|
474
|
-
const cleanup = () => this.disconnect();
|
|
475
|
-
process.once("beforeExit", cleanup);
|
|
476
|
-
process.once("SIGINT", () => {
|
|
477
|
-
cleanup();
|
|
478
|
-
process.exit(0);
|
|
479
|
-
});
|
|
480
|
-
process.once("SIGTERM", () => {
|
|
481
|
-
cleanup();
|
|
482
|
-
process.exit(0);
|
|
483
|
-
});
|
|
587
|
+
// src/api/channels.ts
|
|
588
|
+
var ChannelsAPI = class {
|
|
589
|
+
constructor(http) {
|
|
590
|
+
this.http = http;
|
|
484
591
|
}
|
|
485
592
|
/**
|
|
486
|
-
*
|
|
487
|
-
* Resolves when the `ready` event is received.
|
|
593
|
+
* Fetch all channels in a server the bot is a member of.
|
|
488
594
|
*
|
|
489
595
|
* @example
|
|
490
|
-
* await client.
|
|
596
|
+
* const channels = await client.channels.list('server-id')
|
|
597
|
+
* const textChannels = channels.filter(c => c.type === 'TEXT')
|
|
491
598
|
*/
|
|
492
|
-
|
|
493
|
-
return
|
|
494
|
-
const gatewayUrl = this.options.baseUrl.replace(/\/$/, "") + "/bot-gateway";
|
|
495
|
-
this.socket = (0, import_socket.io)(gatewayUrl, {
|
|
496
|
-
path: "/socket.io",
|
|
497
|
-
transports: ["websocket"],
|
|
498
|
-
auth: { botToken: this.options.token },
|
|
499
|
-
reconnection: true,
|
|
500
|
-
reconnectionAttempts: Infinity,
|
|
501
|
-
reconnectionDelay: 1e3,
|
|
502
|
-
reconnectionDelayMax: 3e4
|
|
503
|
-
});
|
|
504
|
-
const onConnectError = (err) => {
|
|
505
|
-
this.socket?.disconnect();
|
|
506
|
-
this.socket = null;
|
|
507
|
-
reject(new Error(`[nova-bot-sdk] Gateway connection failed: ${err.message}`));
|
|
508
|
-
};
|
|
509
|
-
this.socket.once("connect_error", onConnectError);
|
|
510
|
-
this.socket.once("bot:ready", (data) => {
|
|
511
|
-
this.socket.off("connect_error", onConnectError);
|
|
512
|
-
this.botUser = {
|
|
513
|
-
...data.bot,
|
|
514
|
-
scopes: data.scopes,
|
|
515
|
-
// Flatten botUser fields for convenience so bot.username works
|
|
516
|
-
username: data.bot.botUser?.username ?? data.bot.name,
|
|
517
|
-
displayName: data.bot.botUser?.displayName ?? data.bot.name
|
|
518
|
-
};
|
|
519
|
-
this.emit("ready", this.botUser);
|
|
520
|
-
resolve();
|
|
521
|
-
});
|
|
522
|
-
this.socket.on("interaction:created", (interaction) => {
|
|
523
|
-
this.emit("interactionCreate", interaction);
|
|
524
|
-
});
|
|
525
|
-
this.socket.on("bot:event", (event) => {
|
|
526
|
-
this.emit("event", event);
|
|
527
|
-
const shorthand = EVENT_MAP[event.type];
|
|
528
|
-
if (shorthand) {
|
|
529
|
-
this.emit(shorthand, event.data);
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
this.socket.on("bot:error", (err) => {
|
|
533
|
-
this.emit("error", err);
|
|
534
|
-
});
|
|
535
|
-
this.socket.on("disconnect", (reason) => {
|
|
536
|
-
this.emit("disconnect", reason);
|
|
537
|
-
});
|
|
538
|
-
});
|
|
599
|
+
list(serverId) {
|
|
600
|
+
return this.http.get(`/servers/${serverId}/channels`);
|
|
539
601
|
}
|
|
540
602
|
/**
|
|
541
|
-
*
|
|
603
|
+
* Fetch a single channel by ID.
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* const channel = await client.channels.fetch('channel-id')
|
|
607
|
+
* console.log(channel.name, channel.type)
|
|
542
608
|
*/
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const sock = this.socket;
|
|
546
|
-
this.socket = null;
|
|
547
|
-
try {
|
|
548
|
-
sock.io.reconnection(false);
|
|
549
|
-
} catch {
|
|
550
|
-
}
|
|
551
|
-
try {
|
|
552
|
-
sock.io?.engine?.socket?.terminate?.();
|
|
553
|
-
} catch {
|
|
554
|
-
}
|
|
555
|
-
try {
|
|
556
|
-
sock.io?.engine?.socket?.destroy?.();
|
|
557
|
-
} catch {
|
|
558
|
-
}
|
|
559
|
-
try {
|
|
560
|
-
sock.io?.engine?.close?.();
|
|
561
|
-
} catch {
|
|
562
|
-
}
|
|
563
|
-
try {
|
|
564
|
-
sock.disconnect();
|
|
565
|
-
} catch {
|
|
566
|
-
}
|
|
567
|
-
}
|
|
609
|
+
fetch(channelId) {
|
|
610
|
+
return this.http.get(`/channels/${channelId}`);
|
|
568
611
|
}
|
|
569
612
|
/**
|
|
570
|
-
*
|
|
571
|
-
* Requires the `
|
|
613
|
+
* Create a new channel in a server.
|
|
614
|
+
* Requires the `channels.manage` scope.
|
|
572
615
|
*
|
|
573
616
|
* @example
|
|
574
|
-
* client.
|
|
617
|
+
* const channel = await client.channels.create('server-id', {
|
|
618
|
+
* name: 'announcements',
|
|
619
|
+
* type: 'ANNOUNCEMENT',
|
|
620
|
+
* topic: 'Official announcements only',
|
|
621
|
+
* })
|
|
575
622
|
*/
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
throw new Error("[nova-bot-sdk] Not connected. Call client.connect() first.");
|
|
579
|
-
}
|
|
580
|
-
this.socket.emit("bot:message:send", { channelId, content });
|
|
623
|
+
create(serverId, options) {
|
|
624
|
+
return this.http.post(`/servers/${serverId}/channels`, options);
|
|
581
625
|
}
|
|
582
626
|
/**
|
|
583
|
-
*
|
|
627
|
+
* Edit an existing channel.
|
|
628
|
+
* Requires the `channels.manage` scope.
|
|
629
|
+
*
|
|
630
|
+
* @example
|
|
631
|
+
* await client.channels.edit('channel-id', { topic: 'New topic!' })
|
|
584
632
|
*/
|
|
585
|
-
|
|
586
|
-
this.
|
|
633
|
+
edit(channelId, options) {
|
|
634
|
+
return this.http.patch(`/channels/${channelId}`, options);
|
|
587
635
|
}
|
|
588
636
|
/**
|
|
589
|
-
*
|
|
637
|
+
* Delete a channel.
|
|
638
|
+
* Requires the `channels.manage` scope.
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* await client.channels.delete('channel-id')
|
|
590
642
|
*/
|
|
591
|
-
|
|
592
|
-
this.
|
|
643
|
+
delete(channelId) {
|
|
644
|
+
return this.http.delete(`/channels/${channelId}`);
|
|
593
645
|
}
|
|
594
|
-
|
|
595
|
-
|
|
646
|
+
/**
|
|
647
|
+
* Fetch messages from a channel.
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* const messages = await client.channels.fetchMessages('channel-id', { limit: 50 })
|
|
651
|
+
*/
|
|
652
|
+
fetchMessages(channelId, options = {}) {
|
|
653
|
+
const params = new URLSearchParams();
|
|
654
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
655
|
+
if (options.before) params.set("before", options.before);
|
|
656
|
+
const qs = params.toString();
|
|
657
|
+
return this.http.get(`/channels/${channelId}/messages${qs ? `?${qs}` : ""}`);
|
|
596
658
|
}
|
|
597
|
-
|
|
598
|
-
|
|
659
|
+
/**
|
|
660
|
+
* Fetch all pinned messages in a channel.
|
|
661
|
+
*
|
|
662
|
+
* @example
|
|
663
|
+
* const pins = await client.channels.fetchPins('channel-id')
|
|
664
|
+
*/
|
|
665
|
+
fetchPins(channelId) {
|
|
666
|
+
return this.http.get(`/channels/${channelId}/pins`);
|
|
599
667
|
}
|
|
600
|
-
|
|
601
|
-
|
|
668
|
+
/**
|
|
669
|
+
* Send a typing indicator in a channel.
|
|
670
|
+
* Displayed to users for ~5 seconds.
|
|
671
|
+
*
|
|
672
|
+
* @example
|
|
673
|
+
* await client.channels.startTyping('channel-id')
|
|
674
|
+
*/
|
|
675
|
+
startTyping(channelId) {
|
|
676
|
+
return this.http.post(`/channels/${channelId}/typing`);
|
|
602
677
|
}
|
|
603
|
-
|
|
604
|
-
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
// src/api/reactions.ts
|
|
681
|
+
var ReactionsAPI = class {
|
|
682
|
+
constructor(http) {
|
|
683
|
+
this.http = http;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Add a reaction to a message.
|
|
687
|
+
* Use a plain emoji character or a custom emoji ID.
|
|
688
|
+
*
|
|
689
|
+
* @example
|
|
690
|
+
* await client.reactions.add('message-id', '👍')
|
|
691
|
+
* await client.reactions.add('message-id', '🎉')
|
|
692
|
+
*/
|
|
693
|
+
add(messageId, emoji) {
|
|
694
|
+
const encoded = encodeURIComponent(emoji);
|
|
695
|
+
return this.http.post(`/messages/${messageId}/reactions/${encoded}`);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Remove the bot's reaction from a message.
|
|
699
|
+
*
|
|
700
|
+
* @example
|
|
701
|
+
* await client.reactions.remove('message-id', '👍')
|
|
702
|
+
*/
|
|
703
|
+
remove(messageId, emoji) {
|
|
704
|
+
const encoded = encodeURIComponent(emoji);
|
|
705
|
+
return this.http.delete(`/messages/${messageId}/reactions/${encoded}`);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Remove all reactions from a message.
|
|
709
|
+
* Requires the `messages.manage` scope.
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* await client.reactions.removeAll('message-id')
|
|
713
|
+
*/
|
|
714
|
+
removeAll(messageId) {
|
|
715
|
+
return this.http.delete(`/messages/${messageId}/reactions`);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Remove all reactions of a specific emoji from a message.
|
|
719
|
+
* Requires the `messages.manage` scope.
|
|
720
|
+
*
|
|
721
|
+
* @example
|
|
722
|
+
* await client.reactions.removeEmoji('message-id', '👍')
|
|
723
|
+
*/
|
|
724
|
+
removeEmoji(messageId, emoji) {
|
|
725
|
+
const encoded = encodeURIComponent(emoji);
|
|
726
|
+
return this.http.delete(`/messages/${messageId}/reactions/${encoded}/all`);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Fetch all reactions on a message, broken down by emoji.
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* const reactions = await client.reactions.fetch('message-id')
|
|
733
|
+
* for (const r of reactions) {
|
|
734
|
+
* console.log(`${r.emoji} — ${r.count} reactions from ${r.users.map(u => u.username).join(', ')}`)
|
|
735
|
+
* }
|
|
736
|
+
*/
|
|
737
|
+
fetch(messageId) {
|
|
738
|
+
return this.http.get(`/messages/${messageId}/reactions`);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Fetch reactions for a specific emoji on a message.
|
|
742
|
+
*
|
|
743
|
+
* @example
|
|
744
|
+
* const detail = await client.reactions.fetchEmoji('message-id', '👍')
|
|
745
|
+
* console.log(`${detail.count} thumbs ups`)
|
|
746
|
+
*/
|
|
747
|
+
fetchEmoji(messageId, emoji) {
|
|
748
|
+
const encoded = encodeURIComponent(emoji);
|
|
749
|
+
return this.http.get(`/messages/${messageId}/reactions/${encoded}`);
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// src/structures/NovaInteraction.ts
|
|
754
|
+
var InteractionOptions = class {
|
|
755
|
+
constructor(data) {
|
|
756
|
+
this._map = /* @__PURE__ */ new Map();
|
|
757
|
+
if (data && typeof data === "object") {
|
|
758
|
+
const d = data;
|
|
759
|
+
const opts = Array.isArray(d.options) ? d.options : [];
|
|
760
|
+
for (const o of opts) {
|
|
761
|
+
if (o?.name != null) this._map.set(String(o.name), o.value);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/** Whether this option was supplied by the user. */
|
|
766
|
+
has(name) {
|
|
767
|
+
return this._map.has(name);
|
|
768
|
+
}
|
|
769
|
+
getString(name, required = false) {
|
|
770
|
+
const v = this._map.get(name);
|
|
771
|
+
if (required && (v == null || v === "")) throw new Error(`Required option "${name}" is missing`);
|
|
772
|
+
return v != null ? String(v) : null;
|
|
773
|
+
}
|
|
774
|
+
getInteger(name, required = false) {
|
|
775
|
+
const v = this._map.get(name);
|
|
776
|
+
if (required && v == null) throw new Error(`Required option "${name}" is missing`);
|
|
777
|
+
return v != null ? Math.trunc(Number(v)) : null;
|
|
778
|
+
}
|
|
779
|
+
getNumber(name, required = false) {
|
|
780
|
+
const v = this._map.get(name);
|
|
781
|
+
if (required && v == null) throw new Error(`Required option "${name}" is missing`);
|
|
782
|
+
return v != null ? Number(v) : null;
|
|
783
|
+
}
|
|
784
|
+
/** Returns `true` or `false`, or `null` if not supplied. */
|
|
785
|
+
getBoolean(name) {
|
|
786
|
+
const v = this._map.get(name);
|
|
787
|
+
return v != null ? Boolean(v) : null;
|
|
788
|
+
}
|
|
789
|
+
getUser(name, required = false) {
|
|
790
|
+
return this.getString(name, required);
|
|
791
|
+
}
|
|
792
|
+
getChannel(name, required = false) {
|
|
793
|
+
return this.getString(name, required);
|
|
794
|
+
}
|
|
795
|
+
getRole(name, required = false) {
|
|
796
|
+
return this.getString(name, required);
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
var NovaInteraction = class _NovaInteraction {
|
|
800
|
+
constructor(_raw, _api) {
|
|
801
|
+
this._raw = _raw;
|
|
802
|
+
this._api = _api;
|
|
803
|
+
this.id = _raw.id;
|
|
804
|
+
this.type = _raw.type;
|
|
805
|
+
this.commandName = _raw.commandName ?? null;
|
|
806
|
+
this.customId = _raw.customId ?? null;
|
|
807
|
+
this.userId = _raw.userId;
|
|
808
|
+
this.channelId = _raw.channelId;
|
|
809
|
+
this.serverId = _raw.serverId ?? null;
|
|
810
|
+
this.triggerMsgId = _raw.triggerMsgId ?? null;
|
|
811
|
+
this.values = _raw.values ?? [];
|
|
812
|
+
this.modalData = _raw.modalData ?? {};
|
|
813
|
+
this.createdAt = _raw.createdAt;
|
|
814
|
+
this.options = new InteractionOptions(_raw.data);
|
|
815
|
+
}
|
|
816
|
+
// ── Type guards ─────────────────────────────────────────────────────────────
|
|
817
|
+
/** `true` when triggered by a `/slash` command. */
|
|
818
|
+
isSlashCommand() {
|
|
819
|
+
return this.type === "SLASH_COMMAND";
|
|
820
|
+
}
|
|
821
|
+
/** `true` when triggered by a `!prefix` command. */
|
|
822
|
+
isPrefixCommand() {
|
|
823
|
+
return this.type === "PREFIX_COMMAND";
|
|
824
|
+
}
|
|
825
|
+
/** `true` for both slash and prefix commands. */
|
|
826
|
+
isCommand() {
|
|
827
|
+
return this.isSlashCommand() || this.isPrefixCommand();
|
|
828
|
+
}
|
|
829
|
+
/** `true` when a button component was clicked. */
|
|
830
|
+
isButton() {
|
|
831
|
+
return this.type === "BUTTON_CLICK";
|
|
832
|
+
}
|
|
833
|
+
/** `true` when a select-menu option was chosen. */
|
|
834
|
+
isSelectMenu() {
|
|
835
|
+
return this.type === "SELECT_MENU";
|
|
836
|
+
}
|
|
837
|
+
/** `true` when the user submitted a modal form. */
|
|
838
|
+
isModalSubmit() {
|
|
839
|
+
return this.type === "MODAL_SUBMIT";
|
|
840
|
+
}
|
|
841
|
+
/** `true` during autocomplete suggestion requests. */
|
|
842
|
+
isAutocomplete() {
|
|
843
|
+
return this.type === "AUTOCOMPLETE";
|
|
844
|
+
}
|
|
845
|
+
/** `true` when triggered via a context-menu command. */
|
|
846
|
+
isContextMenu() {
|
|
847
|
+
return this.type === "CONTEXT_MENU";
|
|
848
|
+
}
|
|
849
|
+
// ── Actions ─────────────────────────────────────────────────────────────────
|
|
850
|
+
/**
|
|
851
|
+
* Respond with a message.
|
|
852
|
+
* Accepts a plain string or a full options object.
|
|
853
|
+
*
|
|
854
|
+
* @example
|
|
855
|
+
* await interaction.reply('Pong! 🏓')
|
|
856
|
+
* await interaction.reply({ content: 'Done!', ephemeral: true })
|
|
857
|
+
* await interaction.reply({ embed: new EmbedBuilder().setTitle('Stats').toJSON() })
|
|
858
|
+
*/
|
|
859
|
+
reply(options) {
|
|
860
|
+
return this._api.respond(this.id, typeof options === "string" ? { content: options } : options);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Respond with a message **only visible to the user** who triggered the interaction.
|
|
864
|
+
*
|
|
865
|
+
* @example
|
|
866
|
+
* await interaction.replyEphemeral('Only you can see this!')
|
|
867
|
+
*/
|
|
868
|
+
replyEphemeral(options) {
|
|
869
|
+
const resolved = typeof options === "string" ? { content: options } : options;
|
|
870
|
+
return this._api.respond(this.id, { ...resolved, ephemeral: true });
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Acknowledge the interaction without sending a response yet.
|
|
874
|
+
* Shows a loading indicator to the user.
|
|
875
|
+
* Follow up with `interaction.editReply()` when you're done.
|
|
876
|
+
*
|
|
877
|
+
* @example
|
|
878
|
+
* await interaction.defer()
|
|
879
|
+
* const data = await fetchSomeSlow()
|
|
880
|
+
* await interaction.editReply({ content: `Result: ${data}` })
|
|
881
|
+
*/
|
|
882
|
+
defer() {
|
|
883
|
+
return this._api.ack(this.id);
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Edit the previous reply (e.g. after `defer()`).
|
|
887
|
+
*
|
|
888
|
+
* @example
|
|
889
|
+
* await interaction.defer()
|
|
890
|
+
* await interaction.editReply(`Done — processed ${count} items.`)
|
|
891
|
+
*/
|
|
892
|
+
editReply(options) {
|
|
893
|
+
return this._api.respond(this.id, typeof options === "string" ? { content: options } : options);
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Open a **modal dialog** and return the user's submission as a new `NovaInteraction`.
|
|
897
|
+
* Returns `null` if the user closes the modal or the timeout expires (default: 5 min).
|
|
898
|
+
*
|
|
899
|
+
* **This is the recommended way to open modals.**
|
|
900
|
+
*
|
|
901
|
+
* @example
|
|
902
|
+
* const submitted = await interaction.openModal(
|
|
903
|
+
* new ModalBuilder()
|
|
904
|
+
* .setTitle('Submit a report')
|
|
905
|
+
* .setCustomId('report_modal')
|
|
906
|
+
* .addField(
|
|
907
|
+
* new TextInputBuilder()
|
|
908
|
+
* .setCustomId('reason')
|
|
909
|
+
* .setLabel('Reason')
|
|
910
|
+
* .setStyle('paragraph')
|
|
911
|
+
* .setRequired(true)
|
|
912
|
+
* )
|
|
913
|
+
* )
|
|
914
|
+
*
|
|
915
|
+
* if (!submitted) return // user dismissed or timed out
|
|
916
|
+
*
|
|
917
|
+
* const reason = submitted.modalData.reason
|
|
918
|
+
* await submitted.replyEphemeral(`Report received: ${reason}`)
|
|
919
|
+
*/
|
|
920
|
+
async openModal(modal, options = {}) {
|
|
921
|
+
const def = "toJSON" in modal && typeof modal.toJSON === "function" ? modal.toJSON() : modal;
|
|
922
|
+
const raw = await this._api.awaitModal(this.id, def, options);
|
|
923
|
+
if (!raw) return null;
|
|
924
|
+
return new _NovaInteraction(raw, this._api);
|
|
925
|
+
}
|
|
926
|
+
/** Returns the raw interaction data from the gateway. */
|
|
927
|
+
toJSON() {
|
|
928
|
+
return { ...this._raw };
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// src/structures/NovaMessage.ts
|
|
933
|
+
var NovaMessage = class _NovaMessage {
|
|
934
|
+
constructor(raw, messages, reactions) {
|
|
935
|
+
this.id = raw.id;
|
|
936
|
+
this.content = raw.content;
|
|
937
|
+
this.channelId = raw.channelId;
|
|
938
|
+
this.author = raw.author;
|
|
939
|
+
this.embed = raw.embed ?? null;
|
|
940
|
+
this.components = raw.components ?? [];
|
|
941
|
+
this.replyToId = raw.replyToId ?? null;
|
|
942
|
+
this.attachments = raw.attachments ?? [];
|
|
943
|
+
this.reactions = raw.reactions ?? [];
|
|
944
|
+
this.createdAt = new Date(raw.createdAt);
|
|
945
|
+
this.editedAt = raw.editedAt ? new Date(raw.editedAt) : null;
|
|
946
|
+
this._messages = messages;
|
|
947
|
+
this._reactions = reactions;
|
|
948
|
+
}
|
|
949
|
+
// ─── Type guards ─────────────────────────────────────────────────────────────
|
|
950
|
+
/** Returns true if the message was sent by a bot. */
|
|
951
|
+
isFromBot() {
|
|
952
|
+
return this.author.isBot === true;
|
|
953
|
+
}
|
|
954
|
+
/** Returns true if the message has an embed. */
|
|
955
|
+
hasEmbed() {
|
|
956
|
+
return this.embed !== null;
|
|
957
|
+
}
|
|
958
|
+
/** Returns true if the message has interactive components. */
|
|
959
|
+
hasComponents() {
|
|
960
|
+
return this.components.length > 0;
|
|
961
|
+
}
|
|
962
|
+
/** Returns true if the message has been edited. */
|
|
963
|
+
isEdited() {
|
|
964
|
+
return this.editedAt !== null;
|
|
965
|
+
}
|
|
966
|
+
// ─── Reactions ────────────────────────────────────────────────────────────────
|
|
967
|
+
/**
|
|
968
|
+
* Add a reaction to this message.
|
|
969
|
+
*
|
|
970
|
+
* @example
|
|
971
|
+
* await msg.react('👍')
|
|
972
|
+
* await msg.react('🎉')
|
|
973
|
+
*/
|
|
974
|
+
react(emoji) {
|
|
975
|
+
return this._reactions.add(this.id, emoji);
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Remove the bot's reaction from this message.
|
|
979
|
+
*
|
|
980
|
+
* @example
|
|
981
|
+
* await msg.removeReaction('👍')
|
|
982
|
+
*/
|
|
983
|
+
removeReaction(emoji) {
|
|
984
|
+
return this._reactions.remove(this.id, emoji);
|
|
985
|
+
}
|
|
986
|
+
// ─── Messaging ────────────────────────────────────────────────────────────────
|
|
987
|
+
/**
|
|
988
|
+
* Reply to this message.
|
|
989
|
+
*
|
|
990
|
+
* @example
|
|
991
|
+
* await msg.reply('Got it!')
|
|
992
|
+
* await msg.reply({ embed: new EmbedBuilder().setTitle('Result').toJSON() })
|
|
993
|
+
*/
|
|
994
|
+
reply(options) {
|
|
995
|
+
const opts = typeof options === "string" ? { content: options, replyToId: this.id } : { ...options, replyToId: this.id };
|
|
996
|
+
return this._messages.send(this.channelId, opts).then((raw) => new _NovaMessage(raw, this._messages, this._reactions));
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Edit this message.
|
|
1000
|
+
* Only works if the bot is the author.
|
|
1001
|
+
*
|
|
1002
|
+
* @example
|
|
1003
|
+
* await msg.edit('Updated content')
|
|
1004
|
+
* await msg.edit({ content: 'Updated', embed: { title: 'New embed' } })
|
|
1005
|
+
*/
|
|
1006
|
+
edit(options) {
|
|
1007
|
+
const opts = typeof options === "string" ? { content: options } : options;
|
|
1008
|
+
return this._messages.edit(this.id, opts).then((raw) => new _NovaMessage(raw, this._messages, this._reactions));
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Delete this message.
|
|
1012
|
+
* Only works if the bot is the author.
|
|
1013
|
+
*
|
|
1014
|
+
* @example
|
|
1015
|
+
* await msg.delete()
|
|
1016
|
+
*/
|
|
1017
|
+
delete() {
|
|
1018
|
+
return this._messages.delete(this.id);
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Pin this message in its channel.
|
|
1022
|
+
*
|
|
1023
|
+
* @example
|
|
1024
|
+
* await msg.pin()
|
|
1025
|
+
*/
|
|
1026
|
+
pin() {
|
|
1027
|
+
return this._messages.pin(this.id);
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Unpin this message.
|
|
1031
|
+
*
|
|
1032
|
+
* @example
|
|
1033
|
+
* await msg.unpin()
|
|
1034
|
+
*/
|
|
1035
|
+
unpin() {
|
|
1036
|
+
return this._messages.unpin(this.id);
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Re-fetch the latest version of this message from the server.
|
|
1040
|
+
*
|
|
1041
|
+
* @example
|
|
1042
|
+
* const fresh = await msg.fetch()
|
|
1043
|
+
*/
|
|
1044
|
+
async fetch() {
|
|
1045
|
+
const raw = await this._messages.fetchOne(this.id);
|
|
1046
|
+
return new _NovaMessage(raw, this._messages, this._reactions);
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Get a URL to this message (deep link).
|
|
1050
|
+
*/
|
|
1051
|
+
get url() {
|
|
1052
|
+
return `/channels/${this.channelId}/messages/${this.id}`;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Return the raw message object.
|
|
1056
|
+
*/
|
|
1057
|
+
toJSON() {
|
|
1058
|
+
return {
|
|
1059
|
+
id: this.id,
|
|
1060
|
+
content: this.content,
|
|
1061
|
+
channelId: this.channelId,
|
|
1062
|
+
author: this.author,
|
|
1063
|
+
embed: this.embed,
|
|
1064
|
+
components: this.components,
|
|
1065
|
+
replyToId: this.replyToId,
|
|
1066
|
+
attachments: this.attachments,
|
|
1067
|
+
reactions: this.reactions,
|
|
1068
|
+
createdAt: this.createdAt.toISOString(),
|
|
1069
|
+
editedAt: this.editedAt?.toISOString() ?? null
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
toString() {
|
|
1073
|
+
return `NovaMessage(${this.id}): ${this.content.slice(0, 50)}${this.content.length > 50 ? "\u2026" : ""}`;
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// src/client.ts
|
|
1078
|
+
var NovaClient = class extends import_node_events.EventEmitter {
|
|
1079
|
+
constructor(options) {
|
|
1080
|
+
super();
|
|
1081
|
+
/** The authenticated bot application. Available after `ready` fires. */
|
|
1082
|
+
this.botUser = null;
|
|
1083
|
+
this.socket = null;
|
|
1084
|
+
this._cronTimers = [];
|
|
1085
|
+
// ── Command / component routing ───────────────────────────────────────────────────
|
|
1086
|
+
this._commandHandlers = /* @__PURE__ */ new Map();
|
|
1087
|
+
this._buttonHandlers = /* @__PURE__ */ new Map();
|
|
1088
|
+
this._selectHandlers = /* @__PURE__ */ new Map();
|
|
1089
|
+
if (!options.token) {
|
|
1090
|
+
throw new Error("[nova-bot-sdk] A bot token is required.");
|
|
1091
|
+
}
|
|
1092
|
+
if (!options.token.startsWith("nova_bot_")) {
|
|
1093
|
+
console.warn('[nova-bot-sdk] Warning: token does not start with "nova_bot_". Are you sure this is a bot token?');
|
|
1094
|
+
}
|
|
1095
|
+
this.options = {
|
|
1096
|
+
token: options.token,
|
|
1097
|
+
baseUrl: options.baseUrl ?? "https://novachatapp.com"
|
|
1098
|
+
};
|
|
1099
|
+
this.http = new HttpClient(this.options.baseUrl, this.options.token);
|
|
1100
|
+
this.messages = new MessagesAPI(this.http);
|
|
1101
|
+
this.commands = new CommandsAPI(this.http);
|
|
1102
|
+
this.members = new MembersAPI(this.http);
|
|
1103
|
+
this.servers = new ServersAPI(this.http);
|
|
1104
|
+
this.interactions = new InteractionsAPI(this.http, this);
|
|
1105
|
+
this.permissions = new PermissionsAPI(this.http);
|
|
1106
|
+
this.channels = new ChannelsAPI(this.http);
|
|
1107
|
+
this.reactions = new ReactionsAPI(this.http);
|
|
1108
|
+
this.on("error", () => {
|
|
1109
|
+
});
|
|
1110
|
+
const cleanup = () => this.disconnect();
|
|
1111
|
+
process.once("beforeExit", cleanup);
|
|
1112
|
+
process.once("SIGINT", () => {
|
|
1113
|
+
cleanup();
|
|
1114
|
+
process.exit(0);
|
|
1115
|
+
});
|
|
1116
|
+
process.once("SIGTERM", () => {
|
|
1117
|
+
cleanup();
|
|
1118
|
+
process.exit(0);
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Register a handler for a slash or prefix command by name.
|
|
1123
|
+
* Automatically routes `interactionCreate` events whose `commandName` matches.
|
|
1124
|
+
*
|
|
1125
|
+
* @example
|
|
1126
|
+
* client.command('ping', async (interaction) => {
|
|
1127
|
+
* await interaction.reply('Pong! 🏓')
|
|
1128
|
+
* })
|
|
1129
|
+
*/
|
|
1130
|
+
command(name, handler) {
|
|
1131
|
+
this._commandHandlers.set(name, handler);
|
|
1132
|
+
return this;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Register a handler for a button by its `customId`.
|
|
1136
|
+
*
|
|
1137
|
+
* @example
|
|
1138
|
+
* client.button('confirm_delete', async (interaction) => {
|
|
1139
|
+
* await interaction.replyEphemeral('Deleted.')
|
|
1140
|
+
* })
|
|
1141
|
+
*/
|
|
1142
|
+
button(customId, handler) {
|
|
1143
|
+
this._buttonHandlers.set(customId, handler);
|
|
1144
|
+
return this;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Register a handler for a select menu by its `customId`.
|
|
1148
|
+
*
|
|
1149
|
+
* @example
|
|
1150
|
+
* client.selectMenu('colour_pick', async (interaction) => {
|
|
1151
|
+
* const chosen = interaction.values[0]
|
|
1152
|
+
* await interaction.reply(`You picked: ${chosen}`)
|
|
1153
|
+
* })
|
|
1154
|
+
*/
|
|
1155
|
+
selectMenu(customId, handler) {
|
|
1156
|
+
this._selectHandlers.set(customId, handler);
|
|
1157
|
+
return this;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Connect to the Nova WebSocket gateway.
|
|
1161
|
+
* Resolves when the `ready` event is received.
|
|
1162
|
+
*
|
|
1163
|
+
* @example
|
|
1164
|
+
* await client.connect()
|
|
1165
|
+
*/
|
|
1166
|
+
connect() {
|
|
1167
|
+
return new Promise((resolve, reject) => {
|
|
1168
|
+
const gatewayUrl = this.options.baseUrl.replace(/\/$/, "") + "/bot-gateway";
|
|
1169
|
+
this.socket = (0, import_socket.io)(gatewayUrl, {
|
|
1170
|
+
path: "/socket.io",
|
|
1171
|
+
transports: ["websocket"],
|
|
1172
|
+
auth: { botToken: this.options.token },
|
|
1173
|
+
reconnection: true,
|
|
1174
|
+
reconnectionAttempts: Infinity,
|
|
1175
|
+
reconnectionDelay: 1e3,
|
|
1176
|
+
reconnectionDelayMax: 3e4
|
|
1177
|
+
});
|
|
1178
|
+
const onConnectError = (err) => {
|
|
1179
|
+
this.socket?.disconnect();
|
|
1180
|
+
this.socket = null;
|
|
1181
|
+
reject(new Error(`[nova-bot-sdk] Gateway connection failed: ${err.message}`));
|
|
1182
|
+
};
|
|
1183
|
+
this.socket.once("connect_error", onConnectError);
|
|
1184
|
+
this.socket.once("bot:ready", (data) => {
|
|
1185
|
+
this.socket.off("connect_error", onConnectError);
|
|
1186
|
+
this.botUser = {
|
|
1187
|
+
...data.bot,
|
|
1188
|
+
scopes: data.scopes,
|
|
1189
|
+
// Flatten botUser fields for convenience so bot.username works
|
|
1190
|
+
username: data.bot.botUser?.username ?? data.bot.name,
|
|
1191
|
+
displayName: data.bot.botUser?.displayName ?? data.bot.name
|
|
1192
|
+
};
|
|
1193
|
+
this.emit("ready", this.botUser);
|
|
1194
|
+
resolve();
|
|
1195
|
+
});
|
|
1196
|
+
this.socket.on("interaction:created", (raw) => {
|
|
1197
|
+
const interaction = new NovaInteraction(raw, this.interactions);
|
|
1198
|
+
this.emit("interactionCreate", interaction);
|
|
1199
|
+
const run = (h) => {
|
|
1200
|
+
if (h) Promise.resolve(h(interaction)).catch((e) => this.emit("error", e));
|
|
1201
|
+
};
|
|
1202
|
+
if (interaction.isCommand() && interaction.commandName) {
|
|
1203
|
+
run(this._commandHandlers.get(interaction.commandName));
|
|
1204
|
+
} else if (interaction.isButton() && interaction.customId) {
|
|
1205
|
+
run(this._buttonHandlers.get(interaction.customId));
|
|
1206
|
+
} else if (interaction.isSelectMenu() && interaction.customId) {
|
|
1207
|
+
run(this._selectHandlers.get(interaction.customId));
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
this.socket.on("bot:event", (event) => {
|
|
1211
|
+
this.emit("event", event);
|
|
1212
|
+
switch (event.type) {
|
|
1213
|
+
case "message.created":
|
|
1214
|
+
case "message.edited": {
|
|
1215
|
+
const raw = event.data;
|
|
1216
|
+
const wrapped = new NovaMessage(raw, this.messages, this.reactions);
|
|
1217
|
+
if (event.type === "message.created") this.emit("messageCreate", wrapped);
|
|
1218
|
+
else this.emit("messageUpdate", wrapped);
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
1221
|
+
case "message.deleted":
|
|
1222
|
+
this.emit("messageDelete", event.data);
|
|
1223
|
+
break;
|
|
1224
|
+
case "message.reaction_added":
|
|
1225
|
+
this.emit("reactionAdd", event.data);
|
|
1226
|
+
break;
|
|
1227
|
+
case "message.reaction_removed":
|
|
1228
|
+
this.emit("reactionRemove", event.data);
|
|
1229
|
+
break;
|
|
1230
|
+
case "user.joined_server":
|
|
1231
|
+
this.emit("memberAdd", event.data);
|
|
1232
|
+
break;
|
|
1233
|
+
case "user.left_server":
|
|
1234
|
+
this.emit("memberRemove", event.data);
|
|
1235
|
+
break;
|
|
1236
|
+
case "user.started_typing":
|
|
1237
|
+
this.emit("typingStart", event.data);
|
|
1238
|
+
break;
|
|
1239
|
+
case "message.pinned":
|
|
1240
|
+
this.emit("messagePinned", event.data);
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
this.socket.on("bot:error", (err) => {
|
|
1245
|
+
this.emit("error", err);
|
|
1246
|
+
});
|
|
1247
|
+
this.socket.on("disconnect", (reason) => {
|
|
1248
|
+
this.emit("disconnect", reason);
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Register a recurring task that fires at a set interval.
|
|
1254
|
+
* All cron tasks are automatically cancelled on `disconnect()`.
|
|
1255
|
+
*
|
|
1256
|
+
* @param intervalMs - How often to run the task (in milliseconds).
|
|
1257
|
+
* @param fn - Async or sync function to call on each tick.
|
|
1258
|
+
* @returns A cancel function — call it to stop this specific task.
|
|
1259
|
+
*
|
|
1260
|
+
* @example
|
|
1261
|
+
* // Check for new announcements every 30 seconds
|
|
1262
|
+
* client.cron(30_000, async () => {
|
|
1263
|
+
* const messages = await client.messages.fetch(channelId, { limit: 5 })
|
|
1264
|
+
* // do something...
|
|
1265
|
+
* })
|
|
1266
|
+
*/
|
|
1267
|
+
cron(intervalMs, fn) {
|
|
1268
|
+
const id = setInterval(() => {
|
|
1269
|
+
try {
|
|
1270
|
+
const result = fn();
|
|
1271
|
+
if (result && typeof result.catch === "function") {
|
|
1272
|
+
;
|
|
1273
|
+
result.catch((e) => this.emit("error", e));
|
|
1274
|
+
}
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
this.emit("error", e);
|
|
1277
|
+
}
|
|
1278
|
+
}, intervalMs);
|
|
1279
|
+
this._cronTimers.push(id);
|
|
1280
|
+
return () => {
|
|
1281
|
+
clearInterval(id);
|
|
1282
|
+
this._cronTimers = this._cronTimers.filter((t) => t !== id);
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Set the bot's presence status.
|
|
1287
|
+
* Broadcasts to all servers the bot is in via WebSocket.
|
|
1288
|
+
*
|
|
1289
|
+
* @example
|
|
1290
|
+
* client.setStatus('DND') // Do Not Disturb
|
|
1291
|
+
* client.setStatus('IDLE') // Away
|
|
1292
|
+
* client.setStatus('OFFLINE') // Appear offline
|
|
1293
|
+
* client.setStatus('ONLINE') // Back online
|
|
1294
|
+
*/
|
|
1295
|
+
setStatus(status) {
|
|
1296
|
+
this.socket?.emit("bot:status", { status });
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Disconnect from the gateway and clean up.
|
|
1300
|
+
*/
|
|
1301
|
+
disconnect() {
|
|
1302
|
+
for (const id of this._cronTimers) clearInterval(id);
|
|
1303
|
+
this._cronTimers = [];
|
|
1304
|
+
if (this.socket) {
|
|
1305
|
+
const sock = this.socket;
|
|
1306
|
+
this.socket = null;
|
|
1307
|
+
try {
|
|
1308
|
+
sock.io.reconnection(false);
|
|
1309
|
+
} catch {
|
|
1310
|
+
}
|
|
1311
|
+
try {
|
|
1312
|
+
sock.io?.engine?.socket?.terminate?.();
|
|
1313
|
+
} catch {
|
|
1314
|
+
}
|
|
1315
|
+
try {
|
|
1316
|
+
sock.io?.engine?.socket?.destroy?.();
|
|
1317
|
+
} catch {
|
|
1318
|
+
}
|
|
1319
|
+
try {
|
|
1320
|
+
sock.io?.engine?.close?.();
|
|
1321
|
+
} catch {
|
|
1322
|
+
}
|
|
1323
|
+
try {
|
|
1324
|
+
sock.disconnect();
|
|
1325
|
+
} catch {
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Send a message via the WebSocket gateway (lower latency than HTTP).
|
|
1331
|
+
* Requires the `messages.write` scope.
|
|
1332
|
+
*
|
|
1333
|
+
* @example
|
|
1334
|
+
* client.wsSend('channel-id', 'Hello from the gateway!')
|
|
1335
|
+
*/
|
|
1336
|
+
wsSend(channelId, content) {
|
|
1337
|
+
if (!this.socket?.connected) {
|
|
1338
|
+
throw new Error("[nova-bot-sdk] Not connected. Call client.connect() first.");
|
|
1339
|
+
}
|
|
1340
|
+
this.socket.emit("bot:message:send", { channelId, content });
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Start a typing indicator via WebSocket.
|
|
1344
|
+
*/
|
|
1345
|
+
wsTypingStart(channelId) {
|
|
1346
|
+
this.socket?.emit("bot:typing:start", { channelId });
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Stop a typing indicator via WebSocket.
|
|
1350
|
+
*/
|
|
1351
|
+
wsTypingStop(channelId) {
|
|
1352
|
+
this.socket?.emit("bot:typing:stop", { channelId });
|
|
1353
|
+
}
|
|
1354
|
+
on(event, listener) {
|
|
1355
|
+
return super.on(event, listener);
|
|
1356
|
+
}
|
|
1357
|
+
once(event, listener) {
|
|
1358
|
+
return super.once(event, listener);
|
|
1359
|
+
}
|
|
1360
|
+
off(event, listener) {
|
|
1361
|
+
return super.off(event, listener);
|
|
1362
|
+
}
|
|
1363
|
+
emit(event, ...args) {
|
|
1364
|
+
return super.emit(event, ...args);
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
// src/builders/EmbedBuilder.ts
|
|
1369
|
+
var EmbedBuilder = class {
|
|
1370
|
+
constructor() {
|
|
1371
|
+
this._data = {};
|
|
1372
|
+
}
|
|
1373
|
+
/** Set the embed title (shown in bold at the top). */
|
|
1374
|
+
setTitle(title) {
|
|
1375
|
+
this._data.title = title;
|
|
1376
|
+
return this;
|
|
1377
|
+
}
|
|
1378
|
+
/** Set the main description text (supports markdown). */
|
|
1379
|
+
setDescription(description) {
|
|
1380
|
+
this._data.description = description;
|
|
1381
|
+
return this;
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Set the accent colour.
|
|
1385
|
+
* @param color Hex string — e.g. `'#5865F2'` or `'5865F2'`
|
|
1386
|
+
*/
|
|
1387
|
+
setColor(color) {
|
|
1388
|
+
this._data.color = color.startsWith("#") ? color : `#${color}`;
|
|
1389
|
+
return this;
|
|
1390
|
+
}
|
|
1391
|
+
/** Make the title a clickable hyperlink. */
|
|
1392
|
+
setUrl(url) {
|
|
1393
|
+
this._data.url = url;
|
|
1394
|
+
return this;
|
|
1395
|
+
}
|
|
1396
|
+
/** Small image shown in the top-right corner. */
|
|
1397
|
+
setThumbnail(url) {
|
|
1398
|
+
this._data.thumbnail = url;
|
|
1399
|
+
return this;
|
|
1400
|
+
}
|
|
1401
|
+
/** Large image shown below the fields. */
|
|
1402
|
+
setImage(url) {
|
|
1403
|
+
this._data.image = url;
|
|
1404
|
+
return this;
|
|
1405
|
+
}
|
|
1406
|
+
/** Footer text shown at the very bottom of the embed. */
|
|
1407
|
+
setFooter(text) {
|
|
1408
|
+
this._data.footer = text;
|
|
1409
|
+
return this;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Add a timestamp to the footer line.
|
|
1413
|
+
* @param date Defaults to `new Date()`.
|
|
1414
|
+
*/
|
|
1415
|
+
setTimestamp(date) {
|
|
1416
|
+
const d = date instanceof Date ? date : date != null ? new Date(date) : /* @__PURE__ */ new Date();
|
|
1417
|
+
this._data.timestamp = d.toISOString();
|
|
1418
|
+
return this;
|
|
1419
|
+
}
|
|
1420
|
+
/** Author name + optional icon shown above the title. */
|
|
1421
|
+
setAuthor(author) {
|
|
1422
|
+
this._data.author = author;
|
|
1423
|
+
return this;
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Add a single field.
|
|
1427
|
+
* @param inline Pass `true` to render this field side-by-side with adjacent inline fields.
|
|
1428
|
+
*/
|
|
1429
|
+
addField(name, value, inline) {
|
|
1430
|
+
if (!this._data.fields) this._data.fields = [];
|
|
1431
|
+
this._data.fields.push({ name, value, inline });
|
|
1432
|
+
return this;
|
|
1433
|
+
}
|
|
1434
|
+
/** Add multiple fields at once. */
|
|
1435
|
+
addFields(...fields) {
|
|
1436
|
+
if (!this._data.fields) this._data.fields = [];
|
|
1437
|
+
this._data.fields.push(...fields);
|
|
1438
|
+
return this;
|
|
1439
|
+
}
|
|
1440
|
+
/** Replace all fields. */
|
|
1441
|
+
setFields(fields) {
|
|
1442
|
+
this._data.fields = [...fields];
|
|
1443
|
+
return this;
|
|
1444
|
+
}
|
|
1445
|
+
/** Serialise to a plain `Embed` object you can pass to any API call. */
|
|
1446
|
+
toJSON() {
|
|
1447
|
+
return {
|
|
1448
|
+
...this._data,
|
|
1449
|
+
fields: this._data.fields ? [...this._data.fields] : void 0
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
// src/builders/ButtonBuilder.ts
|
|
1455
|
+
var ButtonBuilder = class {
|
|
1456
|
+
constructor() {
|
|
1457
|
+
this._customId = "";
|
|
1458
|
+
this._label = "";
|
|
1459
|
+
this._style = "primary";
|
|
1460
|
+
this._disabled = false;
|
|
1461
|
+
}
|
|
1462
|
+
/** Custom ID returned in the `BUTTON_CLICK` interaction. Not required for `link` style buttons. */
|
|
1463
|
+
setCustomId(customId) {
|
|
1464
|
+
this._customId = customId;
|
|
1465
|
+
return this;
|
|
1466
|
+
}
|
|
1467
|
+
/** Text displayed on the button. */
|
|
1468
|
+
setLabel(label) {
|
|
1469
|
+
this._label = label;
|
|
1470
|
+
return this;
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Visual style:
|
|
1474
|
+
* - `primary` — blurple / brand colour
|
|
1475
|
+
* - `secondary` — grey
|
|
1476
|
+
* - `success` — green
|
|
1477
|
+
* - `danger` — red
|
|
1478
|
+
* - `link` — grey, navigates to a URL instead of creating an interaction
|
|
1479
|
+
*/
|
|
1480
|
+
setStyle(style) {
|
|
1481
|
+
this._style = style;
|
|
1482
|
+
return this;
|
|
1483
|
+
}
|
|
1484
|
+
/** Emoji shown to the left of the label (unicode or custom e.g. `'👋'`). */
|
|
1485
|
+
setEmoji(emoji) {
|
|
1486
|
+
this._emoji = emoji;
|
|
1487
|
+
return this;
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* URL for `link` style buttons.
|
|
1491
|
+
* Sets the style to `'link'` automatically.
|
|
1492
|
+
*/
|
|
1493
|
+
setUrl(url) {
|
|
1494
|
+
this._url = url;
|
|
1495
|
+
this._style = "link";
|
|
1496
|
+
return this;
|
|
1497
|
+
}
|
|
1498
|
+
/** Prevent users from clicking the button. */
|
|
1499
|
+
setDisabled(disabled = true) {
|
|
1500
|
+
this._disabled = disabled;
|
|
1501
|
+
return this;
|
|
1502
|
+
}
|
|
1503
|
+
toJSON() {
|
|
1504
|
+
return {
|
|
1505
|
+
type: "button",
|
|
1506
|
+
customId: this._customId,
|
|
1507
|
+
label: this._label || void 0,
|
|
1508
|
+
style: this._style,
|
|
1509
|
+
emoji: this._emoji,
|
|
1510
|
+
url: this._url,
|
|
1511
|
+
disabled: this._disabled || void 0
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
// src/builders/SelectMenuBuilder.ts
|
|
1517
|
+
var SelectMenuBuilder = class {
|
|
1518
|
+
constructor() {
|
|
1519
|
+
this._customId = "";
|
|
1520
|
+
this._disabled = false;
|
|
1521
|
+
this._options = [];
|
|
1522
|
+
}
|
|
1523
|
+
/** Custom ID returned in the `SELECT_MENU` interaction. */
|
|
1524
|
+
setCustomId(customId) {
|
|
1525
|
+
this._customId = customId;
|
|
1526
|
+
return this;
|
|
1527
|
+
}
|
|
1528
|
+
/** Greyed-out hint shown when nothing is selected. */
|
|
1529
|
+
setPlaceholder(placeholder) {
|
|
1530
|
+
this._placeholder = placeholder;
|
|
1531
|
+
return this;
|
|
1532
|
+
}
|
|
1533
|
+
/** Prevent users from interacting with the menu. */
|
|
1534
|
+
setDisabled(disabled = true) {
|
|
1535
|
+
this._disabled = disabled;
|
|
1536
|
+
return this;
|
|
1537
|
+
}
|
|
1538
|
+
/** Add a single option. */
|
|
1539
|
+
addOption(option) {
|
|
1540
|
+
this._options.push(option);
|
|
1541
|
+
return this;
|
|
1542
|
+
}
|
|
1543
|
+
/** Add multiple options at once. */
|
|
1544
|
+
addOptions(...options) {
|
|
1545
|
+
this._options.push(...options);
|
|
1546
|
+
return this;
|
|
1547
|
+
}
|
|
1548
|
+
/** Replace all options. */
|
|
1549
|
+
setOptions(options) {
|
|
1550
|
+
this._options.splice(0, this._options.length, ...options);
|
|
1551
|
+
return this;
|
|
1552
|
+
}
|
|
1553
|
+
toJSON() {
|
|
1554
|
+
return {
|
|
1555
|
+
type: "select",
|
|
1556
|
+
customId: this._customId,
|
|
1557
|
+
placeholder: this._placeholder,
|
|
1558
|
+
disabled: this._disabled || void 0,
|
|
1559
|
+
options: [...this._options]
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1564
|
+
// src/builders/ActionRowBuilder.ts
|
|
1565
|
+
var ActionRowBuilder = class {
|
|
1566
|
+
constructor() {
|
|
1567
|
+
this._components = [];
|
|
1568
|
+
}
|
|
1569
|
+
/** Add a single component (button or select menu). */
|
|
1570
|
+
addComponent(component) {
|
|
1571
|
+
this._components.push(component);
|
|
1572
|
+
return this;
|
|
1573
|
+
}
|
|
1574
|
+
/** Add multiple components at once. */
|
|
1575
|
+
addComponents(...components) {
|
|
1576
|
+
this._components.push(...components);
|
|
1577
|
+
return this;
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Serialise to a flat `MessageComponent[]` array — the format accepted by all API calls.
|
|
1581
|
+
*/
|
|
1582
|
+
toJSON() {
|
|
1583
|
+
return this._components.map((c) => c.toJSON());
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
|
|
1587
|
+
// src/builders/ModalBuilder.ts
|
|
1588
|
+
var ModalBuilder = class {
|
|
1589
|
+
constructor() {
|
|
1590
|
+
this._title = "";
|
|
1591
|
+
this._customId = "";
|
|
1592
|
+
this._fields = [];
|
|
1593
|
+
}
|
|
1594
|
+
/** Title shown at the top of the modal dialog. */
|
|
1595
|
+
setTitle(title) {
|
|
1596
|
+
this._title = title;
|
|
1597
|
+
return this;
|
|
1598
|
+
}
|
|
1599
|
+
/** Custom ID passed back with the `MODAL_SUBMIT` interaction. */
|
|
1600
|
+
setCustomId(customId) {
|
|
1601
|
+
this._customId = customId;
|
|
1602
|
+
return this;
|
|
1603
|
+
}
|
|
1604
|
+
/** Add a single text-input field. */
|
|
1605
|
+
addField(field) {
|
|
1606
|
+
this._fields.push("toJSON" in field ? field.toJSON() : field);
|
|
1607
|
+
return this;
|
|
1608
|
+
}
|
|
1609
|
+
/** Add multiple fields at once. */
|
|
1610
|
+
addFields(...fields) {
|
|
1611
|
+
for (const f of fields) this.addField(f);
|
|
1612
|
+
return this;
|
|
1613
|
+
}
|
|
1614
|
+
toJSON() {
|
|
1615
|
+
if (!this._title) throw new Error("ModalBuilder: title is required \u2014 call .setTitle()");
|
|
1616
|
+
if (!this._customId) throw new Error("ModalBuilder: customId is required \u2014 call .setCustomId()");
|
|
1617
|
+
if (this._fields.length === 0) throw new Error("ModalBuilder: at least one field is required \u2014 call .addField()");
|
|
1618
|
+
return { title: this._title, customId: this._customId, fields: [...this._fields] };
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
// src/builders/TextInputBuilder.ts
|
|
1623
|
+
var TextInputBuilder = class {
|
|
1624
|
+
constructor() {
|
|
1625
|
+
this._data = {
|
|
1626
|
+
customId: "",
|
|
1627
|
+
label: "",
|
|
1628
|
+
type: "short"
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
/** Unique ID for this field — the key in `interaction.modalData` on submit. */
|
|
1632
|
+
setCustomId(customId) {
|
|
1633
|
+
this._data.customId = customId;
|
|
1634
|
+
return this;
|
|
1635
|
+
}
|
|
1636
|
+
/** Label shown above the input inside the modal. */
|
|
1637
|
+
setLabel(label) {
|
|
1638
|
+
this._data.label = label;
|
|
1639
|
+
return this;
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Input style:
|
|
1643
|
+
* - `'short'` — single-line text input
|
|
1644
|
+
* - `'paragraph'` — multi-line textarea
|
|
1645
|
+
*/
|
|
1646
|
+
setStyle(style) {
|
|
1647
|
+
this._data.type = style;
|
|
1648
|
+
return this;
|
|
1649
|
+
}
|
|
1650
|
+
/** Greyed-out hint text shown when the field is empty. */
|
|
1651
|
+
setPlaceholder(placeholder) {
|
|
1652
|
+
this._data.placeholder = placeholder;
|
|
1653
|
+
return this;
|
|
1654
|
+
}
|
|
1655
|
+
/** Whether the user must fill in this field before submitting. */
|
|
1656
|
+
setRequired(required = true) {
|
|
1657
|
+
this._data.required = required;
|
|
1658
|
+
return this;
|
|
1659
|
+
}
|
|
1660
|
+
/** Minimum number of characters required. */
|
|
1661
|
+
setMinLength(min) {
|
|
1662
|
+
this._data.minLength = min;
|
|
1663
|
+
return this;
|
|
1664
|
+
}
|
|
1665
|
+
/** Maximum number of characters allowed. */
|
|
1666
|
+
setMaxLength(max) {
|
|
1667
|
+
this._data.maxLength = max;
|
|
1668
|
+
return this;
|
|
1669
|
+
}
|
|
1670
|
+
/** Pre-filled default value. */
|
|
1671
|
+
setValue(value) {
|
|
1672
|
+
this._data.value = value;
|
|
1673
|
+
return this;
|
|
1674
|
+
}
|
|
1675
|
+
toJSON() {
|
|
1676
|
+
return { ...this._data };
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
|
|
1680
|
+
// src/builders/SlashCommandBuilder.ts
|
|
1681
|
+
var SlashCommandOptionBuilder = class {
|
|
1682
|
+
constructor(type) {
|
|
1683
|
+
this._data = { name: "", description: "", type };
|
|
1684
|
+
}
|
|
1685
|
+
/** Internal option name (lowercase, no spaces). Shown after `/command ` in the client UI. */
|
|
1686
|
+
setName(name) {
|
|
1687
|
+
this._data.name = name;
|
|
1688
|
+
return this;
|
|
1689
|
+
}
|
|
1690
|
+
/** Short human-readable description shown in the command picker. */
|
|
1691
|
+
setDescription(description) {
|
|
1692
|
+
this._data.description = description;
|
|
1693
|
+
return this;
|
|
1694
|
+
}
|
|
1695
|
+
/** Whether users must supply this option before sending the command. */
|
|
1696
|
+
setRequired(required = true) {
|
|
1697
|
+
this._data.required = required;
|
|
1698
|
+
return this;
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Restrict the option to specific values.
|
|
1702
|
+
* The picker will show these as autocomplete suggestions.
|
|
1703
|
+
*/
|
|
1704
|
+
addChoice(name, value) {
|
|
1705
|
+
if (!this._data.choices) this._data.choices = [];
|
|
1706
|
+
this._data.choices.push({ name, value });
|
|
1707
|
+
return this;
|
|
1708
|
+
}
|
|
1709
|
+
/** Set all allowed choices at once. */
|
|
1710
|
+
setChoices(choices) {
|
|
1711
|
+
this._data.choices = [...choices];
|
|
1712
|
+
return this;
|
|
1713
|
+
}
|
|
1714
|
+
toJSON() {
|
|
1715
|
+
return { ...this._data };
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
var SlashCommandBuilder = class {
|
|
1719
|
+
constructor() {
|
|
1720
|
+
this._data = {
|
|
1721
|
+
name: "",
|
|
1722
|
+
description: "",
|
|
1723
|
+
options: []
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
/** Command name — lowercase, no spaces (e.g. `'ban'`, `'server-info'`). */
|
|
1727
|
+
setName(name) {
|
|
1728
|
+
this._data.name = name;
|
|
1729
|
+
return this;
|
|
1730
|
+
}
|
|
1731
|
+
/** Short description shown in the command picker UI. */
|
|
1732
|
+
setDescription(description) {
|
|
1733
|
+
this._data.description = description;
|
|
1734
|
+
return this;
|
|
1735
|
+
}
|
|
1736
|
+
/** Add a text (string) option. */
|
|
1737
|
+
addStringOption(fn) {
|
|
1738
|
+
this._data.options.push(fn(new SlashCommandOptionBuilder("STRING")).toJSON());
|
|
1739
|
+
return this;
|
|
1740
|
+
}
|
|
1741
|
+
/** Add an integer (whole number) option. */
|
|
1742
|
+
addIntegerOption(fn) {
|
|
1743
|
+
this._data.options.push(fn(new SlashCommandOptionBuilder("INTEGER")).toJSON());
|
|
1744
|
+
return this;
|
|
1745
|
+
}
|
|
1746
|
+
/** Add a boolean (true/false) option. */
|
|
1747
|
+
addBooleanOption(fn) {
|
|
1748
|
+
this._data.options.push(fn(new SlashCommandOptionBuilder("BOOLEAN")).toJSON());
|
|
1749
|
+
return this;
|
|
1750
|
+
}
|
|
1751
|
+
/** Add a user-mention option (returns a user ID string). */
|
|
1752
|
+
addUserOption(fn) {
|
|
1753
|
+
this._data.options.push(fn(new SlashCommandOptionBuilder("USER")).toJSON());
|
|
1754
|
+
return this;
|
|
1755
|
+
}
|
|
1756
|
+
/** Add a channel-mention option (returns a channel ID string). */
|
|
1757
|
+
addChannelOption(fn) {
|
|
1758
|
+
this._data.options.push(fn(new SlashCommandOptionBuilder("CHANNEL")).toJSON());
|
|
1759
|
+
return this;
|
|
1760
|
+
}
|
|
1761
|
+
/** Add a role-mention option (returns a role ID string). */
|
|
1762
|
+
addRoleOption(fn) {
|
|
1763
|
+
this._data.options.push(fn(new SlashCommandOptionBuilder("ROLE")).toJSON());
|
|
1764
|
+
return this;
|
|
1765
|
+
}
|
|
1766
|
+
toJSON() {
|
|
1767
|
+
if (!this._data.name) throw new Error("SlashCommandBuilder: name is required \u2014 call .setName()");
|
|
1768
|
+
if (!this._data.description) throw new Error("SlashCommandBuilder: description is required \u2014 call .setDescription()");
|
|
1769
|
+
return { ...this._data, options: [...this._data.options ?? []] };
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1773
|
+
// src/builders/PollBuilder.ts
|
|
1774
|
+
var PollBuilder = class {
|
|
1775
|
+
constructor() {
|
|
1776
|
+
this._question = "";
|
|
1777
|
+
this._options = [];
|
|
1778
|
+
this._allowMultiple = false;
|
|
1779
|
+
this._duration = null;
|
|
1780
|
+
this._anonymous = false;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Set the poll question.
|
|
1784
|
+
*
|
|
1785
|
+
* @example
|
|
1786
|
+
* builder.setQuestion('Best programming language?')
|
|
1787
|
+
*/
|
|
1788
|
+
setQuestion(question) {
|
|
1789
|
+
this._question = question;
|
|
1790
|
+
return this;
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Add a single answer option.
|
|
1794
|
+
*
|
|
1795
|
+
* @example
|
|
1796
|
+
* builder.addOption({ label: 'TypeScript', emoji: '🔷' })
|
|
1797
|
+
*/
|
|
1798
|
+
addOption(option) {
|
|
1799
|
+
if (this._options.length >= 10) throw new Error("Polls can have at most 10 options");
|
|
1800
|
+
this._options.push(option);
|
|
1801
|
+
return this;
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* Add multiple answer options at once.
|
|
1805
|
+
*
|
|
1806
|
+
* @example
|
|
1807
|
+
* builder.addOptions([
|
|
1808
|
+
* { label: 'Yes', emoji: '✅' },
|
|
1809
|
+
* { label: 'No', emoji: '❌' },
|
|
1810
|
+
* ])
|
|
1811
|
+
*/
|
|
1812
|
+
addOptions(options) {
|
|
1813
|
+
for (const o of options) this.addOption(o);
|
|
1814
|
+
return this;
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Allow users to select more than one option.
|
|
1818
|
+
* Default: `false` (single choice).
|
|
1819
|
+
*/
|
|
1820
|
+
setAllowMultiple(yes = true) {
|
|
1821
|
+
this._allowMultiple = yes;
|
|
1822
|
+
return this;
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Set the poll duration in **seconds**. Pass `null` to make it permanent.
|
|
1826
|
+
*
|
|
1827
|
+
* @example
|
|
1828
|
+
* builder.setDuration(60 * 60 * 24) // expires in 24 hours
|
|
1829
|
+
* builder.setDuration(null) // no expiry
|
|
1830
|
+
*/
|
|
1831
|
+
setDuration(seconds) {
|
|
1832
|
+
this._duration = seconds;
|
|
1833
|
+
return this;
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Hide which users voted for which option.
|
|
1837
|
+
* Default: `false` (votes are public).
|
|
1838
|
+
*/
|
|
1839
|
+
setAnonymous(yes = true) {
|
|
1840
|
+
this._anonymous = yes;
|
|
1841
|
+
return this;
|
|
1842
|
+
}
|
|
1843
|
+
/** Build the poll definition object. */
|
|
1844
|
+
toJSON() {
|
|
1845
|
+
if (!this._question.trim()) throw new Error("PollBuilder: question is required");
|
|
1846
|
+
if (this._options.length < 2) throw new Error("PollBuilder: at least 2 options are required");
|
|
1847
|
+
return {
|
|
1848
|
+
question: this._question,
|
|
1849
|
+
options: this._options,
|
|
1850
|
+
allowMultiple: this._allowMultiple,
|
|
1851
|
+
duration: this._duration,
|
|
1852
|
+
anonymous: this._anonymous
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
|
|
1857
|
+
// src/builders/MessageBuilder.ts
|
|
1858
|
+
var MessageBuilder = class {
|
|
1859
|
+
constructor() {
|
|
1860
|
+
this._components = [];
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Set the text content of the message.
|
|
1864
|
+
*
|
|
1865
|
+
* @example
|
|
1866
|
+
* builder.setContent('Hello world!')
|
|
1867
|
+
*/
|
|
1868
|
+
setContent(content) {
|
|
1869
|
+
this._content = content;
|
|
1870
|
+
return this;
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Attach an embed. Accepts an `EmbedBuilder` or raw `Embed` object.
|
|
1874
|
+
*
|
|
1875
|
+
* @example
|
|
1876
|
+
* builder.setEmbed(new EmbedBuilder().setTitle('Result'))
|
|
1877
|
+
* builder.setEmbed({ title: 'Result', color: '#57F287' })
|
|
1878
|
+
*/
|
|
1879
|
+
setEmbed(embed) {
|
|
1880
|
+
this._embed = "toJSON" in embed && typeof embed.toJSON === "function" ? embed.toJSON() : embed;
|
|
1881
|
+
return this;
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Remove any embed from this message.
|
|
1885
|
+
*/
|
|
1886
|
+
clearEmbed() {
|
|
1887
|
+
this._embed = void 0;
|
|
1888
|
+
return this;
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Add an `ActionRowBuilder` (or raw component array) as a component row.
|
|
1892
|
+
*
|
|
1893
|
+
* @example
|
|
1894
|
+
* builder.addRow(
|
|
1895
|
+
* new ActionRowBuilder().addComponent(btn1).addComponent(btn2)
|
|
1896
|
+
* )
|
|
1897
|
+
*/
|
|
1898
|
+
addRow(row) {
|
|
1899
|
+
const components = Array.isArray(row) ? row : row.toJSON();
|
|
1900
|
+
this._components.push(...components);
|
|
1901
|
+
return this;
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Replace all existing component rows.
|
|
1905
|
+
*/
|
|
1906
|
+
setComponents(components) {
|
|
1907
|
+
this._components = components;
|
|
1908
|
+
return this;
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Set the message this is replying to.
|
|
1912
|
+
*
|
|
1913
|
+
* @example
|
|
1914
|
+
* builder.setReplyTo(message.id)
|
|
1915
|
+
*/
|
|
1916
|
+
setReplyTo(messageId) {
|
|
1917
|
+
this._replyToId = messageId;
|
|
1918
|
+
return this;
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Attach a poll to the message.
|
|
1922
|
+
* Accepts a `PollBuilder` or raw `PollDefinition`.
|
|
1923
|
+
*
|
|
1924
|
+
* @example
|
|
1925
|
+
* builder.setPoll(
|
|
1926
|
+
* new PollBuilder()
|
|
1927
|
+
* .setQuestion('Favourite language?')
|
|
1928
|
+
* .addOptions([{ label: 'TypeScript' }, { label: 'Python' }])
|
|
1929
|
+
* )
|
|
1930
|
+
*/
|
|
1931
|
+
setPoll(poll) {
|
|
1932
|
+
this._poll = "toJSON" in poll && typeof poll.toJSON === "function" ? poll.toJSON() : poll;
|
|
1933
|
+
return this;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Build the message options object, ready to pass to `client.messages.send()`.
|
|
1937
|
+
*
|
|
1938
|
+
* @throws if neither `content` nor `embed` nor `poll` is set.
|
|
1939
|
+
*/
|
|
1940
|
+
toJSON() {
|
|
1941
|
+
if (!this._content && !this._embed && !this._poll) {
|
|
1942
|
+
throw new Error("MessageBuilder: message must have content, an embed, or a poll");
|
|
1943
|
+
}
|
|
1944
|
+
return {
|
|
1945
|
+
...this._content !== void 0 ? { content: this._content } : {},
|
|
1946
|
+
...this._embed !== void 0 ? { embed: this._embed } : {},
|
|
1947
|
+
...this._components.length > 0 ? { components: this._components } : {},
|
|
1948
|
+
...this._replyToId !== void 0 ? { replyToId: this._replyToId } : {},
|
|
1949
|
+
...this._poll !== void 0 ? { poll: this._poll } : {}
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
|
|
1954
|
+
// src/utils/Collection.ts
|
|
1955
|
+
var Collection = class _Collection extends Map {
|
|
1956
|
+
/**
|
|
1957
|
+
* The first value stored (insertion order), or `undefined` if empty.
|
|
1958
|
+
*/
|
|
1959
|
+
first() {
|
|
1960
|
+
return this.values().next().value;
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* The first `n` values stored (insertion order).
|
|
1964
|
+
*/
|
|
1965
|
+
firstN(n) {
|
|
1966
|
+
const result = [];
|
|
1967
|
+
for (const v of this.values()) {
|
|
1968
|
+
if (result.length >= n) break;
|
|
1969
|
+
result.push(v);
|
|
1970
|
+
}
|
|
1971
|
+
return result;
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* The last value stored, or `undefined` if empty.
|
|
1975
|
+
*/
|
|
1976
|
+
last() {
|
|
1977
|
+
let last;
|
|
1978
|
+
for (const v of this.values()) last = v;
|
|
1979
|
+
return last;
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* The last `n` values stored (insertion order).
|
|
1983
|
+
*/
|
|
1984
|
+
lastN(n) {
|
|
1985
|
+
return this.toArray().slice(-n);
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Returns a random value from the collection, or `undefined` if empty.
|
|
1989
|
+
*/
|
|
1990
|
+
random() {
|
|
1991
|
+
const arr = this.toArray();
|
|
1992
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Find the first value that passes the predicate.
|
|
1996
|
+
*
|
|
1997
|
+
* @example
|
|
1998
|
+
* const bot = members.find(m => m.user.isBot)
|
|
1999
|
+
*/
|
|
2000
|
+
find(fn) {
|
|
2001
|
+
for (const [k, v] of this) {
|
|
2002
|
+
if (fn(v, k, this)) return v;
|
|
2003
|
+
}
|
|
2004
|
+
return void 0;
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Find the key of the first value that passes the predicate.
|
|
2008
|
+
*/
|
|
2009
|
+
findKey(fn) {
|
|
2010
|
+
for (const [k, v] of this) {
|
|
2011
|
+
if (fn(v, k, this)) return k;
|
|
2012
|
+
}
|
|
2013
|
+
return void 0;
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Returns `true` if at least one value satisfies the predicate.
|
|
2017
|
+
*/
|
|
2018
|
+
some(fn) {
|
|
2019
|
+
for (const [k, v] of this) {
|
|
2020
|
+
if (fn(v, k, this)) return true;
|
|
2021
|
+
}
|
|
2022
|
+
return false;
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Returns `true` if every value satisfies the predicate.
|
|
2026
|
+
*/
|
|
2027
|
+
every(fn) {
|
|
2028
|
+
for (const [k, v] of this) {
|
|
2029
|
+
if (!fn(v, k, this)) return false;
|
|
2030
|
+
}
|
|
2031
|
+
return true;
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Filter to a new Collection containing only values that pass the predicate.
|
|
2035
|
+
*
|
|
2036
|
+
* @example
|
|
2037
|
+
* const admins = members.filter(m => m.role === 'ADMIN')
|
|
2038
|
+
*/
|
|
2039
|
+
filter(fn) {
|
|
2040
|
+
const result = new _Collection();
|
|
2041
|
+
for (const [k, v] of this) {
|
|
2042
|
+
if (fn(v, k, this)) result.set(k, v);
|
|
2043
|
+
}
|
|
2044
|
+
return result;
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Map each value to a new array.
|
|
2048
|
+
*
|
|
2049
|
+
* @example
|
|
2050
|
+
* const names = members.map(m => m.user.username)
|
|
2051
|
+
*/
|
|
2052
|
+
map(fn) {
|
|
2053
|
+
const result = [];
|
|
2054
|
+
for (const [k, v] of this) result.push(fn(v, k, this));
|
|
2055
|
+
return result;
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Map to a new Collection with transformed values.
|
|
2059
|
+
*
|
|
2060
|
+
* @example
|
|
2061
|
+
* const names = channels.mapValues(c => c.name.toUpperCase())
|
|
2062
|
+
*/
|
|
2063
|
+
mapValues(fn) {
|
|
2064
|
+
const result = new _Collection();
|
|
2065
|
+
for (const [k, v] of this) result.set(k, fn(v, k, this));
|
|
2066
|
+
return result;
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Reduce the collection to a single value.
|
|
2070
|
+
*
|
|
2071
|
+
* @example
|
|
2072
|
+
* const totalReactions = messages.reduce((sum, m) => sum + m.reactions.length, 0)
|
|
2073
|
+
*/
|
|
2074
|
+
reduce(fn, initial) {
|
|
2075
|
+
let acc = initial;
|
|
2076
|
+
for (const [k, v] of this) acc = fn(acc, v, k, this);
|
|
2077
|
+
return acc;
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Returns values as an array (insertion order).
|
|
2081
|
+
*/
|
|
2082
|
+
toArray() {
|
|
2083
|
+
return [...this.values()];
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* Returns keys as an array (insertion order).
|
|
2087
|
+
*/
|
|
2088
|
+
keyArray() {
|
|
2089
|
+
return [...this.keys()];
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Sort and return a new Collection.
|
|
2093
|
+
* Callback works like `Array.prototype.sort`.
|
|
2094
|
+
*
|
|
2095
|
+
* @example
|
|
2096
|
+
* const sorted = channels.sort((a, b) => a.position - b.position)
|
|
2097
|
+
*/
|
|
2098
|
+
sort(fn) {
|
|
2099
|
+
const entries = [...this.entries()].sort(
|
|
2100
|
+
([ak, av], [bk, bv]) => fn ? fn(av, bv, ak, bk) : 0
|
|
2101
|
+
);
|
|
2102
|
+
return new _Collection(entries);
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Split into two Collections based on a predicate.
|
|
2106
|
+
* Returns `[passing, failing]`.
|
|
2107
|
+
*
|
|
2108
|
+
* @example
|
|
2109
|
+
* const [bots, humans] = members.partition(m => m.user.isBot)
|
|
2110
|
+
*/
|
|
2111
|
+
partition(fn) {
|
|
2112
|
+
const pass = new _Collection();
|
|
2113
|
+
const fail = new _Collection();
|
|
2114
|
+
for (const [k, v] of this) {
|
|
2115
|
+
if (fn(v, k, this)) pass.set(k, v);
|
|
2116
|
+
else fail.set(k, v);
|
|
2117
|
+
}
|
|
2118
|
+
return [pass, fail];
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Merge this collection with one or more others.
|
|
2122
|
+
* Later collections overwrite duplicate keys.
|
|
2123
|
+
*/
|
|
2124
|
+
merge(...others) {
|
|
2125
|
+
const result = new _Collection(this);
|
|
2126
|
+
for (const other of others) {
|
|
2127
|
+
for (const [k, v] of other) result.set(k, v);
|
|
2128
|
+
}
|
|
2129
|
+
return result;
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Serialize to a plain `Record` (requires string keys).
|
|
2133
|
+
*/
|
|
2134
|
+
toJSON() {
|
|
2135
|
+
const obj = {};
|
|
2136
|
+
for (const [k, v] of this) obj[String(k)] = v;
|
|
2137
|
+
return obj;
|
|
2138
|
+
}
|
|
2139
|
+
toString() {
|
|
2140
|
+
return `Collection(${this.size})`;
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
|
|
2144
|
+
// src/utils/Cooldown.ts
|
|
2145
|
+
var Cooldown = class {
|
|
2146
|
+
/**
|
|
2147
|
+
* @param durationMs - Default cooldown duration in milliseconds.
|
|
2148
|
+
*/
|
|
2149
|
+
constructor(durationMs) {
|
|
2150
|
+
this._durations = /* @__PURE__ */ new Map();
|
|
2151
|
+
this._last = /* @__PURE__ */ new Map();
|
|
2152
|
+
this._defaultMs = durationMs;
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Check remaining cooldown for a user.
|
|
2156
|
+
* Returns `0` if they are not on cooldown (free to proceed),
|
|
2157
|
+
* or the **milliseconds remaining** if they are on cooldown.
|
|
2158
|
+
*
|
|
2159
|
+
* @example
|
|
2160
|
+
* const ms = cooldown.check(userId)
|
|
2161
|
+
* if (ms > 0) return interaction.replyEphemeral(`Wait ${Cooldown.format(ms)}!`)
|
|
2162
|
+
*/
|
|
2163
|
+
check(userId) {
|
|
2164
|
+
const last = this._last.get(userId);
|
|
2165
|
+
if (last === void 0) return 0;
|
|
2166
|
+
const duration = this._durations.get(userId) ?? this._defaultMs;
|
|
2167
|
+
const remaining = duration - (Date.now() - last);
|
|
2168
|
+
return remaining > 0 ? remaining : 0;
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Mark a user as having just used the command, starting their cooldown.
|
|
2172
|
+
* Optionally override the cooldown duration for this specific user.
|
|
2173
|
+
*
|
|
2174
|
+
* @example
|
|
2175
|
+
* cooldown.use(userId) // uses default duration
|
|
2176
|
+
* cooldown.use(userId, 10_000) // 10 second cooldown for this use
|
|
2177
|
+
*/
|
|
2178
|
+
use(userId, durationMs) {
|
|
2179
|
+
this._last.set(userId, Date.now());
|
|
2180
|
+
if (durationMs !== void 0) this._durations.set(userId, durationMs);
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Immediately reset (clear) the cooldown for a user.
|
|
2184
|
+
*
|
|
2185
|
+
* @example
|
|
2186
|
+
* cooldown.reset(userId) // user can use the command again immediately
|
|
2187
|
+
*/
|
|
2188
|
+
reset(userId) {
|
|
2189
|
+
this._last.delete(userId);
|
|
2190
|
+
this._durations.delete(userId);
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Reset all active cooldowns.
|
|
2194
|
+
*/
|
|
2195
|
+
resetAll() {
|
|
2196
|
+
this._last.clear();
|
|
2197
|
+
this._durations.clear();
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Returns a list of all users currently on cooldown.
|
|
2201
|
+
*
|
|
2202
|
+
* @example
|
|
2203
|
+
* const active = cooldown.activeCooldowns()
|
|
2204
|
+
* // [{ userId: 'abc', remainingMs: 3200 }, ...]
|
|
2205
|
+
*/
|
|
2206
|
+
activeCooldowns() {
|
|
2207
|
+
const result = [];
|
|
2208
|
+
for (const [userId] of this._last) {
|
|
2209
|
+
const ms = this.check(userId);
|
|
2210
|
+
if (ms > 0) result.push({ userId, remainingMs: ms });
|
|
2211
|
+
}
|
|
2212
|
+
return result;
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Format milliseconds into a human-readable string.
|
|
2216
|
+
*
|
|
2217
|
+
* @example
|
|
2218
|
+
* Cooldown.format(3_600_000) // '1h 0m 0s'
|
|
2219
|
+
* Cooldown.format(90_000) // '1m 30s'
|
|
2220
|
+
* Cooldown.format(4_000) // '4s'
|
|
2221
|
+
*/
|
|
2222
|
+
static format(ms) {
|
|
2223
|
+
const totalSecs = Math.ceil(ms / 1e3);
|
|
2224
|
+
const hours = Math.floor(totalSecs / 3600);
|
|
2225
|
+
const mins = Math.floor(totalSecs % 3600 / 60);
|
|
2226
|
+
const secs = totalSecs % 60;
|
|
2227
|
+
if (hours > 0) return `${hours}h ${mins}m ${secs}s`;
|
|
2228
|
+
if (mins > 0) return `${mins}m ${secs}s`;
|
|
2229
|
+
return `${secs}s`;
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
var CooldownManager = class {
|
|
2233
|
+
constructor() {
|
|
2234
|
+
this._map = /* @__PURE__ */ new Map();
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* Get or create a named cooldown.
|
|
2238
|
+
*/
|
|
2239
|
+
get(name, durationMs = 3e3) {
|
|
2240
|
+
if (!this._map.has(name)) this._map.set(name, new Cooldown(durationMs));
|
|
2241
|
+
return this._map.get(name);
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Check a named cooldown for a user.
|
|
2245
|
+
* Creates the cooldown if it doesn't exist yet.
|
|
2246
|
+
* Returns `0` if free, or remaining ms if on cooldown.
|
|
2247
|
+
*
|
|
2248
|
+
* @example
|
|
2249
|
+
* const ms = cooldowns.check('ban', userId, 30_000)
|
|
2250
|
+
*/
|
|
2251
|
+
check(name, userId, durationMs = 3e3) {
|
|
2252
|
+
return this.get(name, durationMs).check(userId);
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Mark a user as having used a named command.
|
|
2256
|
+
*
|
|
2257
|
+
* @example
|
|
2258
|
+
* cooldowns.use('ban', userId, 30_000)
|
|
2259
|
+
*/
|
|
2260
|
+
use(name, userId, durationMs) {
|
|
2261
|
+
this.get(name, durationMs).use(userId, durationMs);
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Reset a user's cooldown on a named command.
|
|
2265
|
+
*/
|
|
2266
|
+
reset(name, userId) {
|
|
2267
|
+
this._map.get(name)?.reset(userId);
|
|
2268
|
+
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Reset all cooldowns for all commands.
|
|
2271
|
+
*/
|
|
2272
|
+
resetAll() {
|
|
2273
|
+
for (const c of this._map.values()) c.resetAll();
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
|
|
2277
|
+
// src/utils/Logger.ts
|
|
2278
|
+
var Logger = class {
|
|
2279
|
+
/**
|
|
2280
|
+
* @param name - Name shown in square brackets before every message.
|
|
2281
|
+
* @param enableDebug - When `false` (default), `.debug()` calls are silent.
|
|
2282
|
+
*/
|
|
2283
|
+
constructor(name, enableDebug = false) {
|
|
2284
|
+
this._prefix = name;
|
|
2285
|
+
this._debug = enableDebug;
|
|
2286
|
+
}
|
|
2287
|
+
/** Enable or disable debug output at runtime. */
|
|
2288
|
+
setDebug(enabled) {
|
|
2289
|
+
this._debug = enabled;
|
|
2290
|
+
return this;
|
|
2291
|
+
}
|
|
2292
|
+
_timestamp() {
|
|
2293
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
2294
|
+
}
|
|
2295
|
+
_fmt(level, color, ...args) {
|
|
2296
|
+
const ts = this._timestamp();
|
|
2297
|
+
const tag = `\x1B[90m${ts}\x1B[0m ${color}[${level}]\x1B[0m \x1B[36m[${this._prefix}]\x1B[0m`;
|
|
2298
|
+
return `${tag} ${args.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)).join(" ")}`;
|
|
2299
|
+
}
|
|
2300
|
+
/** General info message. */
|
|
2301
|
+
info(...args) {
|
|
2302
|
+
console.log(this._fmt("INFO ", "\x1B[34m", ...args));
|
|
2303
|
+
}
|
|
2304
|
+
/** Warning — something unexpected but non-fatal. */
|
|
2305
|
+
warn(...args) {
|
|
2306
|
+
console.warn(this._fmt("WARN ", "\x1B[33m", ...args));
|
|
2307
|
+
}
|
|
2308
|
+
/** Error — something went wrong. */
|
|
2309
|
+
error(...args) {
|
|
2310
|
+
console.error(this._fmt("ERROR", "\x1B[31m", ...args));
|
|
2311
|
+
}
|
|
2312
|
+
/** Success / positive confirmation. */
|
|
2313
|
+
success(...args) {
|
|
2314
|
+
console.log(this._fmt("OK ", "\x1B[32m", ...args));
|
|
2315
|
+
}
|
|
2316
|
+
/** Debug — only printed when `enableDebug` is true. */
|
|
2317
|
+
debug(...args) {
|
|
2318
|
+
if (this._debug) console.log(this._fmt("DEBUG", "\x1B[35m", ...args));
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Log an incoming command interaction.
|
|
2322
|
+
*
|
|
2323
|
+
* @example
|
|
2324
|
+
* log.command('ping', interaction.userId)
|
|
2325
|
+
* // [MyBot] [CMD] /ping ← user:abc123
|
|
2326
|
+
*/
|
|
2327
|
+
command(name, userId) {
|
|
2328
|
+
console.log(this._fmt("CMD ", "\x1B[36m", `/${name} \x1B[90m\u2190 user:${userId}`));
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Log a gateway event.
|
|
2332
|
+
*
|
|
2333
|
+
* @example
|
|
2334
|
+
* log.event('messageCreate')
|
|
2335
|
+
*/
|
|
2336
|
+
event(type, extra = "") {
|
|
2337
|
+
console.log(this._fmt("EVT ", "\x1B[35m", type + (extra ? ` \x1B[90m${extra}` : "")));
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Log that a command handler threw an error.
|
|
2341
|
+
*
|
|
2342
|
+
* @example
|
|
2343
|
+
* log.commandError('ban', err)
|
|
2344
|
+
*/
|
|
2345
|
+
commandError(name, err) {
|
|
2346
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2347
|
+
console.error(this._fmt("ERROR", "\x1B[31m", `${name} threw: ${msg}`));
|
|
2348
|
+
if (err instanceof Error && err.stack) {
|
|
2349
|
+
console.error("\x1B[90m" + err.stack + "\x1B[0m");
|
|
2350
|
+
}
|
|
605
2351
|
}
|
|
606
2352
|
};
|
|
607
2353
|
// Annotate the CommonJS export names for ESM import in node:
|
|
608
2354
|
0 && (module.exports = {
|
|
2355
|
+
ActionRowBuilder,
|
|
2356
|
+
ButtonBuilder,
|
|
2357
|
+
ChannelsAPI,
|
|
2358
|
+
Collection,
|
|
609
2359
|
CommandsAPI,
|
|
2360
|
+
Cooldown,
|
|
2361
|
+
CooldownManager,
|
|
2362
|
+
EmbedBuilder,
|
|
610
2363
|
HttpClient,
|
|
2364
|
+
InteractionOptions,
|
|
611
2365
|
InteractionsAPI,
|
|
2366
|
+
Logger,
|
|
612
2367
|
MembersAPI,
|
|
2368
|
+
MessageBuilder,
|
|
613
2369
|
MessagesAPI,
|
|
2370
|
+
ModalBuilder,
|
|
614
2371
|
NovaClient,
|
|
2372
|
+
NovaInteraction,
|
|
2373
|
+
NovaMessage,
|
|
615
2374
|
PermissionsAPI,
|
|
616
|
-
|
|
2375
|
+
PollBuilder,
|
|
2376
|
+
ReactionsAPI,
|
|
2377
|
+
SelectMenuBuilder,
|
|
2378
|
+
ServersAPI,
|
|
2379
|
+
SlashCommandBuilder,
|
|
2380
|
+
SlashCommandOptionBuilder,
|
|
2381
|
+
TextInputBuilder
|
|
617
2382
|
});
|