A powerful and extendable Discord bot, with it's own module system :3 thevoid.cafe/projects/voidy
at develop 4.5 kB view raw
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}