A powerful and extendable Discord bot, with it's own module system :3 thevoid.cafe/projects/voidy

✨ Implement Lifecycle events and restructure some client code

+90 -14
+11 -2
src/core/Handler.ts
··· 1 + import type { ChatInputCommandInteraction } from "discord.js"; 2 + import type { VoidyClient } from "./VoidyClient"; 3 + 1 4 interface IHandler<T extends object> { 2 - invoke: (data: T) => void 5 + invoke: (data: ChatInputCommandInteraction) => void 3 6 } 4 7 5 8 export abstract class Handler<T extends object> implements IHandler<T> { 6 - public abstract invoke(data: T): void 9 + protected client: VoidyClient; 10 + 11 + public constructor(client: VoidyClient) { 12 + this.client = client; 13 + } 14 + 15 + public abstract invoke(data: ChatInputCommandInteraction): void 7 16 }
+30
src/core/Lifecycle.ts
··· 1 + export enum LifecycleEvents { 2 + // Registries 3 + RegistryPreCollect = "registry::preCollect", 4 + RegistryPostCollect = "registry::postCollect", 5 + } 6 + 7 + type LifecycleEventCallback = () => void; 8 + 9 + export class Lifecycle { 10 + public static subscribers = new Map<string, Array<LifecycleEventCallback>>(); 11 + 12 + public static subscribe(event: LifecycleEvents, callback: LifecycleEventCallback): void { 13 + const subscribers = this.subscribers.get(event); 14 + if (!subscribers) { 15 + this.subscribers.set(event, [callback]); 16 + return; 17 + } 18 + 19 + this.subscribers.set(event, subscribers.concat([callback])); 20 + } 21 + 22 + public static notify(event: LifecycleEvents) { 23 + const subscribers = this.subscribers.get(event); 24 + if (!subscribers) return; 25 + 26 + for (const subscriber of subscribers) { 27 + subscriber(); 28 + } 29 + } 30 + }
+9 -2
src/core/Registry.ts
··· 1 1 import { CommandLoader, type Command } from "../loaders/CommandLoader" 2 2 import { EventLoader, type Event } from "../loaders/EventLoader" 3 3 import { ModuleLoader, type Module } from "../loaders/ModuleLoader" 4 + import { Lifecycle, LifecycleEvents } from "./Lifecycle" 4 5 5 6 export interface IRegistry { 6 7 dataSource: string ··· 12 13 collect: () => Promise<void> 13 14 prepare: () => Promise<void> 14 15 activate: () => Promise<void> 15 - unload: () => Promise<void> 16 + deactivate: () => Promise<void> 16 17 } 17 18 18 19 export class Registry implements IRegistry { ··· 27 28 } 28 29 29 30 public async collect() { 31 + Lifecycle.notify(LifecycleEvents.RegistryPreCollect); 32 + 30 33 // Collect modules and bundle their JSON contents into an array. 31 34 const moduleLoader = new ModuleLoader(this.dataSource); 32 35 const modules = (await moduleLoader.collect()).getJSON(); 33 36 34 37 // Merge all modules into the store. 35 38 this.modules = this.modules.concat(modules); 39 + 40 + Lifecycle.notify(LifecycleEvents.RegistryPostCollect); 36 41 } 37 42 38 43 public async prepare() { ··· 54 59 this.active = true; 55 60 } 56 61 57 - public async unload() { } 62 + public async deactivate() { 63 + this.active = false; 64 + } 58 65 }
+25 -5
src/core/VoidyClient.ts
··· 21 21 } 22 22 23 23 public async start(token: string) { 24 + // 1. Prepare commands and events, without registering them 25 + const { commands, events } = await this.initialize(); 26 + 27 + // 2. Register event listeners 28 + await this.registerEventHandlers(events); 29 + 30 + // 3. Log in 24 31 await this.login(token); 25 - await this.initialize(); 32 + 33 + // 4. Register/Publish commands 34 + await this.registerCommands(commands); 26 35 } 27 36 28 37 private async initialize() { 29 38 for (const registry of this.registries) { 39 + // 1. Collecting required registry data 40 + console.info(`[Voidy] Collecting registry data: ${registry.dataSource}`); 30 41 await registry.collect(); 42 + 43 + // 2. Preparing collected registry data for activation 44 + console.info(`[Voidy] Preparing registry data: ${registry.dataSource}`); 31 45 await registry.prepare(); 46 + 47 + // 3. Activating registry 48 + console.info(`[Voidy] Activating registry: ${registry.dataSource}`); 32 49 await registry.activate(); 33 50 } 34 51 35 52 const activeRegistries = this.registries 36 53 .filter(registry => registry.active); 37 54 38 - const allEvents = activeRegistries 55 + const events = activeRegistries 39 56 .flatMap(registry => registry.events); 40 57 41 - const allCommands = activeRegistries 58 + const commands = activeRegistries 42 59 .flatMap(registry => registry.commands) 43 60 .flatMap(commands => commands.data.toJSON()) 44 61 45 - await this.registerEventHandlers(allEvents); 46 - await this.registerCommands(allCommands); 62 + return { commands, events }; 47 63 } 48 64 49 65 private async registerEventHandlers(events: Event[]) { 66 + console.log(`[Voidy] Registering ${events.length} event listeners: ${events.map(event => event.name).join(", ")}`); 67 + 50 68 for (const event of events) { 51 69 const execute = (...args: unknown[]) => event.execute(this, ...args); 52 70 ··· 57 75 58 76 // @Todo: fix this type mess, if possible 59 77 private async registerCommands(commands: (RESTPostAPIChatInputApplicationCommandsJSONBody | APIApplicationCommandSubcommandOption | APIApplicationCommandSubcommandGroupOption)[]) { 78 + console.info(`[Voidy] Registering ${commands.length} commands: ${commands.map(command => command.name).join(", ")}`); 79 + 60 80 await this.application?.commands.set(commands as ApplicationCommandDataResolvable[]); 61 81 } 62 82 }
+12 -1
src/handlers/CommandHandler.ts
··· 1 + import type { ChatInputCommandInteraction } from "discord.js"; 1 2 import { Handler } from "../core/Handler"; 2 3 import type { Command } from "../loaders/CommandLoader"; 4 + import type { VoidyClient } from "../core/VoidyClient"; 3 5 4 6 export class CommandHandler extends Handler<Command> { 5 - public invoke(data: Command): void { 7 + public constructor( 8 + client: VoidyClient 9 + ) { 10 + super(client); 11 + } 12 + 13 + public invoke(data: ChatInputCommandInteraction): void { 6 14 console.log(data); 15 + // @Todo: implement invoke method, which fetches command information from registries, based on the command name 16 + // 17 + // @Todo: consider whether we actually need handlers as separate classes, or if we can just give the client a handle method. 7 18 } 8 19 }
-4
src/index.ts
··· 1 1 import { GatewayIntentBits } from "discord.js" 2 2 import { VoidyClient } from "./core/VoidyClient" 3 - // import { EventLoader } from "./loaders/EventLoader"; 4 - // import { join } from "node:path"; 5 3 6 4 // Client initialization with intents and stuff... 7 5 const client = new VoidyClient({ 8 6 intents: [GatewayIntentBits.Guilds], 9 7 }) 10 - 11 - 12 8 13 9 // Token validation and client start 14 10 if (!Bun.env.BOT_TOKEN) throw new Error("[Voidy] Missing bot token");
+3
src/modules/core/events/interactionCreate.ts
··· 1 1 import { Events, type Interaction } from "discord.js"; 2 2 import type { Event } from "../../../loaders/EventLoader"; 3 3 import type { VoidyClient } from "../../../core/VoidyClient"; 4 + import { CommandHandler } from "../../../handlers/CommandHandler"; 4 5 5 6 export default { 6 7 name: Events.InteractionCreate, ··· 8 9 if (!interaction.isChatInputCommand() || !interaction.isCommand()) return null; 9 10 10 11 console.log(interaction.commandName); 12 + 13 + 11 14 } 12 15 } as Event