Yet another Fluxer bot built with TypeScript and Bun
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}