import path from "path"; import fs from "fs"; import { Message, Client, ClientEvents } from "discord.js"; import { EventType, BotEvent, ICommand, isPlugin, BotEventData, isCustomEvent, IPlugin, CommandType } from "./model/plugin"; import { isAuthorisedAsync } from "./util"; import { tryDo } from "../../shared/lib/src/common/async_utils"; interface IDocumentationData { type: CommandType; name: string; doc?: string; example?: string; auth: boolean; } type BotEventCollection = { [event in EventType]?: BotEvent[] }; export class PluginManager { private plugins: IPlugin[] = []; private commands: ICommand[] = []; private botEvents: BotEventCollection = {}; 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) { const getBotEventArray = (evtType: EventType) => { if (!this.botEvents[evtType]) this.botEvents[evtType] = []; return this.botEvents[evtType]; }; 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 (!isPlugin(commandClass)) continue; const cmd = new commandClass(); this.plugins.push(cmd); if (cmd.botCommands) this.commands.push(...cmd.botCommands.map(c => ({ ...c, action: c.action.bind(cmd) }))); if (cmd.botEvents) for (const [i, event] of Object.entries(cmd.botEvents)) { getBotEventArray(i as EventType)?.push((event as BotEvent).bind(cmd)); } } } get documentation(): IDocumentationData[] { return this.commands.filter(m => m.documentation !== undefined).map(m => ({ type: m.type, name: m.pattern.toString(), doc: m.documentation?.description, example: m.documentation?.example, auth: m.auth || false })); } async start(client: Client): Promise { for (const evtName of Object.keys(this.botEvents)) { if (!Object.prototype.hasOwnProperty.call(this.botEvents, evtName) || isCustomEvent(evtName)) continue; client.on(evtName as keyof ClientEvents, async (...args: unknown[]) => await this.trigger(evtName as EventType, ...args) ); } for (const plugin of this.plugins) { if (plugin.start) await plugin.start(); } } async trigger(event: EventType, ...params: unknown[]): Promise { const eventData: BotEventData = { actionsDone: false }; const eventHandlers = this.botEvents[event]; if (!eventHandlers) return eventData.actionsDone; for (const handler of eventHandlers) { const actionResult = handler(eventData, ...params); if (actionResult instanceof Promise) await actionResult; } return eventData.actionsDone; } async runCommand(type: CommandType, m: Message, content: string): Promise { const lowerCaseContent = content.toLowerCase(); for (const c of this.commands) { if (c.type != type) continue; let match = false; let matchData: string | RegExpMatchArray = ""; if (typeof (c.pattern) == "string" && lowerCaseContent.startsWith(c.pattern)) { match = true; matchData = content; } else if (c.pattern instanceof RegExp) { const result = c.pattern.exec(content); if (result != null) { match = true; matchData = result; } } if (match) { if (!c.allowDM && m.channel.type == "dm") { await tryDo(m.reply("Sorry, this command is not available in DMs for now.")); return false; } if (c.auth && !(await isAuthorisedAsync(m.member))) return false; const eventResult = c.action({ message: m, contents: matchData }); if (eventResult instanceof Promise) await eventResult; return true; } } return false; } }