A powerful and extendable Discord bot, with it's own module system :3
thevoid.cafe/projects/voidy
1//===============================================
2// Imports
3//===============================================
4import {
5 type ClientOptions,
6 SlashCommandSubcommandGroupBuilder,
7 SlashCommandSubcommandBuilder,
8 SlashCommandBuilder,
9 Client,
10 Events,
11} from "discord.js";
12import { ModuleManager, type CacheMap } from "./ModuleManager";
13import { Logger } from "./Logger";
14import type { Command } from "./types/Command";
15import type { Button } from "./types/Button";
16import type { Event } from "./types/Event";
17
18//===============================================
19// ClientOptions Override
20//===============================================
21export interface VoidyClientOptions extends ClientOptions {
22 developers?: string[]; // List of developer user ids
23 logChannelId?: string; // ID of the channel to log events to
24}
25
26//===============================================
27// VoidyClient Implementation
28//===============================================
29export class VoidyClient extends Client {
30 public moduleManager = new ModuleManager();
31 public developers: string[] = [];
32 public logger: Logger = new Logger(this);
33
34 public constructor(options: VoidyClientOptions) {
35 super(options);
36
37 // Set developers, if provided.
38 if (options.developers) {
39 this.developers = options.developers;
40 }
41
42 // Inject channel ID into logger, if provided.
43 if (options.logChannelId) {
44 this.logger.setChannelId(options.logChannelId);
45 }
46 }
47
48 /**
49 * Launches the bot
50 * @param token - The Discord application bot token.
51 * @param modulesPath - Where the bot should search for modules.
52 */
53 public async start(token: string, modulesPath: string) {
54 // Load modules and register events
55 await this.moduleManager.loadModules(modulesPath);
56 await this.registerEvents();
57
58 // Register commands on ready event
59 this.on(Events.ClientReady, this.registerCommands);
60
61 // Login using the bot token
62 await this.login(token);
63 }
64
65 /**
66 * Registers all cached events
67 * @param events
68 */
69 private async registerEvents() {
70 const events = this.moduleManager.events;
71
72 for (const [_id, event] of events) {
73 const execute = (...args: unknown[]) => event.execute(this, ...args);
74
75 if (event.once) this.once(event.name, execute);
76 else this.on(event.name, execute);
77 }
78 }
79
80 /**
81 * Registers all provided commands globally
82 * @param commands
83 */
84 private async registerCommands(): Promise<void> {
85 const topLevelCommands = new Map<string, SlashCommandBuilder>();
86
87 for (const cmd of this.moduleManager.commands.values()) {
88 const parts = cmd.id.split("."); // ["music", "set", "channel"]
89 const command = parts[0];
90 const subcommand = parts[1];
91 const subgroupcommand = parts[2];
92
93 if (!command) continue;
94
95 // Ensure top-level builder exists
96 if (!topLevelCommands.has(command)) {
97 const topCommand =
98 (this.moduleManager.commands.get(command)?.data as SlashCommandBuilder) ??
99 new SlashCommandBuilder().setName(command).setDescription("...");
100
101 topLevelCommands.set(command, topCommand);
102 }
103
104 const parent = topLevelCommands.get(command)!;
105
106 if (subcommand && !subgroupcommand) {
107 // It's a subcommand
108 parent.addSubcommand(cmd.data as SlashCommandSubcommandBuilder);
109 } else if (subcommand && subgroupcommand) {
110 // It's a subgroup command
111 let group = parent.options.find(
112 (o): o is SlashCommandSubcommandGroupBuilder =>
113 o instanceof SlashCommandSubcommandGroupBuilder && o.name === subcommand,
114 );
115
116 if (!group) {
117 group = new SlashCommandSubcommandGroupBuilder()
118 .setName(subcommand)
119 .setDescription("...");
120 parent.addSubcommandGroup(group);
121 }
122
123 group.addSubcommand(cmd.data as SlashCommandSubcommandBuilder);
124 }
125 }
126
127 // Finally convert assembled top-level commands to JSON and register them
128 await this.application?.commands.set([...topLevelCommands.values()].map((c) => c.toJSON()));
129 }
130
131 /**
132 * Returns all cached commands
133 */
134 get commands(): CacheMap<Command> {
135 return this.moduleManager.commands;
136 }
137
138 /**
139 * Returns all cached events
140 */
141 get events(): CacheMap<Event> {
142 return this.moduleManager.events;
143 }
144
145 /**
146 * Returns all cached buttons
147 */
148 get buttons(): CacheMap<Button> {
149 return this.moduleManager.buttons;
150 }
151}