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