novaapp-sdk 1.1.0 → 1.3.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
@@ -22,22 +22,42 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ActionRowBuilder: () => ActionRowBuilder,
24
24
  ButtonBuilder: () => ButtonBuilder,
25
+ ChannelsAPI: () => ChannelsAPI,
26
+ Collection: () => Collection,
25
27
  CommandsAPI: () => CommandsAPI,
28
+ Cooldown: () => Cooldown,
29
+ CooldownManager: () => CooldownManager,
26
30
  EmbedBuilder: () => EmbedBuilder,
27
31
  HttpClient: () => HttpClient,
28
32
  InteractionOptions: () => InteractionOptions,
29
33
  InteractionsAPI: () => InteractionsAPI,
34
+ Logger: () => Logger,
30
35
  MembersAPI: () => MembersAPI,
36
+ MessageBuilder: () => MessageBuilder,
31
37
  MessagesAPI: () => MessagesAPI,
32
38
  ModalBuilder: () => ModalBuilder,
39
+ NovaChannel: () => NovaChannel,
33
40
  NovaClient: () => NovaClient,
34
41
  NovaInteraction: () => NovaInteraction,
42
+ NovaMember: () => NovaMember,
43
+ NovaMessage: () => NovaMessage,
44
+ Paginator: () => Paginator,
45
+ Permissions: () => Permissions,
35
46
  PermissionsAPI: () => PermissionsAPI,
47
+ PermissionsBitfield: () => PermissionsBitfield,
48
+ PollBuilder: () => PollBuilder,
49
+ ReactionsAPI: () => ReactionsAPI,
36
50
  SelectMenuBuilder: () => SelectMenuBuilder,
37
51
  ServersAPI: () => ServersAPI,
38
52
  SlashCommandBuilder: () => SlashCommandBuilder,
39
53
  SlashCommandOptionBuilder: () => SlashCommandOptionBuilder,
40
- TextInputBuilder: () => TextInputBuilder
54
+ TextInputBuilder: () => TextInputBuilder,
55
+ countdown: () => countdown,
56
+ formatDuration: () => formatDuration,
57
+ formatRelative: () => formatRelative,
58
+ parseTimestamp: () => parseTimestamp,
59
+ sleep: () => sleep,
60
+ withTimeout: () => withTimeout
41
61
  });
42
62
  module.exports = __toCommonJS(index_exports);
43
63
 
@@ -135,6 +155,70 @@ var MessagesAPI = class {
135
155
  typing(channelId) {
136
156
  return this.http.post(`/channels/${channelId}/typing`);
137
157
  }
158
+ /**
159
+ * Fetch a single message by ID.
160
+ *
161
+ * @example
162
+ * const msg = await client.messages.fetchOne('message-id')
163
+ */
164
+ fetchOne(messageId) {
165
+ return this.http.get(`/messages/${messageId}`);
166
+ }
167
+ /**
168
+ * Pin a message in its channel.
169
+ * The bot must have the `messages.manage` scope.
170
+ *
171
+ * @example
172
+ * await client.messages.pin('message-id')
173
+ */
174
+ pin(messageId) {
175
+ return this.http.post(`/messages/${messageId}/pin`);
176
+ }
177
+ /**
178
+ * Unpin a message.
179
+ *
180
+ * @example
181
+ * await client.messages.unpin('message-id')
182
+ */
183
+ unpin(messageId) {
184
+ return this.http.delete(`/messages/${messageId}/pin`);
185
+ }
186
+ /**
187
+ * Fetch all pinned messages in a channel.
188
+ *
189
+ * @example
190
+ * const pins = await client.messages.fetchPinned('channel-id')
191
+ */
192
+ fetchPinned(channelId) {
193
+ return this.http.get(`/channels/${channelId}/pins`);
194
+ }
195
+ /**
196
+ * Add a reaction to a message.
197
+ *
198
+ * @example
199
+ * await client.messages.addReaction('message-id', '👍')
200
+ */
201
+ addReaction(messageId, emoji) {
202
+ return this.http.post(`/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
203
+ }
204
+ /**
205
+ * Remove the bot's reaction from a message.
206
+ *
207
+ * @example
208
+ * await client.messages.removeReaction('message-id', '👍')
209
+ */
210
+ removeReaction(messageId, emoji) {
211
+ return this.http.delete(`/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
212
+ }
213
+ /**
214
+ * Fetch all reactions on a message.
215
+ *
216
+ * @example
217
+ * const reactions = await client.messages.fetchReactions('message-id')
218
+ */
219
+ fetchReactions(messageId) {
220
+ return this.http.get(`/messages/${messageId}/reactions`);
221
+ }
138
222
  };
139
223
 
140
224
  // src/api/commands.ts
@@ -262,6 +346,61 @@ var MembersAPI = class {
262
346
  ban(serverId, userId, reason) {
263
347
  return this.http.post(`/servers/${serverId}/members/${userId}/ban`, { reason });
264
348
  }
349
+ /**
350
+ * Unban a previously banned user.
351
+ * Requires the `members.ban` scope.
352
+ *
353
+ * @example
354
+ * await client.members.unban('server-id', 'user-id')
355
+ */
356
+ unban(serverId, userId) {
357
+ return this.http.delete(`/servers/${serverId}/bans/${userId}`);
358
+ }
359
+ /**
360
+ * Fetch the ban list for a server.
361
+ * Requires the `members.ban` scope.
362
+ *
363
+ * @example
364
+ * const bans = await client.members.listBans('server-id')
365
+ * for (const ban of bans) {
366
+ * console.log(`${ban.username} — ${ban.reason ?? 'No reason'}`)
367
+ * }
368
+ */
369
+ listBans(serverId) {
370
+ return this.http.get(`/servers/${serverId}/bans`);
371
+ }
372
+ /**
373
+ * Send a direct message to a user (DM).
374
+ * Requires the `messages.write` scope.
375
+ *
376
+ * @example
377
+ * await client.members.dm('user-id', { content: 'Hello!' })
378
+ * await client.members.dm('user-id', 'Hello from the bot!')
379
+ */
380
+ dm(userId, options) {
381
+ const body = typeof options === "string" ? { content: options } : options;
382
+ return this.http.post(`/users/${userId}/dm`, body);
383
+ }
384
+ /**
385
+ * Add a role to a member.
386
+ * Requires the `members.roles` scope.
387
+ *
388
+ * @example
389
+ * await client.members.addRole('server-id', 'user-id', 'role-id')
390
+ */
391
+ addRole(serverId, userId, roleId) {
392
+ return this.http.post(`/servers/${serverId}/members/${userId}/roles/${roleId}`);
393
+ }
394
+ /**
395
+ * Remove a role from a member.
396
+ * Requires the `members.roles` scope.
397
+ *
398
+ * @example
399
+ * await client.members.removeRole('server-id', 'user-id', 'role-id')
400
+ */
401
+ removeRole(serverId, userId, roleId) {
402
+ return this.http.delete(`/servers/${serverId}/members/${userId}/roles/${roleId}`);
403
+ }
265
404
  };
266
405
 
267
406
  // src/api/servers.ts
@@ -279,6 +418,17 @@ var ServersAPI = class {
279
418
  list() {
280
419
  return this.http.get("/servers");
281
420
  }
421
+ /**
422
+ * Fetch all roles in a server.
423
+ * Results are sorted by position (lowest first).
424
+ *
425
+ * @example
426
+ * const roles = await client.servers.listRoles('server-id')
427
+ * const adminRole = roles.find(r => r.name === 'Admin')
428
+ */
429
+ listRoles(serverId) {
430
+ return this.http.get(`/servers/${serverId}/roles`);
431
+ }
282
432
  };
283
433
 
284
434
  // src/api/interactions.ts
@@ -445,6 +595,172 @@ var PermissionsAPI = class {
445
595
  }
446
596
  };
447
597
 
598
+ // src/api/channels.ts
599
+ var ChannelsAPI = class {
600
+ constructor(http) {
601
+ this.http = http;
602
+ }
603
+ /**
604
+ * Fetch all channels in a server the bot is a member of.
605
+ *
606
+ * @example
607
+ * const channels = await client.channels.list('server-id')
608
+ * const textChannels = channels.filter(c => c.type === 'TEXT')
609
+ */
610
+ list(serverId) {
611
+ return this.http.get(`/servers/${serverId}/channels`);
612
+ }
613
+ /**
614
+ * Fetch a single channel by ID.
615
+ *
616
+ * @example
617
+ * const channel = await client.channels.fetch('channel-id')
618
+ * console.log(channel.name, channel.type)
619
+ */
620
+ fetch(channelId) {
621
+ return this.http.get(`/channels/${channelId}`);
622
+ }
623
+ /**
624
+ * Create a new channel in a server.
625
+ * Requires the `channels.manage` scope.
626
+ *
627
+ * @example
628
+ * const channel = await client.channels.create('server-id', {
629
+ * name: 'announcements',
630
+ * type: 'ANNOUNCEMENT',
631
+ * topic: 'Official announcements only',
632
+ * })
633
+ */
634
+ create(serverId, options) {
635
+ return this.http.post(`/servers/${serverId}/channels`, options);
636
+ }
637
+ /**
638
+ * Edit an existing channel.
639
+ * Requires the `channels.manage` scope.
640
+ *
641
+ * @example
642
+ * await client.channels.edit('channel-id', { topic: 'New topic!' })
643
+ */
644
+ edit(channelId, options) {
645
+ return this.http.patch(`/channels/${channelId}`, options);
646
+ }
647
+ /**
648
+ * Delete a channel.
649
+ * Requires the `channels.manage` scope.
650
+ *
651
+ * @example
652
+ * await client.channels.delete('channel-id')
653
+ */
654
+ delete(channelId) {
655
+ return this.http.delete(`/channels/${channelId}`);
656
+ }
657
+ /**
658
+ * Fetch messages from a channel.
659
+ *
660
+ * @example
661
+ * const messages = await client.channels.fetchMessages('channel-id', { limit: 50 })
662
+ */
663
+ fetchMessages(channelId, options = {}) {
664
+ const params = new URLSearchParams();
665
+ if (options.limit) params.set("limit", String(options.limit));
666
+ if (options.before) params.set("before", options.before);
667
+ const qs = params.toString();
668
+ return this.http.get(`/channels/${channelId}/messages${qs ? `?${qs}` : ""}`);
669
+ }
670
+ /**
671
+ * Fetch all pinned messages in a channel.
672
+ *
673
+ * @example
674
+ * const pins = await client.channels.fetchPins('channel-id')
675
+ */
676
+ fetchPins(channelId) {
677
+ return this.http.get(`/channels/${channelId}/pins`);
678
+ }
679
+ /**
680
+ * Send a typing indicator in a channel.
681
+ * Displayed to users for ~5 seconds.
682
+ *
683
+ * @example
684
+ * await client.channels.startTyping('channel-id')
685
+ */
686
+ startTyping(channelId) {
687
+ return this.http.post(`/channels/${channelId}/typing`);
688
+ }
689
+ };
690
+
691
+ // src/api/reactions.ts
692
+ var ReactionsAPI = class {
693
+ constructor(http) {
694
+ this.http = http;
695
+ }
696
+ /**
697
+ * Add a reaction to a message.
698
+ * Use a plain emoji character or a custom emoji ID.
699
+ *
700
+ * @example
701
+ * await client.reactions.add('message-id', '👍')
702
+ * await client.reactions.add('message-id', '🎉')
703
+ */
704
+ add(messageId, emoji) {
705
+ const encoded = encodeURIComponent(emoji);
706
+ return this.http.post(`/messages/${messageId}/reactions/${encoded}`);
707
+ }
708
+ /**
709
+ * Remove the bot's reaction from a message.
710
+ *
711
+ * @example
712
+ * await client.reactions.remove('message-id', '👍')
713
+ */
714
+ remove(messageId, emoji) {
715
+ const encoded = encodeURIComponent(emoji);
716
+ return this.http.delete(`/messages/${messageId}/reactions/${encoded}`);
717
+ }
718
+ /**
719
+ * Remove all reactions from a message.
720
+ * Requires the `messages.manage` scope.
721
+ *
722
+ * @example
723
+ * await client.reactions.removeAll('message-id')
724
+ */
725
+ removeAll(messageId) {
726
+ return this.http.delete(`/messages/${messageId}/reactions`);
727
+ }
728
+ /**
729
+ * Remove all reactions of a specific emoji from a message.
730
+ * Requires the `messages.manage` scope.
731
+ *
732
+ * @example
733
+ * await client.reactions.removeEmoji('message-id', '👍')
734
+ */
735
+ removeEmoji(messageId, emoji) {
736
+ const encoded = encodeURIComponent(emoji);
737
+ return this.http.delete(`/messages/${messageId}/reactions/${encoded}/all`);
738
+ }
739
+ /**
740
+ * Fetch all reactions on a message, broken down by emoji.
741
+ *
742
+ * @example
743
+ * const reactions = await client.reactions.fetch('message-id')
744
+ * for (const r of reactions) {
745
+ * console.log(`${r.emoji} — ${r.count} reactions from ${r.users.map(u => u.username).join(', ')}`)
746
+ * }
747
+ */
748
+ fetch(messageId) {
749
+ return this.http.get(`/messages/${messageId}/reactions`);
750
+ }
751
+ /**
752
+ * Fetch reactions for a specific emoji on a message.
753
+ *
754
+ * @example
755
+ * const detail = await client.reactions.fetchEmoji('message-id', '👍')
756
+ * console.log(`${detail.count} thumbs ups`)
757
+ */
758
+ fetchEmoji(messageId, emoji) {
759
+ const encoded = encodeURIComponent(emoji);
760
+ return this.http.get(`/messages/${messageId}/reactions/${encoded}`);
761
+ }
762
+ };
763
+
448
764
  // src/structures/NovaInteraction.ts
449
765
  var InteractionOptions = class {
450
766
  constructor(data) {
@@ -624,651 +940,2143 @@ var NovaInteraction = class _NovaInteraction {
624
940
  }
625
941
  };
626
942
 
627
- // src/client.ts
628
- var EVENT_MAP = {
629
- "message.created": "messageCreate",
630
- "message.edited": "messageUpdate",
631
- "message.deleted": "messageDelete",
632
- "message.reaction_added": "reactionAdd",
633
- "message.reaction_removed": "reactionRemove",
634
- "user.joined_server": "memberAdd",
635
- "user.left_server": "memberRemove",
636
- "user.started_typing": "typingStart"
637
- };
638
- var NovaClient = class extends import_node_events.EventEmitter {
639
- constructor(options) {
640
- super();
641
- /** The authenticated bot application. Available after `ready` fires. */
642
- this.botUser = null;
643
- this.socket = null;
644
- // ── Command / component routing ───────────────────────────────────────────────────
645
- this._commandHandlers = /* @__PURE__ */ new Map();
646
- this._buttonHandlers = /* @__PURE__ */ new Map();
647
- this._selectHandlers = /* @__PURE__ */ new Map();
648
- if (!options.token) {
649
- throw new Error("[nova-bot-sdk] A bot token is required.");
650
- }
651
- if (!options.token.startsWith("nova_bot_")) {
652
- console.warn('[nova-bot-sdk] Warning: token does not start with "nova_bot_". Are you sure this is a bot token?');
653
- }
654
- this.options = {
655
- token: options.token,
656
- baseUrl: options.baseUrl ?? "https://novachatapp.com"
657
- };
658
- this.http = new HttpClient(this.options.baseUrl, this.options.token);
659
- this.messages = new MessagesAPI(this.http);
660
- this.commands = new CommandsAPI(this.http);
661
- this.members = new MembersAPI(this.http);
662
- this.servers = new ServersAPI(this.http);
663
- this.interactions = new InteractionsAPI(this.http, this);
664
- this.permissions = new PermissionsAPI(this.http);
665
- this.on("error", () => {
666
- });
667
- const cleanup = () => this.disconnect();
668
- process.once("beforeExit", cleanup);
669
- process.once("SIGINT", () => {
670
- cleanup();
671
- process.exit(0);
672
- });
673
- process.once("SIGTERM", () => {
674
- cleanup();
675
- process.exit(0);
676
- });
943
+ // src/structures/NovaMessage.ts
944
+ var NovaMessage = class _NovaMessage {
945
+ constructor(raw, messages, reactions) {
946
+ this.id = raw.id;
947
+ this.content = raw.content;
948
+ this.channelId = raw.channelId;
949
+ this.author = raw.author;
950
+ this.embed = raw.embed ?? null;
951
+ this.components = raw.components ?? [];
952
+ this.replyToId = raw.replyToId ?? null;
953
+ this.attachments = raw.attachments ?? [];
954
+ this.reactions = raw.reactions ?? [];
955
+ this.createdAt = new Date(raw.createdAt);
956
+ this.editedAt = raw.editedAt ? new Date(raw.editedAt) : null;
957
+ this._messages = messages;
958
+ this._reactions = reactions;
959
+ }
960
+ // ─── Type guards ─────────────────────────────────────────────────────────────
961
+ /** Returns true if the message was sent by a bot. */
962
+ isFromBot() {
963
+ return this.author.isBot === true;
964
+ }
965
+ /** Returns true if the message has an embed. */
966
+ hasEmbed() {
967
+ return this.embed !== null;
968
+ }
969
+ /** Returns true if the message has interactive components. */
970
+ hasComponents() {
971
+ return this.components.length > 0;
972
+ }
973
+ /** Returns true if the message has been edited. */
974
+ isEdited() {
975
+ return this.editedAt !== null;
976
+ }
977
+ // ─── Reactions ────────────────────────────────────────────────────────────────
978
+ /**
979
+ * Add a reaction to this message.
980
+ *
981
+ * @example
982
+ * await msg.react('👍')
983
+ * await msg.react('🎉')
984
+ */
985
+ react(emoji) {
986
+ return this._reactions.add(this.id, emoji);
677
987
  }
678
988
  /**
679
- * Register a handler for a slash or prefix command by name.
680
- * Automatically routes `interactionCreate` events whose `commandName` matches.
989
+ * Remove the bot's reaction from this message.
681
990
  *
682
991
  * @example
683
- * client.command('ping', async (interaction) => {
684
- * await interaction.reply('Pong! 🏓')
685
- * })
992
+ * await msg.removeReaction('👍')
686
993
  */
687
- command(name, handler) {
688
- this._commandHandlers.set(name, handler);
689
- return this;
994
+ removeReaction(emoji) {
995
+ return this._reactions.remove(this.id, emoji);
690
996
  }
997
+ // ─── Messaging ────────────────────────────────────────────────────────────────
691
998
  /**
692
- * Register a handler for a button by its `customId`.
999
+ * Reply to this message.
693
1000
  *
694
1001
  * @example
695
- * client.button('confirm_delete', async (interaction) => {
696
- * await interaction.replyEphemeral('Deleted.')
697
- * })
1002
+ * await msg.reply('Got it!')
1003
+ * await msg.reply({ embed: new EmbedBuilder().setTitle('Result').toJSON() })
698
1004
  */
699
- button(customId, handler) {
700
- this._buttonHandlers.set(customId, handler);
701
- return this;
1005
+ reply(options) {
1006
+ const opts = typeof options === "string" ? { content: options, replyToId: this.id } : { ...options, replyToId: this.id };
1007
+ return this._messages.send(this.channelId, opts).then((raw) => new _NovaMessage(raw, this._messages, this._reactions));
702
1008
  }
703
1009
  /**
704
- * Register a handler for a select menu by its `customId`.
1010
+ * Edit this message.
1011
+ * Only works if the bot is the author.
705
1012
  *
706
1013
  * @example
707
- * client.selectMenu('colour_pick', async (interaction) => {
708
- * const chosen = interaction.values[0]
709
- * await interaction.reply(`You picked: ${chosen}`)
710
- * })
1014
+ * await msg.edit('Updated content')
1015
+ * await msg.edit({ content: 'Updated', embed: { title: 'New embed' } })
711
1016
  */
712
- selectMenu(customId, handler) {
713
- this._selectHandlers.set(customId, handler);
714
- return this;
1017
+ edit(options) {
1018
+ const opts = typeof options === "string" ? { content: options } : options;
1019
+ return this._messages.edit(this.id, opts).then((raw) => new _NovaMessage(raw, this._messages, this._reactions));
715
1020
  }
716
1021
  /**
717
- * Connect to the Nova WebSocket gateway.
718
- * Resolves when the `ready` event is received.
1022
+ * Delete this message.
1023
+ * Only works if the bot is the author.
719
1024
  *
720
1025
  * @example
721
- * await client.connect()
1026
+ * await msg.delete()
722
1027
  */
723
- connect() {
724
- return new Promise((resolve, reject) => {
725
- const gatewayUrl = this.options.baseUrl.replace(/\/$/, "") + "/bot-gateway";
726
- this.socket = (0, import_socket.io)(gatewayUrl, {
727
- path: "/socket.io",
728
- transports: ["websocket"],
729
- auth: { botToken: this.options.token },
730
- reconnection: true,
731
- reconnectionAttempts: Infinity,
732
- reconnectionDelay: 1e3,
733
- reconnectionDelayMax: 3e4
734
- });
735
- const onConnectError = (err) => {
736
- this.socket?.disconnect();
737
- this.socket = null;
738
- reject(new Error(`[nova-bot-sdk] Gateway connection failed: ${err.message}`));
739
- };
740
- this.socket.once("connect_error", onConnectError);
741
- this.socket.once("bot:ready", (data) => {
742
- this.socket.off("connect_error", onConnectError);
743
- this.botUser = {
744
- ...data.bot,
745
- scopes: data.scopes,
746
- // Flatten botUser fields for convenience so bot.username works
747
- username: data.bot.botUser?.username ?? data.bot.name,
748
- displayName: data.bot.botUser?.displayName ?? data.bot.name
749
- };
750
- this.emit("ready", this.botUser);
751
- resolve();
752
- });
753
- this.socket.on("interaction:created", (raw) => {
754
- const interaction = new NovaInteraction(raw, this.interactions);
755
- this.emit("interactionCreate", interaction);
756
- const run = (h) => {
757
- if (h) Promise.resolve(h(interaction)).catch((e) => this.emit("error", e));
758
- };
759
- if (interaction.isCommand() && interaction.commandName) {
760
- run(this._commandHandlers.get(interaction.commandName));
761
- } else if (interaction.isButton() && interaction.customId) {
762
- run(this._buttonHandlers.get(interaction.customId));
763
- } else if (interaction.isSelectMenu() && interaction.customId) {
764
- run(this._selectHandlers.get(interaction.customId));
765
- }
766
- });
767
- this.socket.on("bot:event", (event) => {
768
- this.emit("event", event);
769
- const shorthand = EVENT_MAP[event.type];
770
- if (shorthand) {
771
- this.emit(shorthand, event.data);
772
- }
773
- });
774
- this.socket.on("bot:error", (err) => {
775
- this.emit("error", err);
776
- });
777
- this.socket.on("disconnect", (reason) => {
778
- this.emit("disconnect", reason);
779
- });
780
- });
1028
+ delete() {
1029
+ return this._messages.delete(this.id);
781
1030
  }
782
1031
  /**
783
- * Disconnect from the gateway and clean up.
1032
+ * Pin this message in its channel.
1033
+ *
1034
+ * @example
1035
+ * await msg.pin()
784
1036
  */
785
- disconnect() {
786
- if (this.socket) {
787
- const sock = this.socket;
788
- this.socket = null;
789
- try {
790
- sock.io.reconnection(false);
791
- } catch {
792
- }
793
- try {
794
- sock.io?.engine?.socket?.terminate?.();
795
- } catch {
796
- }
797
- try {
798
- sock.io?.engine?.socket?.destroy?.();
799
- } catch {
800
- }
801
- try {
802
- sock.io?.engine?.close?.();
803
- } catch {
804
- }
805
- try {
806
- sock.disconnect();
807
- } catch {
808
- }
809
- }
1037
+ pin() {
1038
+ return this._messages.pin(this.id);
810
1039
  }
811
1040
  /**
812
- * Send a message via the WebSocket gateway (lower latency than HTTP).
813
- * Requires the `messages.write` scope.
1041
+ * Unpin this message.
814
1042
  *
815
1043
  * @example
816
- * client.wsSend('channel-id', 'Hello from the gateway!')
1044
+ * await msg.unpin()
817
1045
  */
818
- wsSend(channelId, content) {
819
- if (!this.socket?.connected) {
820
- throw new Error("[nova-bot-sdk] Not connected. Call client.connect() first.");
821
- }
822
- this.socket.emit("bot:message:send", { channelId, content });
1046
+ unpin() {
1047
+ return this._messages.unpin(this.id);
823
1048
  }
824
1049
  /**
825
- * Start a typing indicator via WebSocket.
1050
+ * Re-fetch the latest version of this message from the server.
1051
+ *
1052
+ * @example
1053
+ * const fresh = await msg.fetch()
826
1054
  */
827
- wsTypingStart(channelId) {
828
- this.socket?.emit("bot:typing:start", { channelId });
1055
+ async fetch() {
1056
+ const raw = await this._messages.fetchOne(this.id);
1057
+ return new _NovaMessage(raw, this._messages, this._reactions);
829
1058
  }
830
1059
  /**
831
- * Stop a typing indicator via WebSocket.
1060
+ * Get a URL to this message (deep link).
832
1061
  */
833
- wsTypingStop(channelId) {
834
- this.socket?.emit("bot:typing:stop", { channelId });
835
- }
836
- on(event, listener) {
837
- return super.on(event, listener);
838
- }
839
- once(event, listener) {
840
- return super.once(event, listener);
1062
+ get url() {
1063
+ return `/channels/${this.channelId}/messages/${this.id}`;
841
1064
  }
842
- off(event, listener) {
843
- return super.off(event, listener);
1065
+ /**
1066
+ * Return the raw message object.
1067
+ */
1068
+ toJSON() {
1069
+ return {
1070
+ id: this.id,
1071
+ content: this.content,
1072
+ channelId: this.channelId,
1073
+ author: this.author,
1074
+ embed: this.embed,
1075
+ components: this.components,
1076
+ replyToId: this.replyToId,
1077
+ attachments: this.attachments,
1078
+ reactions: this.reactions,
1079
+ createdAt: this.createdAt.toISOString(),
1080
+ editedAt: this.editedAt?.toISOString() ?? null
1081
+ };
844
1082
  }
845
- emit(event, ...args) {
846
- return super.emit(event, ...args);
1083
+ toString() {
1084
+ return `NovaMessage(${this.id}): ${this.content.slice(0, 50)}${this.content.length > 50 ? "\u2026" : ""}`;
847
1085
  }
848
1086
  };
849
1087
 
850
- // src/builders/EmbedBuilder.ts
851
- var EmbedBuilder = class {
852
- constructor() {
853
- this._data = {};
854
- }
855
- /** Set the embed title (shown in bold at the top). */
856
- setTitle(title) {
857
- this._data.title = title;
858
- return this;
859
- }
860
- /** Set the main description text (supports markdown). */
861
- setDescription(description) {
862
- this._data.description = description;
863
- return this;
864
- }
1088
+ // src/structures/NovaChannel.ts
1089
+ var NovaChannel = class _NovaChannel {
1090
+ constructor(raw, channels, messages) {
1091
+ this.id = raw.id;
1092
+ this.name = raw.name;
1093
+ this.type = raw.type;
1094
+ this.serverId = raw.serverId;
1095
+ this.topic = raw.topic;
1096
+ this.position = raw.position;
1097
+ this.slowMode = raw.slowMode ?? 0;
1098
+ this.createdAt = new Date(raw.createdAt);
1099
+ this._channels = channels;
1100
+ this._messages = messages;
1101
+ }
1102
+ // ─── Type guards ────────────────────────────────────────────────────────────
1103
+ /** `true` for text channels. */
1104
+ isText() {
1105
+ return this.type === "TEXT";
1106
+ }
1107
+ /** `true` for voice channels. */
1108
+ isVoice() {
1109
+ return this.type === "VOICE";
1110
+ }
1111
+ /** `true` for announcement channels. */
1112
+ isAnnouncement() {
1113
+ return this.type === "ANNOUNCEMENT";
1114
+ }
1115
+ /** `true` for forum channels. */
1116
+ isForum() {
1117
+ return this.type === "FORUM";
1118
+ }
1119
+ /** `true` for stage channels. */
1120
+ isStage() {
1121
+ return this.type === "STAGE";
1122
+ }
1123
+ /** `true` if slow-mode is enabled on this channel. */
1124
+ hasSlowMode() {
1125
+ return this.slowMode > 0;
1126
+ }
1127
+ // ─── Messaging ──────────────────────────────────────────────────────────────
865
1128
  /**
866
- * Set the accent colour.
867
- * @param color Hex string e.g. `'#5865F2'` or `'5865F2'`
1129
+ * Send a message to this channel.
1130
+ * Accepts a plain string or a full options object.
1131
+ *
1132
+ * @example
1133
+ * await channel.send('Hello!')
1134
+ * await channel.send({ content: 'Hi', embed: { title: 'Stats' } })
868
1135
  */
869
- setColor(color) {
870
- this._data.color = color.startsWith("#") ? color : `#${color}`;
871
- return this;
872
- }
873
- /** Make the title a clickable hyperlink. */
874
- setUrl(url) {
875
- this._data.url = url;
876
- return this;
877
- }
878
- /** Small image shown in the top-right corner. */
879
- setThumbnail(url) {
880
- this._data.thumbnail = url;
881
- return this;
882
- }
883
- /** Large image shown below the fields. */
884
- setImage(url) {
885
- this._data.image = url;
886
- return this;
1136
+ send(options) {
1137
+ const body = typeof options === "string" ? { content: options } : options;
1138
+ return this._messages.send(this.id, body);
887
1139
  }
888
- /** Footer text shown at the very bottom of the embed. */
889
- setFooter(text) {
890
- this._data.footer = text;
891
- return this;
1140
+ /**
1141
+ * Fetch recent messages from this channel.
1142
+ *
1143
+ * @example
1144
+ * const messages = await channel.fetchMessages({ limit: 50 })
1145
+ */
1146
+ fetchMessages(options = {}) {
1147
+ return this._channels.fetchMessages(this.id, options);
892
1148
  }
893
1149
  /**
894
- * Add a timestamp to the footer line.
895
- * @param date Defaults to `new Date()`.
1150
+ * Fetch all pinned messages in this channel.
1151
+ *
1152
+ * @example
1153
+ * const pins = await channel.fetchPins()
896
1154
  */
897
- setTimestamp(date) {
898
- const d = date instanceof Date ? date : date != null ? new Date(date) : /* @__PURE__ */ new Date();
899
- this._data.timestamp = d.toISOString();
900
- return this;
1155
+ fetchPins() {
1156
+ return this._channels.fetchPins(this.id);
901
1157
  }
902
- /** Author name + optional icon shown above the title. */
903
- setAuthor(author) {
904
- this._data.author = author;
905
- return this;
1158
+ /**
1159
+ * Start a typing indicator (shows "Bot is typing…" for ~5 seconds).
1160
+ *
1161
+ * @example
1162
+ * await channel.startTyping()
1163
+ */
1164
+ startTyping() {
1165
+ return this._channels.startTyping(this.id);
906
1166
  }
1167
+ // ─── Management ─────────────────────────────────────────────────────────────
907
1168
  /**
908
- * Add a single field.
909
- * @param inline Pass `true` to render this field side-by-side with adjacent inline fields.
1169
+ * Edit this channel's properties.
1170
+ * Requires the `channels.manage` scope.
1171
+ *
1172
+ * @example
1173
+ * await channel.edit({ name: 'general-2', topic: 'The second general channel' })
910
1174
  */
911
- addField(name, value, inline) {
912
- if (!this._data.fields) this._data.fields = [];
913
- this._data.fields.push({ name, value, inline });
914
- return this;
1175
+ async edit(options) {
1176
+ const raw = await this._channels.edit(this.id, options);
1177
+ return new _NovaChannel(raw, this._channels, this._messages);
915
1178
  }
916
- /** Add multiple fields at once. */
917
- addFields(...fields) {
918
- if (!this._data.fields) this._data.fields = [];
919
- this._data.fields.push(...fields);
920
- return this;
1179
+ /**
1180
+ * Delete this channel.
1181
+ * Requires the `channels.manage` scope.
1182
+ *
1183
+ * @example
1184
+ * await channel.delete()
1185
+ */
1186
+ delete() {
1187
+ return this._channels.delete(this.id);
921
1188
  }
922
- /** Replace all fields. */
923
- setFields(fields) {
924
- this._data.fields = [...fields];
925
- return this;
1189
+ // ─── Serialisation ──────────────────────────────────────────────────────────
1190
+ /**
1191
+ * Returns the channel as a mention-style string: `#name`.
1192
+ */
1193
+ toString() {
1194
+ return `#${this.name}`;
926
1195
  }
927
- /** Serialise to a plain `Embed` object you can pass to any API call. */
1196
+ /** Returns the raw channel data. */
928
1197
  toJSON() {
929
1198
  return {
930
- ...this._data,
931
- fields: this._data.fields ? [...this._data.fields] : void 0
1199
+ id: this.id,
1200
+ name: this.name,
1201
+ type: this.type,
1202
+ serverId: this.serverId,
1203
+ topic: this.topic,
1204
+ position: this.position,
1205
+ slowMode: this.slowMode,
1206
+ createdAt: this.createdAt.toISOString()
932
1207
  };
933
1208
  }
934
1209
  };
935
1210
 
936
- // src/builders/ButtonBuilder.ts
937
- var ButtonBuilder = class {
938
- constructor() {
939
- this._customId = "";
940
- this._label = "";
941
- this._style = "primary";
942
- this._disabled = false;
943
- }
944
- /** Custom ID returned in the `BUTTON_CLICK` interaction. Not required for `link` style buttons. */
945
- setCustomId(customId) {
946
- this._customId = customId;
947
- return this;
1211
+ // src/structures/NovaMember.ts
1212
+ var NovaMember = class {
1213
+ constructor(raw, serverId, members) {
1214
+ this.userId = raw.user.id;
1215
+ this.serverId = serverId;
1216
+ this.username = raw.user.username;
1217
+ this.displayName = raw.user.displayName;
1218
+ this.avatar = raw.user.avatar;
1219
+ this.role = raw.role;
1220
+ this.status = raw.user.status;
1221
+ this.isBot = raw.user.isBot;
1222
+ this.joinedAt = new Date(raw.joinedAt);
1223
+ this._members = members;
1224
+ }
1225
+ // ─── Type guards ────────────────────────────────────────────────────────────
1226
+ /** `true` if this member is the server owner. */
1227
+ isOwner() {
1228
+ return this.role === "OWNER";
1229
+ }
1230
+ /** `true` if this member is an admin or the server owner. */
1231
+ isAdmin() {
1232
+ return this.role === "ADMIN" || this.role === "OWNER";
1233
+ }
1234
+ /** `true` if this member is a regular (non-privileged) member. */
1235
+ isRegularMember() {
1236
+ return this.role === "MEMBER";
1237
+ }
1238
+ /** `true` if the user is currently online. */
1239
+ isOnline() {
1240
+ return this.status === "ONLINE";
1241
+ }
1242
+ /** `true` if the user is idle / away. */
1243
+ isIdle() {
1244
+ return this.status === "IDLE";
1245
+ }
1246
+ /** `true` if the user is set to Do Not Disturb. */
1247
+ isDND() {
1248
+ return this.status === "DND";
1249
+ }
1250
+ /** `true` if the user appears offline. */
1251
+ isOffline() {
1252
+ return this.status === "OFFLINE";
1253
+ }
1254
+ // ─── Actions ────────────────────────────────────────────────────────────────
1255
+ /**
1256
+ * Kick this member from the server.
1257
+ * Bots cannot kick owners or admins (throws 403).
1258
+ *
1259
+ * @example
1260
+ * await member.kick()
1261
+ */
1262
+ kick() {
1263
+ return this._members.kick(this.serverId, this.userId);
948
1264
  }
949
- /** Text displayed on the button. */
950
- setLabel(label) {
951
- this._label = label;
952
- return this;
1265
+ /**
1266
+ * Ban this member from the server with an optional reason.
1267
+ * Bots cannot ban owners or admins (throws 403).
1268
+ *
1269
+ * @example
1270
+ * await member.ban('Repeated rule violations')
1271
+ */
1272
+ ban(reason) {
1273
+ return this._members.ban(this.serverId, this.userId, reason);
953
1274
  }
954
1275
  /**
955
- * Visual style:
956
- * - `primary` — blurple / brand colour
957
- * - `secondary` — grey
958
- * - `success` — green
959
- * - `danger` red
960
- * - `link` grey, navigates to a URL instead of creating an interaction
1276
+ * Send this user a direct message.
1277
+ * Requires the `messages.write` scope.
1278
+ *
1279
+ * @example
1280
+ * await member.dm('Welcome to the server!')
1281
+ * await member.dm({ content: 'Hello', embed: { title: 'Rules' } })
961
1282
  */
962
- setStyle(style) {
963
- this._style = style;
964
- return this;
1283
+ dm(options) {
1284
+ return this._members.dm(this.userId, options);
965
1285
  }
966
- /** Emoji shown to the left of the label (unicode or custom e.g. `'👋'`). */
967
- setEmoji(emoji) {
968
- this._emoji = emoji;
969
- return this;
1286
+ /**
1287
+ * Assign a custom role to this member.
1288
+ * Requires the `members.roles` scope.
1289
+ *
1290
+ * @example
1291
+ * await member.addRole('role-id')
1292
+ */
1293
+ addRole(roleId) {
1294
+ return this._members.addRole(this.serverId, this.userId, roleId);
970
1295
  }
971
1296
  /**
972
- * URL for `link` style buttons.
973
- * Sets the style to `'link'` automatically.
1297
+ * Remove a custom role from this member.
1298
+ * Requires the `members.roles` scope.
1299
+ *
1300
+ * @example
1301
+ * await member.removeRole('role-id')
974
1302
  */
975
- setUrl(url) {
976
- this._url = url;
977
- this._style = "link";
978
- return this;
1303
+ removeRole(roleId) {
1304
+ return this._members.removeRole(this.serverId, this.userId, roleId);
979
1305
  }
980
- /** Prevent users from clicking the button. */
981
- setDisabled(disabled = true) {
982
- this._disabled = disabled;
983
- return this;
1306
+ // ─── Serialisation ──────────────────────────────────────────────────────────
1307
+ /**
1308
+ * Returns a mention-style string: `@displayName`.
1309
+ */
1310
+ toString() {
1311
+ return `@${this.displayName}`;
984
1312
  }
1313
+ /** Returns the raw member data. */
985
1314
  toJSON() {
986
1315
  return {
987
- type: "button",
988
- customId: this._customId,
989
- label: this._label || void 0,
990
- style: this._style,
991
- emoji: this._emoji,
992
- url: this._url,
993
- disabled: this._disabled || void 0
1316
+ role: this.role,
1317
+ joinedAt: this.joinedAt.toISOString(),
1318
+ user: {
1319
+ id: this.userId,
1320
+ username: this.username,
1321
+ displayName: this.displayName,
1322
+ avatar: this.avatar,
1323
+ status: this.status,
1324
+ isBot: this.isBot
1325
+ }
994
1326
  };
995
1327
  }
996
1328
  };
997
1329
 
998
- // src/builders/SelectMenuBuilder.ts
999
- var SelectMenuBuilder = class {
1000
- constructor() {
1001
- this._customId = "";
1002
- this._disabled = false;
1003
- this._options = [];
1004
- }
1005
- /** Custom ID returned in the `SELECT_MENU` interaction. */
1006
- setCustomId(customId) {
1007
- this._customId = customId;
1008
- return this;
1009
- }
1010
- /** Greyed-out hint shown when nothing is selected. */
1011
- setPlaceholder(placeholder) {
1012
- this._placeholder = placeholder;
1013
- return this;
1014
- }
1330
+ // src/client.ts
1331
+ var NovaClient = class extends import_node_events.EventEmitter {
1332
+ constructor(options) {
1333
+ super();
1334
+ /** The authenticated bot application. Available after `ready` fires. */
1335
+ this.botUser = null;
1336
+ this.socket = null;
1337
+ this._cronTimers = [];
1338
+ // ── Command / component routing ───────────────────────────────────────────────────
1339
+ this._commandHandlers = /* @__PURE__ */ new Map();
1340
+ this._buttonHandlers = /* @__PURE__ */ new Map();
1341
+ this._selectHandlers = /* @__PURE__ */ new Map();
1342
+ this._modalHandlers = /* @__PURE__ */ new Map();
1343
+ if (!options.token) {
1344
+ throw new Error("[nova-bot-sdk] A bot token is required.");
1345
+ }
1346
+ if (!options.token.startsWith("nova_bot_")) {
1347
+ console.warn('[nova-bot-sdk] Warning: token does not start with "nova_bot_". Are you sure this is a bot token?');
1348
+ }
1349
+ this.options = {
1350
+ token: options.token,
1351
+ baseUrl: options.baseUrl ?? "https://novachatapp.com"
1352
+ };
1353
+ this.http = new HttpClient(this.options.baseUrl, this.options.token);
1354
+ this.messages = new MessagesAPI(this.http);
1355
+ this.commands = new CommandsAPI(this.http);
1356
+ this.members = new MembersAPI(this.http);
1357
+ this.servers = new ServersAPI(this.http);
1358
+ this.interactions = new InteractionsAPI(this.http, this);
1359
+ this.permissions = new PermissionsAPI(this.http);
1360
+ this.channels = new ChannelsAPI(this.http);
1361
+ this.reactions = new ReactionsAPI(this.http);
1362
+ this.on("error", () => {
1363
+ });
1364
+ const cleanup = () => this.disconnect();
1365
+ process.once("beforeExit", cleanup);
1366
+ process.once("SIGINT", () => {
1367
+ cleanup();
1368
+ process.exit(0);
1369
+ });
1370
+ process.once("SIGTERM", () => {
1371
+ cleanup();
1372
+ process.exit(0);
1373
+ });
1374
+ }
1375
+ /**
1376
+ * Register a handler for a slash or prefix command by name.
1377
+ * Automatically routes `interactionCreate` events whose `commandName` matches.
1378
+ *
1379
+ * @example
1380
+ * client.command('ping', async (interaction) => {
1381
+ * await interaction.reply('Pong! 🏓')
1382
+ * })
1383
+ */
1384
+ command(name, handler) {
1385
+ this._commandHandlers.set(name, handler);
1386
+ return this;
1387
+ }
1388
+ /**
1389
+ * Register a handler for a button by its `customId`.
1390
+ *
1391
+ * @example
1392
+ * client.button('confirm_delete', async (interaction) => {
1393
+ * await interaction.replyEphemeral('Deleted.')
1394
+ * })
1395
+ */
1396
+ button(customId, handler) {
1397
+ this._buttonHandlers.set(customId, handler);
1398
+ return this;
1399
+ }
1400
+ /**
1401
+ * Register a handler for a select menu by its `customId`.
1402
+ *
1403
+ * @example
1404
+ * client.selectMenu('colour_pick', async (interaction) => {
1405
+ * const chosen = interaction.values[0]
1406
+ * await interaction.reply(`You picked: ${chosen}`)
1407
+ * })
1408
+ */
1409
+ selectMenu(customId, handler) {
1410
+ this._selectHandlers.set(customId, handler);
1411
+ return this;
1412
+ }
1413
+ /**
1414
+ * Register a handler for a modal submission by its `customId`.
1415
+ * Called automatically when the user submits a modal with that `customId`.
1416
+ *
1417
+ * @example
1418
+ * client.modal('report_modal', async (interaction) => {
1419
+ * const reason = interaction.modalData.reason
1420
+ * await interaction.replyEphemeral(`Report received: ${reason}`)
1421
+ * })
1422
+ */
1423
+ modal(customId, handler) {
1424
+ this._modalHandlers.set(customId, handler);
1425
+ return this;
1426
+ }
1427
+ /**
1428
+ * Connect to the Nova WebSocket gateway.
1429
+ * Resolves when the `ready` event is received.
1430
+ *
1431
+ * @example
1432
+ * await client.connect()
1433
+ */
1434
+ connect() {
1435
+ return new Promise((resolve, reject) => {
1436
+ const gatewayUrl = this.options.baseUrl.replace(/\/$/, "") + "/bot-gateway";
1437
+ this.socket = (0, import_socket.io)(gatewayUrl, {
1438
+ path: "/socket.io",
1439
+ transports: ["websocket"],
1440
+ auth: { botToken: this.options.token },
1441
+ reconnection: true,
1442
+ reconnectionAttempts: Infinity,
1443
+ reconnectionDelay: 1e3,
1444
+ reconnectionDelayMax: 3e4
1445
+ });
1446
+ const onConnectError = (err) => {
1447
+ this.socket?.disconnect();
1448
+ this.socket = null;
1449
+ reject(new Error(`[nova-bot-sdk] Gateway connection failed: ${err.message}`));
1450
+ };
1451
+ this.socket.once("connect_error", onConnectError);
1452
+ this.socket.once("bot:ready", (data) => {
1453
+ this.socket.off("connect_error", onConnectError);
1454
+ this.botUser = {
1455
+ ...data.bot,
1456
+ scopes: data.scopes,
1457
+ // Flatten botUser fields for convenience so bot.username works
1458
+ username: data.bot.botUser?.username ?? data.bot.name,
1459
+ displayName: data.bot.botUser?.displayName ?? data.bot.name
1460
+ };
1461
+ this.emit("ready", this.botUser);
1462
+ resolve();
1463
+ });
1464
+ this.socket.on("interaction:created", (raw) => {
1465
+ const interaction = new NovaInteraction(raw, this.interactions);
1466
+ this.emit("interactionCreate", interaction);
1467
+ const run = (h) => {
1468
+ if (h) Promise.resolve(h(interaction)).catch((e) => this.emit("error", e));
1469
+ };
1470
+ if (interaction.isCommand() && interaction.commandName) {
1471
+ run(this._commandHandlers.get(interaction.commandName));
1472
+ } else if (interaction.isButton() && interaction.customId) {
1473
+ run(this._buttonHandlers.get(interaction.customId));
1474
+ } else if (interaction.isSelectMenu() && interaction.customId) {
1475
+ run(this._selectHandlers.get(interaction.customId));
1476
+ } else if (interaction.isModalSubmit() && interaction.customId) {
1477
+ run(this._modalHandlers.get(interaction.customId));
1478
+ }
1479
+ });
1480
+ this.socket.on("bot:event", (event) => {
1481
+ this.emit("event", event);
1482
+ switch (event.type) {
1483
+ case "message.created":
1484
+ case "message.edited": {
1485
+ const raw = event.data;
1486
+ const wrapped = new NovaMessage(raw, this.messages, this.reactions);
1487
+ if (event.type === "message.created") this.emit("messageCreate", wrapped);
1488
+ else this.emit("messageUpdate", wrapped);
1489
+ break;
1490
+ }
1491
+ case "message.deleted":
1492
+ this.emit("messageDelete", event.data);
1493
+ break;
1494
+ case "message.reaction_added":
1495
+ this.emit("reactionAdd", event.data);
1496
+ break;
1497
+ case "message.reaction_removed":
1498
+ this.emit("reactionRemove", event.data);
1499
+ break;
1500
+ case "user.joined_server":
1501
+ this.emit("memberAdd", event.data);
1502
+ break;
1503
+ case "user.left_server":
1504
+ this.emit("memberRemove", event.data);
1505
+ break;
1506
+ case "user.started_typing":
1507
+ this.emit("typingStart", event.data);
1508
+ break;
1509
+ case "message.pinned":
1510
+ this.emit("messagePinned", event.data);
1511
+ break;
1512
+ case "channel.created":
1513
+ this.emit("channelCreate", event.data);
1514
+ break;
1515
+ case "channel.updated":
1516
+ this.emit("channelUpdate", event.data);
1517
+ break;
1518
+ case "channel.deleted":
1519
+ this.emit("channelDelete", event.data);
1520
+ break;
1521
+ case "role.created":
1522
+ this.emit("roleCreate", event.data);
1523
+ break;
1524
+ case "role.deleted":
1525
+ this.emit("roleDelete", event.data);
1526
+ break;
1527
+ case "user.voice_joined":
1528
+ this.emit("voiceJoin", event.data);
1529
+ break;
1530
+ case "user.voice_left":
1531
+ this.emit("voiceLeave", event.data);
1532
+ break;
1533
+ case "user.banned":
1534
+ this.emit("memberBanned", event.data);
1535
+ break;
1536
+ case "user.unbanned":
1537
+ this.emit("memberUnbanned", event.data);
1538
+ break;
1539
+ case "user.updated_profile":
1540
+ this.emit("memberUpdate", event.data);
1541
+ break;
1542
+ }
1543
+ });
1544
+ this.socket.on("bot:error", (err) => {
1545
+ this.emit("error", err);
1546
+ });
1547
+ this.socket.on("disconnect", (reason) => {
1548
+ this.emit("disconnect", reason);
1549
+ });
1550
+ });
1551
+ }
1552
+ /**
1553
+ * Register a recurring task that fires at a set interval.
1554
+ * All cron tasks are automatically cancelled on `disconnect()`.
1555
+ *
1556
+ * @param intervalMs - How often to run the task (in milliseconds).
1557
+ * @param fn - Async or sync function to call on each tick.
1558
+ * @returns A cancel function — call it to stop this specific task.
1559
+ *
1560
+ * @example
1561
+ * // Check for new announcements every 30 seconds
1562
+ * client.cron(30_000, async () => {
1563
+ * const messages = await client.messages.fetch(channelId, { limit: 5 })
1564
+ * // do something...
1565
+ * })
1566
+ */
1567
+ cron(intervalMs, fn) {
1568
+ const id = setInterval(() => {
1569
+ try {
1570
+ const result = fn();
1571
+ if (result && typeof result.catch === "function") {
1572
+ ;
1573
+ result.catch((e) => this.emit("error", e));
1574
+ }
1575
+ } catch (e) {
1576
+ this.emit("error", e);
1577
+ }
1578
+ }, intervalMs);
1579
+ this._cronTimers.push(id);
1580
+ return () => {
1581
+ clearInterval(id);
1582
+ this._cronTimers = this._cronTimers.filter((t) => t !== id);
1583
+ };
1584
+ }
1585
+ /**
1586
+ * Set the bot's presence status.
1587
+ * Broadcasts to all servers the bot is in via WebSocket.
1588
+ *
1589
+ * @example
1590
+ * client.setStatus('DND') // Do Not Disturb
1591
+ * client.setStatus('IDLE') // Away
1592
+ * client.setStatus('OFFLINE') // Appear offline
1593
+ * client.setStatus('ONLINE') // Back online
1594
+ */
1595
+ setStatus(status) {
1596
+ this.socket?.emit("bot:status", { status });
1597
+ }
1598
+ // ─── Rich-wrapper helpers ────────────────────────────────────────────────────
1599
+ /**
1600
+ * Fetch a single channel and return it as a rich `NovaChannel` wrapper.
1601
+ *
1602
+ * @example
1603
+ * const channel = await client.fetchChannel('channel-id')
1604
+ * await channel.send('Hello!')
1605
+ */
1606
+ async fetchChannel(channelId) {
1607
+ const raw = await this.channels.fetch(channelId);
1608
+ return new NovaChannel(raw, this.channels, this.messages);
1609
+ }
1610
+ /**
1611
+ * Fetch all channels in a server and return them as `NovaChannel` wrappers.
1612
+ *
1613
+ * @example
1614
+ * const channels = await client.fetchChannels('server-id')
1615
+ * const textChannels = channels.filter(c => c.isText())
1616
+ */
1617
+ async fetchChannels(serverId) {
1618
+ const list = await this.channels.list(serverId);
1619
+ return list.map((raw) => new NovaChannel(raw, this.channels, this.messages));
1620
+ }
1621
+ /**
1622
+ * Fetch all members in a server and return them as `NovaMember` wrappers.
1623
+ *
1624
+ * @example
1625
+ * const members = await client.fetchMembers('server-id')
1626
+ * const bots = members.filter(m => m.isBot)
1627
+ */
1628
+ async fetchMembers(serverId) {
1629
+ const list = await this.members.list(serverId);
1630
+ return list.map((raw) => new NovaMember(raw, serverId, this.members));
1631
+ }
1632
+ /**
1633
+ * Fetch a single member from a server and return them as a `NovaMember` wrapper.
1634
+ * Throws if the user is not found in the server.
1635
+ *
1636
+ * @example
1637
+ * const member = await client.fetchMember('server-id', 'user-id')
1638
+ * await member.dm('Welcome!')
1639
+ */
1640
+ async fetchMember(serverId, userId) {
1641
+ const list = await this.members.list(serverId);
1642
+ const raw = list.find((m) => m.user.id === userId);
1643
+ if (!raw) throw new Error(`[nova-bot-sdk] Member ${userId} not found in server ${serverId}`);
1644
+ return new NovaMember(raw, serverId, this.members);
1645
+ }
1646
+ // ─── Event fence ──────────────────────────────────────────────────────────────
1647
+ /**
1648
+ * Wait for a specific event to be emitted, optionally filtered.
1649
+ * Rejects after `timeoutMs` (default 30 s) if the condition never fires.
1650
+ *
1651
+ * @example
1652
+ * // Wait for any message in a specific channel
1653
+ * const msg = await client.waitFor('messageCreate', m => m.channelId === channelId)
1654
+ *
1655
+ * // Wait for a button click with a timeout
1656
+ * const i = await client.waitFor('interactionCreate', i => i.isButton(), 60_000)
1657
+ */
1658
+ waitFor(event, filter, timeoutMs = 3e4) {
1659
+ return new Promise((resolve, reject) => {
1660
+ const timer = setTimeout(() => {
1661
+ this.off(event, handler);
1662
+ reject(new Error(`[nova-bot-sdk] waitFor('${String(event)}') timed out after ${timeoutMs}ms`));
1663
+ }, timeoutMs);
1664
+ const handler = (...args) => {
1665
+ const arg = args[0];
1666
+ if (!filter || filter(arg)) {
1667
+ clearTimeout(timer);
1668
+ this.off(event, handler);
1669
+ resolve(arg);
1670
+ }
1671
+ };
1672
+ this.on(event, handler);
1673
+ });
1674
+ }
1675
+ /**
1676
+ * Disconnect from the gateway and clean up.
1677
+ */
1678
+ disconnect() {
1679
+ for (const id of this._cronTimers) clearInterval(id);
1680
+ this._cronTimers = [];
1681
+ if (this.socket) {
1682
+ const sock = this.socket;
1683
+ this.socket = null;
1684
+ try {
1685
+ sock.io.reconnection(false);
1686
+ } catch {
1687
+ }
1688
+ try {
1689
+ sock.io?.engine?.socket?.terminate?.();
1690
+ } catch {
1691
+ }
1692
+ try {
1693
+ sock.io?.engine?.socket?.destroy?.();
1694
+ } catch {
1695
+ }
1696
+ try {
1697
+ sock.io?.engine?.close?.();
1698
+ } catch {
1699
+ }
1700
+ try {
1701
+ sock.disconnect();
1702
+ } catch {
1703
+ }
1704
+ }
1705
+ }
1706
+ /**
1707
+ * Send a message via the WebSocket gateway (lower latency than HTTP).
1708
+ * Requires the `messages.write` scope.
1709
+ *
1710
+ * @example
1711
+ * client.wsSend('channel-id', 'Hello from the gateway!')
1712
+ */
1713
+ wsSend(channelId, content) {
1714
+ if (!this.socket?.connected) {
1715
+ throw new Error("[nova-bot-sdk] Not connected. Call client.connect() first.");
1716
+ }
1717
+ this.socket.emit("bot:message:send", { channelId, content });
1718
+ }
1719
+ /**
1720
+ * Start a typing indicator via WebSocket.
1721
+ */
1722
+ wsTypingStart(channelId) {
1723
+ this.socket?.emit("bot:typing:start", { channelId });
1724
+ }
1725
+ /**
1726
+ * Stop a typing indicator via WebSocket.
1727
+ */
1728
+ wsTypingStop(channelId) {
1729
+ this.socket?.emit("bot:typing:stop", { channelId });
1730
+ }
1731
+ on(event, listener) {
1732
+ return super.on(event, listener);
1733
+ }
1734
+ once(event, listener) {
1735
+ return super.once(event, listener);
1736
+ }
1737
+ off(event, listener) {
1738
+ return super.off(event, listener);
1739
+ }
1740
+ emit(event, ...args) {
1741
+ return super.emit(event, ...args);
1742
+ }
1743
+ };
1744
+
1745
+ // src/builders/EmbedBuilder.ts
1746
+ var EmbedBuilder = class {
1747
+ constructor() {
1748
+ this._data = {};
1749
+ }
1750
+ /** Set the embed title (shown in bold at the top). */
1751
+ setTitle(title) {
1752
+ this._data.title = title;
1753
+ return this;
1754
+ }
1755
+ /** Set the main description text (supports markdown). */
1756
+ setDescription(description) {
1757
+ this._data.description = description;
1758
+ return this;
1759
+ }
1760
+ /**
1761
+ * Set the accent colour.
1762
+ * @param color Hex string — e.g. `'#5865F2'` or `'5865F2'`
1763
+ */
1764
+ setColor(color) {
1765
+ this._data.color = color.startsWith("#") ? color : `#${color}`;
1766
+ return this;
1767
+ }
1768
+ /** Make the title a clickable hyperlink. */
1769
+ setUrl(url) {
1770
+ this._data.url = url;
1771
+ return this;
1772
+ }
1773
+ /** Small image shown in the top-right corner. */
1774
+ setThumbnail(url) {
1775
+ this._data.thumbnail = url;
1776
+ return this;
1777
+ }
1778
+ /** Large image shown below the fields. */
1779
+ setImage(url) {
1780
+ this._data.image = url;
1781
+ return this;
1782
+ }
1783
+ /** Footer text shown at the very bottom of the embed. */
1784
+ setFooter(text) {
1785
+ this._data.footer = text;
1786
+ return this;
1787
+ }
1788
+ /**
1789
+ * Add a timestamp to the footer line.
1790
+ * @param date Defaults to `new Date()`.
1791
+ */
1792
+ setTimestamp(date) {
1793
+ const d = date instanceof Date ? date : date != null ? new Date(date) : /* @__PURE__ */ new Date();
1794
+ this._data.timestamp = d.toISOString();
1795
+ return this;
1796
+ }
1797
+ /** Author name + optional icon shown above the title. */
1798
+ setAuthor(author) {
1799
+ this._data.author = author;
1800
+ return this;
1801
+ }
1802
+ /**
1803
+ * Add a single field.
1804
+ * @param inline Pass `true` to render this field side-by-side with adjacent inline fields.
1805
+ */
1806
+ addField(name, value, inline) {
1807
+ if (!this._data.fields) this._data.fields = [];
1808
+ this._data.fields.push({ name, value, inline });
1809
+ return this;
1810
+ }
1811
+ /** Add multiple fields at once. */
1812
+ addFields(...fields) {
1813
+ if (!this._data.fields) this._data.fields = [];
1814
+ this._data.fields.push(...fields);
1815
+ return this;
1816
+ }
1817
+ /** Replace all fields. */
1818
+ setFields(fields) {
1819
+ this._data.fields = [...fields];
1820
+ return this;
1821
+ }
1822
+ /** Serialise to a plain `Embed` object you can pass to any API call. */
1823
+ toJSON() {
1824
+ return {
1825
+ ...this._data,
1826
+ fields: this._data.fields ? [...this._data.fields] : void 0
1827
+ };
1828
+ }
1829
+ };
1830
+
1831
+ // src/builders/ButtonBuilder.ts
1832
+ var ButtonBuilder = class {
1833
+ constructor() {
1834
+ this._customId = "";
1835
+ this._label = "";
1836
+ this._style = "primary";
1837
+ this._disabled = false;
1838
+ }
1839
+ /** Custom ID returned in the `BUTTON_CLICK` interaction. Not required for `link` style buttons. */
1840
+ setCustomId(customId) {
1841
+ this._customId = customId;
1842
+ return this;
1843
+ }
1844
+ /** Text displayed on the button. */
1845
+ setLabel(label) {
1846
+ this._label = label;
1847
+ return this;
1848
+ }
1849
+ /**
1850
+ * Visual style:
1851
+ * - `primary` — blurple / brand colour
1852
+ * - `secondary` — grey
1853
+ * - `success` — green
1854
+ * - `danger` — red
1855
+ * - `link` — grey, navigates to a URL instead of creating an interaction
1856
+ */
1857
+ setStyle(style) {
1858
+ this._style = style;
1859
+ return this;
1860
+ }
1861
+ /** Emoji shown to the left of the label (unicode or custom e.g. `'👋'`). */
1862
+ setEmoji(emoji) {
1863
+ this._emoji = emoji;
1864
+ return this;
1865
+ }
1866
+ /**
1867
+ * URL for `link` style buttons.
1868
+ * Sets the style to `'link'` automatically.
1869
+ */
1870
+ setUrl(url) {
1871
+ this._url = url;
1872
+ this._style = "link";
1873
+ return this;
1874
+ }
1875
+ /** Prevent users from clicking the button. */
1876
+ setDisabled(disabled = true) {
1877
+ this._disabled = disabled;
1878
+ return this;
1879
+ }
1880
+ toJSON() {
1881
+ return {
1882
+ type: "button",
1883
+ customId: this._customId,
1884
+ label: this._label || void 0,
1885
+ style: this._style,
1886
+ emoji: this._emoji,
1887
+ url: this._url,
1888
+ disabled: this._disabled || void 0
1889
+ };
1890
+ }
1891
+ };
1892
+
1893
+ // src/builders/SelectMenuBuilder.ts
1894
+ var SelectMenuBuilder = class {
1895
+ constructor() {
1896
+ this._customId = "";
1897
+ this._disabled = false;
1898
+ this._options = [];
1899
+ }
1900
+ /** Custom ID returned in the `SELECT_MENU` interaction. */
1901
+ setCustomId(customId) {
1902
+ this._customId = customId;
1903
+ return this;
1904
+ }
1905
+ /** Greyed-out hint shown when nothing is selected. */
1906
+ setPlaceholder(placeholder) {
1907
+ this._placeholder = placeholder;
1908
+ return this;
1909
+ }
1015
1910
  /** Prevent users from interacting with the menu. */
1016
1911
  setDisabled(disabled = true) {
1017
1912
  this._disabled = disabled;
1018
1913
  return this;
1019
1914
  }
1020
- /** Add a single option. */
1915
+ /** Add a single option. */
1916
+ addOption(option) {
1917
+ this._options.push(option);
1918
+ return this;
1919
+ }
1920
+ /** Add multiple options at once. */
1921
+ addOptions(...options) {
1922
+ this._options.push(...options);
1923
+ return this;
1924
+ }
1925
+ /** Replace all options. */
1926
+ setOptions(options) {
1927
+ this._options.splice(0, this._options.length, ...options);
1928
+ return this;
1929
+ }
1930
+ toJSON() {
1931
+ return {
1932
+ type: "select",
1933
+ customId: this._customId,
1934
+ placeholder: this._placeholder,
1935
+ disabled: this._disabled || void 0,
1936
+ options: [...this._options]
1937
+ };
1938
+ }
1939
+ };
1940
+
1941
+ // src/builders/ActionRowBuilder.ts
1942
+ var ActionRowBuilder = class {
1943
+ constructor() {
1944
+ this._components = [];
1945
+ }
1946
+ /** Add a single component (button or select menu). */
1947
+ addComponent(component) {
1948
+ this._components.push(component);
1949
+ return this;
1950
+ }
1951
+ /** Add multiple components at once. */
1952
+ addComponents(...components) {
1953
+ this._components.push(...components);
1954
+ return this;
1955
+ }
1956
+ /**
1957
+ * Serialise to a flat `MessageComponent[]` array — the format accepted by all API calls.
1958
+ */
1959
+ toJSON() {
1960
+ return this._components.map((c) => c.toJSON());
1961
+ }
1962
+ };
1963
+
1964
+ // src/builders/ModalBuilder.ts
1965
+ var ModalBuilder = class {
1966
+ constructor() {
1967
+ this._title = "";
1968
+ this._customId = "";
1969
+ this._fields = [];
1970
+ }
1971
+ /** Title shown at the top of the modal dialog. */
1972
+ setTitle(title) {
1973
+ this._title = title;
1974
+ return this;
1975
+ }
1976
+ /** Custom ID passed back with the `MODAL_SUBMIT` interaction. */
1977
+ setCustomId(customId) {
1978
+ this._customId = customId;
1979
+ return this;
1980
+ }
1981
+ /** Add a single text-input field. */
1982
+ addField(field) {
1983
+ this._fields.push("toJSON" in field ? field.toJSON() : field);
1984
+ return this;
1985
+ }
1986
+ /** Add multiple fields at once. */
1987
+ addFields(...fields) {
1988
+ for (const f of fields) this.addField(f);
1989
+ return this;
1990
+ }
1991
+ toJSON() {
1992
+ if (!this._title) throw new Error("ModalBuilder: title is required \u2014 call .setTitle()");
1993
+ if (!this._customId) throw new Error("ModalBuilder: customId is required \u2014 call .setCustomId()");
1994
+ if (this._fields.length === 0) throw new Error("ModalBuilder: at least one field is required \u2014 call .addField()");
1995
+ return { title: this._title, customId: this._customId, fields: [...this._fields] };
1996
+ }
1997
+ };
1998
+
1999
+ // src/builders/TextInputBuilder.ts
2000
+ var TextInputBuilder = class {
2001
+ constructor() {
2002
+ this._data = {
2003
+ customId: "",
2004
+ label: "",
2005
+ type: "short"
2006
+ };
2007
+ }
2008
+ /** Unique ID for this field — the key in `interaction.modalData` on submit. */
2009
+ setCustomId(customId) {
2010
+ this._data.customId = customId;
2011
+ return this;
2012
+ }
2013
+ /** Label shown above the input inside the modal. */
2014
+ setLabel(label) {
2015
+ this._data.label = label;
2016
+ return this;
2017
+ }
2018
+ /**
2019
+ * Input style:
2020
+ * - `'short'` — single-line text input
2021
+ * - `'paragraph'` — multi-line textarea
2022
+ */
2023
+ setStyle(style) {
2024
+ this._data.type = style;
2025
+ return this;
2026
+ }
2027
+ /** Greyed-out hint text shown when the field is empty. */
2028
+ setPlaceholder(placeholder) {
2029
+ this._data.placeholder = placeholder;
2030
+ return this;
2031
+ }
2032
+ /** Whether the user must fill in this field before submitting. */
2033
+ setRequired(required = true) {
2034
+ this._data.required = required;
2035
+ return this;
2036
+ }
2037
+ /** Minimum number of characters required. */
2038
+ setMinLength(min) {
2039
+ this._data.minLength = min;
2040
+ return this;
2041
+ }
2042
+ /** Maximum number of characters allowed. */
2043
+ setMaxLength(max) {
2044
+ this._data.maxLength = max;
2045
+ return this;
2046
+ }
2047
+ /** Pre-filled default value. */
2048
+ setValue(value) {
2049
+ this._data.value = value;
2050
+ return this;
2051
+ }
2052
+ toJSON() {
2053
+ return { ...this._data };
2054
+ }
2055
+ };
2056
+
2057
+ // src/builders/SlashCommandBuilder.ts
2058
+ var SlashCommandOptionBuilder = class {
2059
+ constructor(type) {
2060
+ this._data = { name: "", description: "", type };
2061
+ }
2062
+ /** Internal option name (lowercase, no spaces). Shown after `/command ` in the client UI. */
2063
+ setName(name) {
2064
+ this._data.name = name;
2065
+ return this;
2066
+ }
2067
+ /** Short human-readable description shown in the command picker. */
2068
+ setDescription(description) {
2069
+ this._data.description = description;
2070
+ return this;
2071
+ }
2072
+ /** Whether users must supply this option before sending the command. */
2073
+ setRequired(required = true) {
2074
+ this._data.required = required;
2075
+ return this;
2076
+ }
2077
+ /**
2078
+ * Restrict the option to specific values.
2079
+ * The picker will show these as autocomplete suggestions.
2080
+ */
2081
+ addChoice(name, value) {
2082
+ if (!this._data.choices) this._data.choices = [];
2083
+ this._data.choices.push({ name, value });
2084
+ return this;
2085
+ }
2086
+ /** Set all allowed choices at once. */
2087
+ setChoices(choices) {
2088
+ this._data.choices = [...choices];
2089
+ return this;
2090
+ }
2091
+ toJSON() {
2092
+ return { ...this._data };
2093
+ }
2094
+ };
2095
+ var SlashCommandBuilder = class {
2096
+ constructor() {
2097
+ this._data = {
2098
+ name: "",
2099
+ description: "",
2100
+ options: []
2101
+ };
2102
+ }
2103
+ /** Command name — lowercase, no spaces (e.g. `'ban'`, `'server-info'`). */
2104
+ setName(name) {
2105
+ this._data.name = name;
2106
+ return this;
2107
+ }
2108
+ /** Short description shown in the command picker UI. */
2109
+ setDescription(description) {
2110
+ this._data.description = description;
2111
+ return this;
2112
+ }
2113
+ /** Add a text (string) option. */
2114
+ addStringOption(fn) {
2115
+ this._data.options.push(fn(new SlashCommandOptionBuilder("STRING")).toJSON());
2116
+ return this;
2117
+ }
2118
+ /** Add an integer (whole number) option. */
2119
+ addIntegerOption(fn) {
2120
+ this._data.options.push(fn(new SlashCommandOptionBuilder("INTEGER")).toJSON());
2121
+ return this;
2122
+ }
2123
+ /** Add a boolean (true/false) option. */
2124
+ addBooleanOption(fn) {
2125
+ this._data.options.push(fn(new SlashCommandOptionBuilder("BOOLEAN")).toJSON());
2126
+ return this;
2127
+ }
2128
+ /** Add a user-mention option (returns a user ID string). */
2129
+ addUserOption(fn) {
2130
+ this._data.options.push(fn(new SlashCommandOptionBuilder("USER")).toJSON());
2131
+ return this;
2132
+ }
2133
+ /** Add a channel-mention option (returns a channel ID string). */
2134
+ addChannelOption(fn) {
2135
+ this._data.options.push(fn(new SlashCommandOptionBuilder("CHANNEL")).toJSON());
2136
+ return this;
2137
+ }
2138
+ /** Add a role-mention option (returns a role ID string). */
2139
+ addRoleOption(fn) {
2140
+ this._data.options.push(fn(new SlashCommandOptionBuilder("ROLE")).toJSON());
2141
+ return this;
2142
+ }
2143
+ toJSON() {
2144
+ if (!this._data.name) throw new Error("SlashCommandBuilder: name is required \u2014 call .setName()");
2145
+ if (!this._data.description) throw new Error("SlashCommandBuilder: description is required \u2014 call .setDescription()");
2146
+ return { ...this._data, options: [...this._data.options ?? []] };
2147
+ }
2148
+ };
2149
+
2150
+ // src/builders/PollBuilder.ts
2151
+ var PollBuilder = class {
2152
+ constructor() {
2153
+ this._question = "";
2154
+ this._options = [];
2155
+ this._allowMultiple = false;
2156
+ this._duration = null;
2157
+ this._anonymous = false;
2158
+ }
2159
+ /**
2160
+ * Set the poll question.
2161
+ *
2162
+ * @example
2163
+ * builder.setQuestion('Best programming language?')
2164
+ */
2165
+ setQuestion(question) {
2166
+ this._question = question;
2167
+ return this;
2168
+ }
2169
+ /**
2170
+ * Add a single answer option.
2171
+ *
2172
+ * @example
2173
+ * builder.addOption({ label: 'TypeScript', emoji: '🔷' })
2174
+ */
1021
2175
  addOption(option) {
2176
+ if (this._options.length >= 10) throw new Error("Polls can have at most 10 options");
1022
2177
  this._options.push(option);
1023
2178
  return this;
1024
2179
  }
1025
- /** Add multiple options at once. */
1026
- addOptions(...options) {
1027
- this._options.push(...options);
2180
+ /**
2181
+ * Add multiple answer options at once.
2182
+ *
2183
+ * @example
2184
+ * builder.addOptions([
2185
+ * { label: 'Yes', emoji: '✅' },
2186
+ * { label: 'No', emoji: '❌' },
2187
+ * ])
2188
+ */
2189
+ addOptions(options) {
2190
+ for (const o of options) this.addOption(o);
1028
2191
  return this;
1029
2192
  }
1030
- /** Replace all options. */
1031
- setOptions(options) {
1032
- this._options.splice(0, this._options.length, ...options);
2193
+ /**
2194
+ * Allow users to select more than one option.
2195
+ * Default: `false` (single choice).
2196
+ */
2197
+ setAllowMultiple(yes = true) {
2198
+ this._allowMultiple = yes;
2199
+ return this;
2200
+ }
2201
+ /**
2202
+ * Set the poll duration in **seconds**. Pass `null` to make it permanent.
2203
+ *
2204
+ * @example
2205
+ * builder.setDuration(60 * 60 * 24) // expires in 24 hours
2206
+ * builder.setDuration(null) // no expiry
2207
+ */
2208
+ setDuration(seconds) {
2209
+ this._duration = seconds;
2210
+ return this;
2211
+ }
2212
+ /**
2213
+ * Hide which users voted for which option.
2214
+ * Default: `false` (votes are public).
2215
+ */
2216
+ setAnonymous(yes = true) {
2217
+ this._anonymous = yes;
1033
2218
  return this;
1034
2219
  }
2220
+ /** Build the poll definition object. */
1035
2221
  toJSON() {
2222
+ if (!this._question.trim()) throw new Error("PollBuilder: question is required");
2223
+ if (this._options.length < 2) throw new Error("PollBuilder: at least 2 options are required");
1036
2224
  return {
1037
- type: "select",
1038
- customId: this._customId,
1039
- placeholder: this._placeholder,
1040
- disabled: this._disabled || void 0,
1041
- options: [...this._options]
2225
+ question: this._question,
2226
+ options: this._options,
2227
+ allowMultiple: this._allowMultiple,
2228
+ duration: this._duration,
2229
+ anonymous: this._anonymous
1042
2230
  };
1043
2231
  }
1044
2232
  };
1045
2233
 
1046
- // src/builders/ActionRowBuilder.ts
1047
- var ActionRowBuilder = class {
2234
+ // src/builders/MessageBuilder.ts
2235
+ var MessageBuilder = class {
1048
2236
  constructor() {
1049
2237
  this._components = [];
1050
2238
  }
1051
- /** Add a single component (button or select menu). */
1052
- addComponent(component) {
1053
- this._components.push(component);
2239
+ /**
2240
+ * Set the text content of the message.
2241
+ *
2242
+ * @example
2243
+ * builder.setContent('Hello world!')
2244
+ */
2245
+ setContent(content) {
2246
+ this._content = content;
1054
2247
  return this;
1055
2248
  }
1056
- /** Add multiple components at once. */
1057
- addComponents(...components) {
2249
+ /**
2250
+ * Attach an embed. Accepts an `EmbedBuilder` or raw `Embed` object.
2251
+ *
2252
+ * @example
2253
+ * builder.setEmbed(new EmbedBuilder().setTitle('Result'))
2254
+ * builder.setEmbed({ title: 'Result', color: '#57F287' })
2255
+ */
2256
+ setEmbed(embed) {
2257
+ this._embed = "toJSON" in embed && typeof embed.toJSON === "function" ? embed.toJSON() : embed;
2258
+ return this;
2259
+ }
2260
+ /**
2261
+ * Remove any embed from this message.
2262
+ */
2263
+ clearEmbed() {
2264
+ this._embed = void 0;
2265
+ return this;
2266
+ }
2267
+ /**
2268
+ * Add an `ActionRowBuilder` (or raw component array) as a component row.
2269
+ *
2270
+ * @example
2271
+ * builder.addRow(
2272
+ * new ActionRowBuilder().addComponent(btn1).addComponent(btn2)
2273
+ * )
2274
+ */
2275
+ addRow(row) {
2276
+ const components = Array.isArray(row) ? row : row.toJSON();
1058
2277
  this._components.push(...components);
1059
2278
  return this;
1060
2279
  }
1061
2280
  /**
1062
- * Serialise to a flat `MessageComponent[]` array — the format accepted by all API calls.
2281
+ * Replace all existing component rows.
2282
+ */
2283
+ setComponents(components) {
2284
+ this._components = components;
2285
+ return this;
2286
+ }
2287
+ /**
2288
+ * Set the message this is replying to.
2289
+ *
2290
+ * @example
2291
+ * builder.setReplyTo(message.id)
2292
+ */
2293
+ setReplyTo(messageId) {
2294
+ this._replyToId = messageId;
2295
+ return this;
2296
+ }
2297
+ /**
2298
+ * Attach a poll to the message.
2299
+ * Accepts a `PollBuilder` or raw `PollDefinition`.
2300
+ *
2301
+ * @example
2302
+ * builder.setPoll(
2303
+ * new PollBuilder()
2304
+ * .setQuestion('Favourite language?')
2305
+ * .addOptions([{ label: 'TypeScript' }, { label: 'Python' }])
2306
+ * )
2307
+ */
2308
+ setPoll(poll) {
2309
+ this._poll = "toJSON" in poll && typeof poll.toJSON === "function" ? poll.toJSON() : poll;
2310
+ return this;
2311
+ }
2312
+ /**
2313
+ * Build the message options object, ready to pass to `client.messages.send()`.
2314
+ *
2315
+ * @throws if neither `content` nor `embed` nor `poll` is set.
2316
+ */
2317
+ toJSON() {
2318
+ if (!this._content && !this._embed && !this._poll) {
2319
+ throw new Error("MessageBuilder: message must have content, an embed, or a poll");
2320
+ }
2321
+ return {
2322
+ ...this._content !== void 0 ? { content: this._content } : {},
2323
+ ...this._embed !== void 0 ? { embed: this._embed } : {},
2324
+ ...this._components.length > 0 ? { components: this._components } : {},
2325
+ ...this._replyToId !== void 0 ? { replyToId: this._replyToId } : {},
2326
+ ...this._poll !== void 0 ? { poll: this._poll } : {}
2327
+ };
2328
+ }
2329
+ };
2330
+
2331
+ // src/utils/Collection.ts
2332
+ var Collection = class _Collection extends Map {
2333
+ /**
2334
+ * The first value stored (insertion order), or `undefined` if empty.
2335
+ */
2336
+ first() {
2337
+ return this.values().next().value;
2338
+ }
2339
+ /**
2340
+ * The first `n` values stored (insertion order).
2341
+ */
2342
+ firstN(n) {
2343
+ const result = [];
2344
+ for (const v of this.values()) {
2345
+ if (result.length >= n) break;
2346
+ result.push(v);
2347
+ }
2348
+ return result;
2349
+ }
2350
+ /**
2351
+ * The last value stored, or `undefined` if empty.
2352
+ */
2353
+ last() {
2354
+ let last;
2355
+ for (const v of this.values()) last = v;
2356
+ return last;
2357
+ }
2358
+ /**
2359
+ * The last `n` values stored (insertion order).
2360
+ */
2361
+ lastN(n) {
2362
+ return this.toArray().slice(-n);
2363
+ }
2364
+ /**
2365
+ * Returns a random value from the collection, or `undefined` if empty.
2366
+ */
2367
+ random() {
2368
+ const arr = this.toArray();
2369
+ return arr[Math.floor(Math.random() * arr.length)];
2370
+ }
2371
+ /**
2372
+ * Find the first value that passes the predicate.
2373
+ *
2374
+ * @example
2375
+ * const bot = members.find(m => m.user.isBot)
2376
+ */
2377
+ find(fn) {
2378
+ for (const [k, v] of this) {
2379
+ if (fn(v, k, this)) return v;
2380
+ }
2381
+ return void 0;
2382
+ }
2383
+ /**
2384
+ * Find the key of the first value that passes the predicate.
2385
+ */
2386
+ findKey(fn) {
2387
+ for (const [k, v] of this) {
2388
+ if (fn(v, k, this)) return k;
2389
+ }
2390
+ return void 0;
2391
+ }
2392
+ /**
2393
+ * Returns `true` if at least one value satisfies the predicate.
2394
+ */
2395
+ some(fn) {
2396
+ for (const [k, v] of this) {
2397
+ if (fn(v, k, this)) return true;
2398
+ }
2399
+ return false;
2400
+ }
2401
+ /**
2402
+ * Returns `true` if every value satisfies the predicate.
2403
+ */
2404
+ every(fn) {
2405
+ for (const [k, v] of this) {
2406
+ if (!fn(v, k, this)) return false;
2407
+ }
2408
+ return true;
2409
+ }
2410
+ /**
2411
+ * Filter to a new Collection containing only values that pass the predicate.
2412
+ *
2413
+ * @example
2414
+ * const admins = members.filter(m => m.role === 'ADMIN')
2415
+ */
2416
+ filter(fn) {
2417
+ const result = new _Collection();
2418
+ for (const [k, v] of this) {
2419
+ if (fn(v, k, this)) result.set(k, v);
2420
+ }
2421
+ return result;
2422
+ }
2423
+ /**
2424
+ * Map each value to a new array.
2425
+ *
2426
+ * @example
2427
+ * const names = members.map(m => m.user.username)
2428
+ */
2429
+ map(fn) {
2430
+ const result = [];
2431
+ for (const [k, v] of this) result.push(fn(v, k, this));
2432
+ return result;
2433
+ }
2434
+ /**
2435
+ * Map to a new Collection with transformed values.
2436
+ *
2437
+ * @example
2438
+ * const names = channels.mapValues(c => c.name.toUpperCase())
2439
+ */
2440
+ mapValues(fn) {
2441
+ const result = new _Collection();
2442
+ for (const [k, v] of this) result.set(k, fn(v, k, this));
2443
+ return result;
2444
+ }
2445
+ /**
2446
+ * Reduce the collection to a single value.
2447
+ *
2448
+ * @example
2449
+ * const totalReactions = messages.reduce((sum, m) => sum + m.reactions.length, 0)
2450
+ */
2451
+ reduce(fn, initial) {
2452
+ let acc = initial;
2453
+ for (const [k, v] of this) acc = fn(acc, v, k, this);
2454
+ return acc;
2455
+ }
2456
+ /**
2457
+ * Returns values as an array (insertion order).
2458
+ */
2459
+ toArray() {
2460
+ return [...this.values()];
2461
+ }
2462
+ /**
2463
+ * Returns keys as an array (insertion order).
2464
+ */
2465
+ keyArray() {
2466
+ return [...this.keys()];
2467
+ }
2468
+ /**
2469
+ * Sort and return a new Collection.
2470
+ * Callback works like `Array.prototype.sort`.
2471
+ *
2472
+ * @example
2473
+ * const sorted = channels.sort((a, b) => a.position - b.position)
2474
+ */
2475
+ sort(fn) {
2476
+ const entries = [...this.entries()].sort(
2477
+ ([ak, av], [bk, bv]) => fn ? fn(av, bv, ak, bk) : 0
2478
+ );
2479
+ return new _Collection(entries);
2480
+ }
2481
+ /**
2482
+ * Split into two Collections based on a predicate.
2483
+ * Returns `[passing, failing]`.
2484
+ *
2485
+ * @example
2486
+ * const [bots, humans] = members.partition(m => m.user.isBot)
2487
+ */
2488
+ partition(fn) {
2489
+ const pass = new _Collection();
2490
+ const fail = new _Collection();
2491
+ for (const [k, v] of this) {
2492
+ if (fn(v, k, this)) pass.set(k, v);
2493
+ else fail.set(k, v);
2494
+ }
2495
+ return [pass, fail];
2496
+ }
2497
+ /**
2498
+ * Merge this collection with one or more others.
2499
+ * Later collections overwrite duplicate keys.
2500
+ */
2501
+ merge(...others) {
2502
+ const result = new _Collection(this);
2503
+ for (const other of others) {
2504
+ for (const [k, v] of other) result.set(k, v);
2505
+ }
2506
+ return result;
2507
+ }
2508
+ /**
2509
+ * Serialize to a plain `Record` (requires string keys).
2510
+ */
2511
+ toJSON() {
2512
+ const obj = {};
2513
+ for (const [k, v] of this) obj[String(k)] = v;
2514
+ return obj;
2515
+ }
2516
+ toString() {
2517
+ return `Collection(${this.size})`;
2518
+ }
2519
+ };
2520
+
2521
+ // src/utils/Cooldown.ts
2522
+ var Cooldown = class {
2523
+ /**
2524
+ * @param durationMs - Default cooldown duration in milliseconds.
2525
+ */
2526
+ constructor(durationMs) {
2527
+ this._durations = /* @__PURE__ */ new Map();
2528
+ this._last = /* @__PURE__ */ new Map();
2529
+ this._defaultMs = durationMs;
2530
+ }
2531
+ /**
2532
+ * Check remaining cooldown for a user.
2533
+ * Returns `0` if they are not on cooldown (free to proceed),
2534
+ * or the **milliseconds remaining** if they are on cooldown.
2535
+ *
2536
+ * @example
2537
+ * const ms = cooldown.check(userId)
2538
+ * if (ms > 0) return interaction.replyEphemeral(`Wait ${Cooldown.format(ms)}!`)
2539
+ */
2540
+ check(userId) {
2541
+ const last = this._last.get(userId);
2542
+ if (last === void 0) return 0;
2543
+ const duration = this._durations.get(userId) ?? this._defaultMs;
2544
+ const remaining = duration - (Date.now() - last);
2545
+ return remaining > 0 ? remaining : 0;
2546
+ }
2547
+ /**
2548
+ * Mark a user as having just used the command, starting their cooldown.
2549
+ * Optionally override the cooldown duration for this specific user.
2550
+ *
2551
+ * @example
2552
+ * cooldown.use(userId) // uses default duration
2553
+ * cooldown.use(userId, 10_000) // 10 second cooldown for this use
2554
+ */
2555
+ use(userId, durationMs) {
2556
+ this._last.set(userId, Date.now());
2557
+ if (durationMs !== void 0) this._durations.set(userId, durationMs);
2558
+ }
2559
+ /**
2560
+ * Immediately reset (clear) the cooldown for a user.
2561
+ *
2562
+ * @example
2563
+ * cooldown.reset(userId) // user can use the command again immediately
2564
+ */
2565
+ reset(userId) {
2566
+ this._last.delete(userId);
2567
+ this._durations.delete(userId);
2568
+ }
2569
+ /**
2570
+ * Reset all active cooldowns.
2571
+ */
2572
+ resetAll() {
2573
+ this._last.clear();
2574
+ this._durations.clear();
2575
+ }
2576
+ /**
2577
+ * Returns a list of all users currently on cooldown.
2578
+ *
2579
+ * @example
2580
+ * const active = cooldown.activeCooldowns()
2581
+ * // [{ userId: 'abc', remainingMs: 3200 }, ...]
2582
+ */
2583
+ activeCooldowns() {
2584
+ const result = [];
2585
+ for (const [userId] of this._last) {
2586
+ const ms = this.check(userId);
2587
+ if (ms > 0) result.push({ userId, remainingMs: ms });
2588
+ }
2589
+ return result;
2590
+ }
2591
+ /**
2592
+ * Format milliseconds into a human-readable string.
2593
+ *
2594
+ * @example
2595
+ * Cooldown.format(3_600_000) // '1h 0m 0s'
2596
+ * Cooldown.format(90_000) // '1m 30s'
2597
+ * Cooldown.format(4_000) // '4s'
1063
2598
  */
1064
- toJSON() {
1065
- return this._components.map((c) => c.toJSON());
2599
+ static format(ms) {
2600
+ const totalSecs = Math.ceil(ms / 1e3);
2601
+ const hours = Math.floor(totalSecs / 3600);
2602
+ const mins = Math.floor(totalSecs % 3600 / 60);
2603
+ const secs = totalSecs % 60;
2604
+ if (hours > 0) return `${hours}h ${mins}m ${secs}s`;
2605
+ if (mins > 0) return `${mins}m ${secs}s`;
2606
+ return `${secs}s`;
1066
2607
  }
1067
2608
  };
1068
-
1069
- // src/builders/ModalBuilder.ts
1070
- var ModalBuilder = class {
2609
+ var CooldownManager = class {
1071
2610
  constructor() {
1072
- this._title = "";
1073
- this._customId = "";
1074
- this._fields = [];
2611
+ this._map = /* @__PURE__ */ new Map();
1075
2612
  }
1076
- /** Title shown at the top of the modal dialog. */
1077
- setTitle(title) {
1078
- this._title = title;
1079
- return this;
2613
+ /**
2614
+ * Get or create a named cooldown.
2615
+ */
2616
+ get(name, durationMs = 3e3) {
2617
+ if (!this._map.has(name)) this._map.set(name, new Cooldown(durationMs));
2618
+ return this._map.get(name);
1080
2619
  }
1081
- /** Custom ID passed back with the `MODAL_SUBMIT` interaction. */
1082
- setCustomId(customId) {
1083
- this._customId = customId;
1084
- return this;
2620
+ /**
2621
+ * Check a named cooldown for a user.
2622
+ * Creates the cooldown if it doesn't exist yet.
2623
+ * Returns `0` if free, or remaining ms if on cooldown.
2624
+ *
2625
+ * @example
2626
+ * const ms = cooldowns.check('ban', userId, 30_000)
2627
+ */
2628
+ check(name, userId, durationMs = 3e3) {
2629
+ return this.get(name, durationMs).check(userId);
1085
2630
  }
1086
- /** Add a single text-input field. */
1087
- addField(field) {
1088
- this._fields.push("toJSON" in field ? field.toJSON() : field);
1089
- return this;
2631
+ /**
2632
+ * Mark a user as having used a named command.
2633
+ *
2634
+ * @example
2635
+ * cooldowns.use('ban', userId, 30_000)
2636
+ */
2637
+ use(name, userId, durationMs) {
2638
+ this.get(name, durationMs).use(userId, durationMs);
1090
2639
  }
1091
- /** Add multiple fields at once. */
1092
- addFields(...fields) {
1093
- for (const f of fields) this.addField(f);
1094
- return this;
2640
+ /**
2641
+ * Reset a user's cooldown on a named command.
2642
+ */
2643
+ reset(name, userId) {
2644
+ this._map.get(name)?.reset(userId);
1095
2645
  }
1096
- toJSON() {
1097
- if (!this._title) throw new Error("ModalBuilder: title is required \u2014 call .setTitle()");
1098
- if (!this._customId) throw new Error("ModalBuilder: customId is required \u2014 call .setCustomId()");
1099
- if (this._fields.length === 0) throw new Error("ModalBuilder: at least one field is required \u2014 call .addField()");
1100
- return { title: this._title, customId: this._customId, fields: [...this._fields] };
2646
+ /**
2647
+ * Reset all cooldowns for all commands.
2648
+ */
2649
+ resetAll() {
2650
+ for (const c of this._map.values()) c.resetAll();
1101
2651
  }
1102
2652
  };
1103
2653
 
1104
- // src/builders/TextInputBuilder.ts
1105
- var TextInputBuilder = class {
1106
- constructor() {
1107
- this._data = {
1108
- customId: "",
1109
- label: "",
1110
- type: "short"
1111
- };
2654
+ // src/utils/Logger.ts
2655
+ var Logger = class {
2656
+ /**
2657
+ * @param name - Name shown in square brackets before every message.
2658
+ * @param enableDebug - When `false` (default), `.debug()` calls are silent.
2659
+ */
2660
+ constructor(name, enableDebug = false) {
2661
+ this._prefix = name;
2662
+ this._debug = enableDebug;
1112
2663
  }
1113
- /** Unique ID for this field the key in `interaction.modalData` on submit. */
1114
- setCustomId(customId) {
1115
- this._data.customId = customId;
2664
+ /** Enable or disable debug output at runtime. */
2665
+ setDebug(enabled) {
2666
+ this._debug = enabled;
1116
2667
  return this;
1117
2668
  }
1118
- /** Label shown above the input inside the modal. */
1119
- setLabel(label) {
1120
- this._data.label = label;
1121
- return this;
2669
+ _timestamp() {
2670
+ return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
1122
2671
  }
1123
- /**
1124
- * Input style:
1125
- * - `'short'` single-line text input
1126
- * - `'paragraph'` multi-line textarea
1127
- */
1128
- setStyle(style) {
1129
- this._data.type = style;
1130
- return this;
2672
+ _fmt(level, color, ...args) {
2673
+ const ts = this._timestamp();
2674
+ const tag = `\x1B[90m${ts}\x1B[0m ${color}[${level}]\x1B[0m \x1B[36m[${this._prefix}]\x1B[0m`;
2675
+ return `${tag} ${args.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)).join(" ")}`;
1131
2676
  }
1132
- /** Greyed-out hint text shown when the field is empty. */
1133
- setPlaceholder(placeholder) {
1134
- this._data.placeholder = placeholder;
1135
- return this;
2677
+ /** General info message. */
2678
+ info(...args) {
2679
+ console.log(this._fmt("INFO ", "\x1B[34m", ...args));
1136
2680
  }
1137
- /** Whether the user must fill in this field before submitting. */
1138
- setRequired(required = true) {
1139
- this._data.required = required;
1140
- return this;
2681
+ /** Warning something unexpected but non-fatal. */
2682
+ warn(...args) {
2683
+ console.warn(this._fmt("WARN ", "\x1B[33m", ...args));
1141
2684
  }
1142
- /** Minimum number of characters required. */
1143
- setMinLength(min) {
1144
- this._data.minLength = min;
1145
- return this;
2685
+ /** Error something went wrong. */
2686
+ error(...args) {
2687
+ console.error(this._fmt("ERROR", "\x1B[31m", ...args));
1146
2688
  }
1147
- /** Maximum number of characters allowed. */
1148
- setMaxLength(max) {
1149
- this._data.maxLength = max;
1150
- return this;
2689
+ /** Success / positive confirmation. */
2690
+ success(...args) {
2691
+ console.log(this._fmt("OK ", "\x1B[32m", ...args));
1151
2692
  }
1152
- /** Pre-filled default value. */
1153
- setValue(value) {
1154
- this._data.value = value;
1155
- return this;
2693
+ /** Debug only printed when `enableDebug` is true. */
2694
+ debug(...args) {
2695
+ if (this._debug) console.log(this._fmt("DEBUG", "\x1B[35m", ...args));
1156
2696
  }
1157
- toJSON() {
1158
- return { ...this._data };
2697
+ /**
2698
+ * Log an incoming command interaction.
2699
+ *
2700
+ * @example
2701
+ * log.command('ping', interaction.userId)
2702
+ * // [MyBot] [CMD] /ping ← user:abc123
2703
+ */
2704
+ command(name, userId) {
2705
+ console.log(this._fmt("CMD ", "\x1B[36m", `/${name} \x1B[90m\u2190 user:${userId}`));
2706
+ }
2707
+ /**
2708
+ * Log a gateway event.
2709
+ *
2710
+ * @example
2711
+ * log.event('messageCreate')
2712
+ */
2713
+ event(type, extra = "") {
2714
+ console.log(this._fmt("EVT ", "\x1B[35m", type + (extra ? ` \x1B[90m${extra}` : "")));
2715
+ }
2716
+ /**
2717
+ * Log that a command handler threw an error.
2718
+ *
2719
+ * @example
2720
+ * log.commandError('ban', err)
2721
+ */
2722
+ commandError(name, err) {
2723
+ const msg = err instanceof Error ? err.message : String(err);
2724
+ console.error(this._fmt("ERROR", "\x1B[31m", `${name} threw: ${msg}`));
2725
+ if (err instanceof Error && err.stack) {
2726
+ console.error("\x1B[90m" + err.stack + "\x1B[0m");
2727
+ }
1159
2728
  }
1160
2729
  };
1161
2730
 
1162
- // src/builders/SlashCommandBuilder.ts
1163
- var SlashCommandOptionBuilder = class {
1164
- constructor(type) {
1165
- this._data = { name: "", description: "", type };
1166
- }
1167
- /** Internal option name (lowercase, no spaces). Shown after `/command ` in the client UI. */
1168
- setName(name) {
1169
- this._data.name = name;
1170
- return this;
2731
+ // src/utils/Paginator.ts
2732
+ var Paginator = class {
2733
+ constructor(fetchFn) {
2734
+ this.fetchFn = fetchFn;
2735
+ this._cursor = null;
2736
+ this._done = false;
2737
+ this._totalFetched = 0;
2738
+ }
2739
+ /** Whether all pages have been consumed. */
2740
+ get done() {
2741
+ return this._done;
2742
+ }
2743
+ /** Total number of items fetched so far (across all pages). */
2744
+ get totalFetched() {
2745
+ return this._totalFetched;
1171
2746
  }
1172
- /** Short human-readable description shown in the command picker. */
1173
- setDescription(description) {
1174
- this._data.description = description;
1175
- return this;
2747
+ /**
2748
+ * Fetch the next page of results.
2749
+ * Returns an empty array when there are no more pages.
2750
+ *
2751
+ * @example
2752
+ * const page1 = await paginator.fetchPage()
2753
+ * const page2 = await paginator.fetchPage()
2754
+ */
2755
+ async fetchPage() {
2756
+ if (this._done) return [];
2757
+ const { items, cursor } = await this.fetchFn(this._cursor);
2758
+ this._totalFetched += items.length;
2759
+ this._cursor = cursor;
2760
+ if (!cursor || items.length === 0) this._done = true;
2761
+ return items;
1176
2762
  }
1177
- /** Whether users must supply this option before sending the command. */
1178
- setRequired(required = true) {
1179
- this._data.required = required;
1180
- return this;
2763
+ /**
2764
+ * Fetch **all** remaining pages and return a flat array.
2765
+ *
2766
+ * ⚠️ Use with caution on very large collections.
2767
+ *
2768
+ * @example
2769
+ * const allMembers = await paginator.fetchAll()
2770
+ */
2771
+ async fetchAll() {
2772
+ const all = [];
2773
+ while (!this._done) {
2774
+ const page = await this.fetchPage();
2775
+ all.push(...page);
2776
+ }
2777
+ return all;
1181
2778
  }
1182
2779
  /**
1183
- * Restrict the option to specific values.
1184
- * The picker will show these as autocomplete suggestions.
2780
+ * Fetch up to `n` items total (across however many pages are needed).
2781
+ *
2782
+ * @example
2783
+ * const first100 = await paginator.fetchN(100)
1185
2784
  */
1186
- addChoice(name, value) {
1187
- if (!this._data.choices) this._data.choices = [];
1188
- this._data.choices.push({ name, value });
1189
- return this;
2785
+ async fetchN(n) {
2786
+ const result = [];
2787
+ while (!this._done && result.length < n) {
2788
+ const page = await this.fetchPage();
2789
+ result.push(...page.slice(0, n - result.length));
2790
+ }
2791
+ return result;
1190
2792
  }
1191
- /** Set all allowed choices at once. */
1192
- setChoices(choices) {
1193
- this._data.choices = [...choices];
1194
- return this;
2793
+ /**
2794
+ * Async-iterate over every item, one page at a time.
2795
+ *
2796
+ * @example
2797
+ * for await (const msg of paginator) {
2798
+ * process(msg)
2799
+ * }
2800
+ */
2801
+ async *[Symbol.asyncIterator]() {
2802
+ while (!this._done) {
2803
+ const page = await this.fetchPage();
2804
+ for (const item of page) yield item;
2805
+ }
1195
2806
  }
1196
- toJSON() {
1197
- return { ...this._data };
2807
+ /**
2808
+ * Reset the paginator so you can iterate from the beginning again.
2809
+ *
2810
+ * @example
2811
+ * await paginator.fetchAll()
2812
+ * paginator.reset()
2813
+ * const againFirst = await paginator.fetchPage()
2814
+ */
2815
+ reset() {
2816
+ this._cursor = null;
2817
+ this._done = false;
2818
+ this._totalFetched = 0;
1198
2819
  }
1199
2820
  };
1200
- var SlashCommandBuilder = class {
1201
- constructor() {
1202
- this._data = {
1203
- name: "",
1204
- description: "",
1205
- options: []
1206
- };
2821
+
2822
+ // src/utils/PermissionsBitfield.ts
2823
+ var Permissions = {
2824
+ /** Read messages in channels. */
2825
+ MESSAGES_READ: "messages.read",
2826
+ /** Send messages in channels. */
2827
+ MESSAGES_WRITE: "messages.write",
2828
+ /** Edit / delete any message (not just the bot's own). */
2829
+ MESSAGES_MANAGE: "messages.manage",
2830
+ /** Create, edit and delete channels. */
2831
+ CHANNELS_MANAGE: "channels.manage",
2832
+ /** Kick members from the server. */
2833
+ MEMBERS_KICK: "members.kick",
2834
+ /** Ban and unban members from the server. */
2835
+ MEMBERS_BAN: "members.ban",
2836
+ /** Assign and remove roles on members. */
2837
+ MEMBERS_ROLES: "members.roles",
2838
+ /** Edit server settings. */
2839
+ SERVERS_MANAGE: "servers.manage"
2840
+ };
2841
+ var _PermissionsBitfield = class _PermissionsBitfield {
2842
+ constructor(perms = {}) {
2843
+ this._perms = { ...perms };
1207
2844
  }
1208
- /** Command name — lowercase, no spaces (e.g. `'ban'`, `'server-info'`). */
1209
- setName(name) {
1210
- this._data.name = name;
1211
- return this;
2845
+ // ─── Checking ──────────────────────────────────────────────────────────────
2846
+ /**
2847
+ * Returns `true` if the given permission is explicitly granted.
2848
+ *
2849
+ * @example
2850
+ * if (!perms.has(Permissions.MESSAGES_WRITE)) {
2851
+ * throw new Error('Bot cannot write messages here.')
2852
+ * }
2853
+ */
2854
+ has(permission) {
2855
+ return this._perms[permission] === true;
1212
2856
  }
1213
- /** Short description shown in the command picker UI. */
1214
- setDescription(description) {
1215
- this._data.description = description;
1216
- return this;
2857
+ /**
2858
+ * Returns `true` if **all** listed permissions are granted.
2859
+ *
2860
+ * @example
2861
+ * perms.hasAll('messages.read', 'messages.write') // true
2862
+ */
2863
+ hasAll(...permissions) {
2864
+ return permissions.every((p) => this.has(p));
1217
2865
  }
1218
- /** Add a text (string) option. */
1219
- addStringOption(fn) {
1220
- this._data.options.push(fn(new SlashCommandOptionBuilder("STRING")).toJSON());
1221
- return this;
2866
+ /**
2867
+ * Returns `true` if **at least one** of the listed permissions is granted.
2868
+ *
2869
+ * @example
2870
+ * perms.hasAny('channels.manage', 'servers.manage') // true if either is set
2871
+ */
2872
+ hasAny(...permissions) {
2873
+ return permissions.some((p) => this.has(p));
1222
2874
  }
1223
- /** Add an integer (whole number) option. */
1224
- addIntegerOption(fn) {
1225
- this._data.options.push(fn(new SlashCommandOptionBuilder("INTEGER")).toJSON());
1226
- return this;
2875
+ /**
2876
+ * Returns the list of permissions that are **not** granted.
2877
+ *
2878
+ * @example
2879
+ * const missing = perms.missing('messages.write', 'members.kick')
2880
+ * if (missing.length) throw new Error(`Missing: ${missing.join(', ')}`)
2881
+ */
2882
+ missing(...permissions) {
2883
+ return permissions.filter((p) => !this.has(p));
1227
2884
  }
1228
- /** Add a boolean (true/false) option. */
1229
- addBooleanOption(fn) {
1230
- this._data.options.push(fn(new SlashCommandOptionBuilder("BOOLEAN")).toJSON());
1231
- return this;
2885
+ // ─── Building ──────────────────────────────────────────────────────────────
2886
+ /**
2887
+ * Return a **new** `PermissionsBitfield` with the given permission granted.
2888
+ *
2889
+ * @example
2890
+ * const updated = perms.grant('channels.manage')
2891
+ */
2892
+ grant(...permissions) {
2893
+ const next = { ...this._perms };
2894
+ for (const p of permissions) next[p] = true;
2895
+ return new _PermissionsBitfield(next);
1232
2896
  }
1233
- /** Add a user-mention option (returns a user ID string). */
1234
- addUserOption(fn) {
1235
- this._data.options.push(fn(new SlashCommandOptionBuilder("USER")).toJSON());
1236
- return this;
2897
+ /**
2898
+ * Return a **new** `PermissionsBitfield` with the given permission denied.
2899
+ *
2900
+ * @example
2901
+ * const restricted = perms.deny('members.kick')
2902
+ */
2903
+ deny(...permissions) {
2904
+ const next = { ...this._perms };
2905
+ for (const p of permissions) next[p] = false;
2906
+ return new _PermissionsBitfield(next);
1237
2907
  }
1238
- /** Add a channel-mention option (returns a channel ID string). */
1239
- addChannelOption(fn) {
1240
- this._data.options.push(fn(new SlashCommandOptionBuilder("CHANNEL")).toJSON());
1241
- return this;
2908
+ /**
2909
+ * Merge another record or `PermissionsBitfield` on top of this one.
2910
+ * The incoming values **override** any existing ones.
2911
+ *
2912
+ * @example
2913
+ * const merged = serverPerms.merge(channelOverrides)
2914
+ */
2915
+ merge(other) {
2916
+ const otherRecord = other instanceof _PermissionsBitfield ? other.toRecord() : other;
2917
+ return new _PermissionsBitfield({ ...this._perms, ...otherRecord });
1242
2918
  }
1243
- /** Add a role-mention option (returns a role ID string). */
1244
- addRoleOption(fn) {
1245
- this._data.options.push(fn(new SlashCommandOptionBuilder("ROLE")).toJSON());
1246
- return this;
2919
+ // ─── Inspection ────────────────────────────────────────────────────────────
2920
+ /**
2921
+ * Returns an array of the permission keys that are **currently granted**.
2922
+ *
2923
+ * @example
2924
+ * perms.toArray() // ['messages.read', 'messages.write']
2925
+ */
2926
+ toArray() {
2927
+ return Object.entries(this._perms).filter(([, v]) => v).map(([k]) => k);
1247
2928
  }
1248
- toJSON() {
1249
- if (!this._data.name) throw new Error("SlashCommandBuilder: name is required \u2014 call .setName()");
1250
- if (!this._data.description) throw new Error("SlashCommandBuilder: description is required \u2014 call .setDescription()");
1251
- return { ...this._data, options: [...this._data.options ?? []] };
2929
+ /**
2930
+ * Returns the raw `Record<string, boolean>` map.
2931
+ */
2932
+ toRecord() {
2933
+ return { ...this._perms };
2934
+ }
2935
+ /**
2936
+ * Pretty-print the granted permissions.
2937
+ *
2938
+ * @example
2939
+ * console.log(String(perms)) // 'PermissionsBitfield[messages.read, messages.write]'
2940
+ */
2941
+ toString() {
2942
+ const granted = this.toArray();
2943
+ return granted.length > 0 ? `PermissionsBitfield[${granted.join(", ")}]` : "PermissionsBitfield[none]";
2944
+ }
2945
+ // ─── Static helpers ─────────────────────────────────────────────────────────
2946
+ /** Create a `PermissionsBitfield` from an existing record. */
2947
+ static from(perms) {
2948
+ return new _PermissionsBitfield(perms);
1252
2949
  }
1253
2950
  };
2951
+ /** A `PermissionsBitfield` with all known permissions granted. */
2952
+ _PermissionsBitfield.ALL = new _PermissionsBitfield({
2953
+ [Permissions.MESSAGES_READ]: true,
2954
+ [Permissions.MESSAGES_WRITE]: true,
2955
+ [Permissions.MESSAGES_MANAGE]: true,
2956
+ [Permissions.CHANNELS_MANAGE]: true,
2957
+ [Permissions.MEMBERS_KICK]: true,
2958
+ [Permissions.MEMBERS_BAN]: true,
2959
+ [Permissions.MEMBERS_ROLES]: true,
2960
+ [Permissions.SERVERS_MANAGE]: true
2961
+ });
2962
+ /** A `PermissionsBitfield` with no permissions granted. */
2963
+ _PermissionsBitfield.NONE = new _PermissionsBitfield({});
2964
+ var PermissionsBitfield = _PermissionsBitfield;
2965
+
2966
+ // src/utils/time.ts
2967
+ function sleep(ms) {
2968
+ return new Promise((resolve) => setTimeout(resolve, ms));
2969
+ }
2970
+ function withTimeout(promise, ms, message) {
2971
+ return new Promise((resolve, reject) => {
2972
+ const id = setTimeout(() => {
2973
+ reject(new Error(message ?? `[nova-bot-sdk] Operation timed out after ${ms}ms`));
2974
+ }, ms);
2975
+ promise.then(
2976
+ (v) => {
2977
+ clearTimeout(id);
2978
+ resolve(v);
2979
+ },
2980
+ (e) => {
2981
+ clearTimeout(id);
2982
+ reject(e);
2983
+ }
2984
+ );
2985
+ });
2986
+ }
2987
+ var SECOND = 1e3;
2988
+ var MINUTE = 60 * SECOND;
2989
+ var HOUR = 60 * MINUTE;
2990
+ var DAY = 24 * HOUR;
2991
+ var WEEK = 7 * DAY;
2992
+ function formatDuration(ms) {
2993
+ if (ms < SECOND) return "0s";
2994
+ const weeks = Math.floor(ms / WEEK);
2995
+ const days = Math.floor(ms % WEEK / DAY);
2996
+ const hours = Math.floor(ms % DAY / HOUR);
2997
+ const minutes = Math.floor(ms % HOUR / MINUTE);
2998
+ const seconds = Math.floor(ms % MINUTE / SECOND);
2999
+ const parts = [];
3000
+ if (weeks) parts.push(`${weeks}w`);
3001
+ if (days) parts.push(`${days}d`);
3002
+ if (hours) parts.push(`${hours}h`);
3003
+ if (minutes) parts.push(`${minutes}m`);
3004
+ if (seconds) parts.push(`${seconds}s`);
3005
+ return parts.join(" ");
3006
+ }
3007
+ function formatRelative(timestamp) {
3008
+ const ts = timestamp instanceof Date ? timestamp.getTime() : typeof timestamp === "string" ? new Date(timestamp).getTime() : timestamp;
3009
+ const diff = Date.now() - ts;
3010
+ const abs = Math.abs(diff);
3011
+ const past = diff > 0;
3012
+ const fmt = (n, unit) => past ? `${n} ${unit}${n !== 1 ? "s" : ""} ago` : `in ${n} ${unit}${n !== 1 ? "s" : ""}`;
3013
+ if (abs < 5e3) return "just now";
3014
+ if (abs < MINUTE) return fmt(Math.round(abs / SECOND), "second");
3015
+ if (abs < 2 * MINUTE) return past ? "a minute ago" : "in a minute";
3016
+ if (abs < HOUR) return fmt(Math.round(abs / MINUTE), "minute");
3017
+ if (abs < 2 * HOUR) return past ? "an hour ago" : "in an hour";
3018
+ if (abs < DAY) return fmt(Math.round(abs / HOUR), "hour");
3019
+ if (abs < 2 * DAY) return past ? "yesterday" : "tomorrow";
3020
+ if (abs < WEEK) return fmt(Math.round(abs / DAY), "day");
3021
+ if (abs < 4 * WEEK) return fmt(Math.round(abs / WEEK), "week");
3022
+ return new Date(ts).toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
3023
+ }
3024
+ function parseTimestamp(value) {
3025
+ if (value == null) return null;
3026
+ if (value instanceof Date) return isNaN(value.getTime()) ? null : value;
3027
+ const d = new Date(value);
3028
+ return isNaN(d.getTime()) ? null : d;
3029
+ }
3030
+ function countdown(target) {
3031
+ const targetMs = target instanceof Date ? target.getTime() : typeof target === "string" ? new Date(target).getTime() : target;
3032
+ const total = Math.max(0, targetMs - Date.now());
3033
+ return {
3034
+ total,
3035
+ weeks: Math.floor(total / WEEK),
3036
+ days: Math.floor(total % WEEK / DAY),
3037
+ hours: Math.floor(total % DAY / HOUR),
3038
+ minutes: Math.floor(total % HOUR / MINUTE),
3039
+ seconds: Math.floor(total % MINUTE / SECOND)
3040
+ };
3041
+ }
1254
3042
  // Annotate the CommonJS export names for ESM import in node:
1255
3043
  0 && (module.exports = {
1256
3044
  ActionRowBuilder,
1257
3045
  ButtonBuilder,
3046
+ ChannelsAPI,
3047
+ Collection,
1258
3048
  CommandsAPI,
3049
+ Cooldown,
3050
+ CooldownManager,
1259
3051
  EmbedBuilder,
1260
3052
  HttpClient,
1261
3053
  InteractionOptions,
1262
3054
  InteractionsAPI,
3055
+ Logger,
1263
3056
  MembersAPI,
3057
+ MessageBuilder,
1264
3058
  MessagesAPI,
1265
3059
  ModalBuilder,
3060
+ NovaChannel,
1266
3061
  NovaClient,
1267
3062
  NovaInteraction,
3063
+ NovaMember,
3064
+ NovaMessage,
3065
+ Paginator,
3066
+ Permissions,
1268
3067
  PermissionsAPI,
3068
+ PermissionsBitfield,
3069
+ PollBuilder,
3070
+ ReactionsAPI,
1269
3071
  SelectMenuBuilder,
1270
3072
  ServersAPI,
1271
3073
  SlashCommandBuilder,
1272
3074
  SlashCommandOptionBuilder,
1273
- TextInputBuilder
3075
+ TextInputBuilder,
3076
+ countdown,
3077
+ formatDuration,
3078
+ formatRelative,
3079
+ parseTimestamp,
3080
+ sleep,
3081
+ withTimeout
1274
3082
  });