Bläddra i källkod

Migrate command-related code to CommandManager

ghorsington 4 år sedan
förälder
incheckning
b7001de3c2
3 ändrade filer med 131 tillägg och 110 borttagningar
  1. 114 0
      bot/src/command_manager.ts
  2. 3 3
      bot/src/commands/help.ts
  3. 14 107
      bot/src/main.ts

+ 114 - 0
bot/src/command_manager.ts

@@ -0,0 +1,114 @@
+import path from "path";
+import fs from "fs";
+import { isCommandSet, ICommand, IBotCommand, ActionType, BotAction } from "./model/command";
+import { getNumberEnums, assertOk } from "./util";
+import { Message } from "discord.js";
+
+interface IDocumentationData {
+    name: string;
+    doc?: string;
+    example?: string;
+    auth: boolean;
+}
+
+type BotEventCollection = { [event in ActionType]: BotAction[] };
+
+export class CommandManager {
+    private commandSets: ICommand[] = [];
+    private botCommands: IBotCommand[] = [];
+    private botEvents: BotEventCollection = getNumberEnums(ActionType).reduce((p, c) => { p[c as ActionType] = []; return p; }, {} as BotEventCollection);
+    private startActions: Array<() => void | Promise<void>> = [];
+
+    constructor(private cmdPath: string) {
+        this.init();
+    }
+
+    private init(): void {
+        const files = fs.readdirSync(this.cmdPath);
+
+        for (const file of files) {
+            const ext = path.extname(file);
+            if (ext != ".js")
+                continue;
+
+            // eslint-disable-next-line @typescript-eslint/no-var-requires
+            this.loadCommand(require(path.resolve(this.cmdPath, file)));
+        }
+    }
+
+    private loadCommand(mod: Record<string, unknown>) {
+        for (const i in mod) {
+            if (!Object.prototype.hasOwnProperty.call(mod, i))
+                continue;
+    
+            const commandClass = mod[i] as unknown;
+            // Ensure this is indeed a command class
+            if (!isCommandSet(commandClass))
+                continue;
+    
+            const cmd = new commandClass();
+            this.commandSets.push(cmd);
+    
+            if (cmd._botCommands)
+                this.botCommands.push(...cmd._botCommands.map(c => ({ ...c, action: c.action.bind(cmd) })));
+    
+            if (cmd._botEvents)
+                for (const [i, event] of Object.entries(cmd._botEvents)) {
+                    this.botEvents[+i as ActionType].push((event as BotAction).bind(cmd));
+                }
+    
+            if (cmd.onStart)
+                this.startActions.push(cmd.onStart.bind(cmd));
+        }
+    }
+
+    get documentation(): IDocumentationData[] {
+        return this.botCommands.filter(m => m.documentation !== undefined).map(m => ({
+            name: m.pattern.toString(),
+            doc: m.documentation?.description,
+            example: m.documentation?.example,
+            auth: m.auth || false
+        }));
+    }
+
+    async onStart(): Promise<void> {
+        for (let i = 0; i < this.startActions.length; i++) {
+            const action = this.startActions[i];
+            const val = action();
+            if (val instanceof Promise)
+                await assertOk(val);
+        }
+    }
+
+    async trigger(type: ActionType, ...params: unknown[]): Promise<boolean> {
+        let actionDone = false;
+        const actions = this.botEvents[type];
+        for (let i = 0; i < actions.length; i++) {
+            const action = actions[i] as (...args: unknown[]) => boolean | Promise<boolean>;
+            const actionResult = action(actionDone, ...params);
+            if (actionResult instanceof Promise)
+                actionDone = (await assertOk(actionResult)) || actionDone;
+            else
+                actionDone = actionResult || actionDone;
+        }
+        return actionDone;
+    }
+
+    runCommand(m: Message, content: string): boolean {
+        const lowerCaseContent = content.toLowerCase();
+        for (const c of this.botCommands) {
+            if (typeof (c.pattern) == "string" && lowerCaseContent.startsWith(c.pattern)) {
+                c.action(m, content);
+                return true;
+            }
+            else if (c.pattern instanceof RegExp) {
+                const result = c.pattern.exec(content);
+                if (result != null) {
+                    c.action(m, content, result);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+}

+ 3 - 3
bot/src/commands/help.ts

@@ -1,7 +1,7 @@
 import { isAuthorisedAsync } from "../util";
 import { CommandSet, Command } from "src/model/command";
 import { Message } from "discord.js";
-import { getDocumentation } from "src/main";
+import { cmdMgr } from "src/main";
 
 @CommandSet
 export class Help {
@@ -11,8 +11,8 @@ export class Help {
 
         let baseCommands = "\n";
         let modCommands = "\n";
-
-        for (const doc of getDocumentation()) {
+        
+        for (const doc of cmdMgr.documentation) {
             if (isAuthed && doc.auth)
                 modCommands = `${modCommands}${doc.example}  -  ${doc.doc}\n`;
             else if (!doc.auth)

+ 14 - 107
bot/src/main.ts

@@ -2,52 +2,25 @@
 require("module-alias/register");
 
 import "./environment";
-import * as fs from "fs";
 import * as path from "path";
 import { client } from "./client";
 import * as mCmd from "./model/command";
-import "reflect-metadata";
 import { createConnection, getConnectionOptions, getRepository } from "typeorm";
-import { getNumberEnums, formatString } from "./util";
+import { formatString, assertOk } from "./util";
 import { DB_ENTITIES } from "@shared/db/entities";
 import { logger } from "./logging";
 import { GuildGreeting } from "@shared/db/entity/GuildGreeting";
 import { TextChannel, GuildMember, PartialGuildMember } from "discord.js";
+import { CommandManager } from "./command_manager";
 
-const REACT_PROBABILITY = 0.3;
-
-async function trigger(type: mCmd.ActionType, ...params: unknown[]) {
-    let actionDone = false;
-    const actions = botEvents[type];
-    for (let i = 0; i < actions.length; i++) {
-        const action = actions[i] as (...args: unknown[]) => boolean | Promise<boolean>;
-        const actionResult = action(actionDone, ...params);
-        if (actionResult instanceof Promise)
-            actionDone = (await actionResult) || actionDone;
-        else
-            actionDone = actionResult || actionDone;
-    }
-    return actionDone;
-}
+export const cmdMgr: CommandManager = new CommandManager(path.resolve(path.dirname(module.filename), "commands"));
 
-type BotEventCollection = { [event in mCmd.ActionType]: mCmd.BotAction[] };
-
-const commandSets: mCmd.ICommand[] = [];
-const botCommands: mCmd.IBotCommand[] = [];
-const botEvents: BotEventCollection = getNumberEnums(mCmd.ActionType).reduce((p, c) => { p[c as mCmd.ActionType] = []; return p; }, {} as BotEventCollection);
-const startActions: Array<() => void | Promise<void>> = [];
+const REACT_PROBABILITY = 0.3;
 
 client.bot.on("ready", async () => {
     logger.info("Starting up NoctBot");
-    client.botUser.setActivity(process.env.NODE_ENV == "dev" ? "Maintenance" : "@NoctBot help", {
-        type: "PLAYING"
-    });
-    for (let i = 0; i < startActions.length; i++) {
-        const action = startActions[i];
-        const val = action();
-        if (val instanceof Promise)
-            await val;
-    }
+    await client.botUser.setActivity(`@${client.botUser.username} help`, { type: "PLAYING" });
+    await assertOk(cmdMgr.onStart());
     logger.info("NoctBot is ready");
 });
 
@@ -69,38 +42,27 @@ client.bot.on("message", async m => {
 
     let content = m.cleanContent.trim();
 
-    if (await trigger(mCmd.ActionType.MESSAGE, m, content))
+    if (await cmdMgr.trigger(mCmd.ActionType.MESSAGE, m, content))
         return;
 
     if (m.mentions.users.size > 0 && m.mentions.users.has(client.botUser.id)) {
         const trimmedContent = m.content.trim();
         if (trimmedContent.startsWith(client.nameMention) || trimmedContent.startsWith(client.usernameMention)) {
             content = content.substring(`@${client.botUser.username}`.length).trim();
-
             const lowerCaseContent = content.toLowerCase();
-            for (const c of botCommands) {
-                if (typeof (c.pattern) == "string" && lowerCaseContent.startsWith(c.pattern)) {
-                    c.action(m, content);
-                    return;
-                }
-                else if (c.pattern instanceof RegExp) {
-                    const result = c.pattern.exec(content);
-                    if (result != null) {
-                        c.action(m, content, result);
-                        return;
-                    }
-                }
-            }
-
-            if (await trigger(mCmd.ActionType.DIRECT_MENTION, m, lowerCaseContent))
+            
+            if (cmdMgr.runCommand(m, content))
+                return;
+
+            if (await cmdMgr.trigger(mCmd.ActionType.DIRECT_MENTION, m, lowerCaseContent))
                 return;
         }
 
-        if (await trigger(mCmd.ActionType.INDIRECT_MENTION, m))
+        if (await cmdMgr.trigger(mCmd.ActionType.INDIRECT_MENTION, m))
             return;
     }
 
-    await trigger(mCmd.ActionType.POST_MESSAGE);
+    await cmdMgr.trigger(mCmd.ActionType.POST_MESSAGE);
 });
 
 client.bot.on("messageReactionAdd", (r, u) => {
@@ -110,7 +72,6 @@ client.bot.on("messageReactionAdd", (r, u) => {
     }
 });
 
-
 async function getGreeting(member: GuildMember | PartialGuildMember) {
     const repo = getRepository(GuildGreeting);
 
@@ -160,66 +121,12 @@ client.bot.on("guildMemberRemove", async member => {
     }));
 });
 
-function loadCommand(mod: Record<string, unknown>) {
-    for (const i in mod) {
-        if (!Object.prototype.hasOwnProperty.call(mod, i))
-            continue;
-
-        const commandClass = mod[i] as unknown;
-        // Ensure this is indeed a command class
-        if (!mCmd.isCommandSet(commandClass))
-            continue;
-
-        const cmd = new commandClass();
-        commandSets.push(cmd);
-
-        if (cmd._botCommands)
-            botCommands.push(...cmd._botCommands.map(c => ({ ...c, action: c.action.bind(cmd) })));
-
-        if (cmd._botEvents)
-            for (const [i, event] of Object.entries(cmd._botEvents)) {
-                botEvents[+i as mCmd.ActionType].push((event as mCmd.BotAction).bind(cmd));
-            }
-
-        if (cmd.onStart)
-            startActions.push(cmd.onStart.bind(cmd));
-    }
-}
-
-interface IDocumentationData {
-    name: string;
-    doc?: string;
-    example?: string;
-    auth: boolean;
-}
-
-export function getDocumentation(): IDocumentationData[] {
-    return botCommands.filter(m => m.documentation !== undefined).map(m => ({
-        name: m.pattern.toString(),
-        doc: m.documentation?.description,
-        example: m.documentation?.example,
-        auth: m.auth || false
-    }));
-}
-
 async function main() {
     await createConnection({
         ...await getConnectionOptions(),
         entities: DB_ENTITIES
     });
 
-    const commandsPath = path.resolve(path.dirname(module.filename), "commands");
-    const files = fs.readdirSync(commandsPath);
-
-    for (const file of files) {
-        const ext = path.extname(file);
-        if (ext != ".js")
-            continue;
-
-        // eslint-disable-next-line @typescript-eslint/no-var-requires
-        loadCommand(require(path.resolve(commandsPath, file)));
-    }
-
     client.bot.login(process.env.BOT_TOKEN);
 }