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.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
- ServersAPI: () => ServersAPI
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/client.ts
439
- var EVENT_MAP = {
440
- "message.created": "messageCreate",
441
- "message.edited": "messageUpdate",
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
- * Connect to the Nova WebSocket gateway.
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.connect()
596
+ * const channels = await client.channels.list('server-id')
597
+ * const textChannels = channels.filter(c => c.type === 'TEXT')
491
598
  */
492
- connect() {
493
- return new Promise((resolve, reject) => {
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
- * Disconnect from the gateway and clean up.
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
- disconnect() {
544
- if (this.socket) {
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
- * Send a message via the WebSocket gateway (lower latency than HTTP).
571
- * Requires the `messages.write` scope.
613
+ * Create a new channel in a server.
614
+ * Requires the `channels.manage` scope.
572
615
  *
573
616
  * @example
574
- * client.wsSend('channel-id', 'Hello from the gateway!')
617
+ * const channel = await client.channels.create('server-id', {
618
+ * name: 'announcements',
619
+ * type: 'ANNOUNCEMENT',
620
+ * topic: 'Official announcements only',
621
+ * })
575
622
  */
576
- wsSend(channelId, content) {
577
- if (!this.socket?.connected) {
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
- * Start a typing indicator via WebSocket.
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
- wsTypingStart(channelId) {
586
- this.socket?.emit("bot:typing:start", { channelId });
633
+ edit(channelId, options) {
634
+ return this.http.patch(`/channels/${channelId}`, options);
587
635
  }
588
636
  /**
589
- * Stop a typing indicator via WebSocket.
637
+ * Delete a channel.
638
+ * Requires the `channels.manage` scope.
639
+ *
640
+ * @example
641
+ * await client.channels.delete('channel-id')
590
642
  */
591
- wsTypingStop(channelId) {
592
- this.socket?.emit("bot:typing:stop", { channelId });
643
+ delete(channelId) {
644
+ return this.http.delete(`/channels/${channelId}`);
593
645
  }
594
- on(event, listener) {
595
- return super.on(event, listener);
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
- once(event, listener) {
598
- return super.once(event, listener);
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
- off(event, listener) {
601
- return super.off(event, listener);
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
- emit(event, ...args) {
604
- return super.emit(event, ...args);
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
- ServersAPI
2375
+ PollBuilder,
2376
+ ReactionsAPI,
2377
+ SelectMenuBuilder,
2378
+ ServersAPI,
2379
+ SlashCommandBuilder,
2380
+ SlashCommandOptionBuilder,
2381
+ TextInputBuilder
617
2382
  });