Yet another Fluxer bot built with TypeScript and Bun
at feat/sqlite 99 lines 3.3 kB view raw
1import type { Message } from "@fluxerjs/core"; 2import { COMMAND_META_KEY, type CommandMeta } from "@/decorators/command"; 3import { GUARDS_KEY, type Guard } from "@/decorators/guards"; 4import { parseArgs } from "@/args"; 5import type { BotContext, ICommand } from "@/types"; 6import type { CommandCtor } from "@/types"; 7import { Logger } from "tslog"; 8 9interface FileRecord { 10 filePath: string; 11 triggers: string[]; 12} 13 14type AnyCommand = ICommand<any>; 15 16export class CommandRegistry { 17 private map = new Map<string, AnyCommand>(); 18 private guards = new Map<string, Guard[]>(); 19 private fileSources = new Map<string, FileRecord>(); 20 private logger: Logger<CommandRegistry>; 21 22 constructor() { 23 this.logger = new Logger({ 24 type: "pretty", 25 name: "CommandRegistry", 26 }); 27 } 28 29 register(prefix: string, filePath: string | null, ...ctors: CommandCtor[]): void { 30 for (const ctor of ctors) { 31 try { 32 const meta: CommandMeta = Reflect.getMetadata(COMMAND_META_KEY, ctor); 33 // Checks if command class has @Command decorators 34 if (!meta) throw new Error(`${ctor.name} has missing @Command decorators`); 35 36 const instance = new ctor(); 37 const instanceGuards: Guard[] = Reflect.getMetadata(GUARDS_KEY, ctor) ?? []; 38 const triggers = [meta.name, ...(meta.aliases ?? [])].map((n) => `${prefix}${n}`); 39 40 for (const trigger of triggers) { 41 this.map.set(trigger, instance); 42 this.guards.set(trigger, instanceGuards); 43 } 44 45 if (filePath) { 46 this.fileSources.set(meta.name, { filePath, triggers }); 47 } 48 } catch (e) { 49 if (e instanceof Error) 50 this.logger.error(`Something went wrong while registring: ${e.message}`); 51 } 52 } 53 } 54 55 unregisterFile(filePath: string): void { 56 for (const [name, record] of this.fileSources) { 57 if (record.filePath === filePath) { 58 for (const trigger of record.triggers) { 59 this.map.delete(trigger); 60 this.guards.delete(trigger); 61 } 62 this.fileSources.delete(name); 63 } 64 } 65 } 66 67 async handle(ctx: BotContext, prefix: string, message: Message): Promise<void> { 68 if (!message.content.startsWith(prefix)) return; 69 70 try { 71 const [rawCmd, ...args] = message.content.slice(prefix.length).trim().split(/\s+/); 72 const key = `${prefix}${rawCmd?.toLowerCase()}`; 73 74 const command = this.map.get(key); 75 if (!command) return; 76 77 const guards = this.guards.get(key) ?? []; 78 for (const guard of guards) { 79 if (!(await guard(message))) return; 80 } 81 82 // Parse raw string args through the command's schema if it has one, 83 // otherwise pass the raw array cast to the expected type. 84 const parsedArgs = command.args ? await parseArgs(command.args, args) : ({} as never); 85 86 this.logger.info( 87 `Executing ${rawCmd} ${args ? `with arguments: ${args.join(", ")}` : "without arguments"}`, 88 ); 89 await command.execute(ctx, message, parsedArgs); 90 } catch (err) { 91 if (err instanceof Error) { 92 this.logger.error(`Something went wrong while executing this command: ${err.message}`); 93 message.reply({ 94 content: `Something went wrong while executing this command:\n\`\`\`\n${err.message}\n\`\`\``, 95 }); 96 } 97 } 98 } 99}