//IMPORTS import { Permissions, PermissionResolvable, Message, GuildMember, MessageEmbed, } from "discord.js"; import { Argument, ArgumentResolvable, ProcessArgument } from "./Arguments.js"; //TYPE DEFINITIONS type GetMode = "ALL" | "PREFIX" | "NO_PREFIX"; type PermissionCheckTypes = "ALL" | "ANY"; export interface CommandMessageStructure { command: Command; arguments: ArgumentResolvable[]; } interface CommandBuilder { name: string; arguments?: Argument[]; aliases?: string[] | string; keywords?: string[] | string; description?: string; usage?: string; permissionCheck?: PermissionCheckTypes; permissions?: PermissionResolvable; visible?: boolean; function: ( message?: Message, cmdArguments?: ArgumentResolvable[] ) => void | string | MessageEmbed | Promise; } interface PhraseOccurrenceData { command: Command; type: "NAME" | "ALIAS"; } //CLASSES export class PermissionsError { private command: Command; private user: GuildMember | null; constructor(command: Command, user?: GuildMember | null) { this.command = command; this.user = user || null; } toString() { return `User ${this.user?.user.tag} doesn't have enough permissions to run "${this.command.name}" command`; } } export class Command { name: string; arguments: Argument[]; aliases: string[]; keywords: string[]; description: string; usage: string; permissionCheck: PermissionCheckTypes; permissions: Permissions; visible: boolean; private function: ( message?: Message, cmdArguments?: ArgumentResolvable[] ) => void | string | MessageEmbed | Promise; /** * Command constructor * @constructor * @param {CommandBuilder} options - all command properties * @param {string} options.name - command name (used to trigger the command) * @param {string | string[]} [options.aliases] - other words that can trigger the command with prefix * @param {string | string[]} [options.keywords] - other words that can trigger the command without prefix * @param {string} [options.description="No description"] - command description shown in the help message * @param {string} [options.usage] - command usage description shown in the help message * @param {PermissionCheckTypes} [options.permissionCheck='ANY'] - specifies if the caller has to have all of the specified permissions or any of that * @param {PermissionResolvable} [options.permissions] - permissions needed to run the command * @param {boolean} [options.visible=true] - show command in the help message * @param {Function} options.function - function that will trigger when the commands gets called */ constructor(options: CommandBuilder) { this.name = options.name.split(" ").join("_"); this.arguments = options.arguments || []; this.aliases = Command.processPhrase(options.aliases); this.keywords = Command.processPhrase(options.keywords); this.description = options.description || "No description"; this.usage = options.usage || this.generateUsageFromArguments(); this.permissionCheck = options.permissionCheck == "ALL" || options.permissionCheck == "ANY" ? options.permissionCheck : "ANY"; this.permissions = new Permissions(options.permissions || 0); this.visible = options.visible != undefined ? options.visible : true; this.function = options.function; } /** * Starts the command * @param {Message} [message] - a *Message* object used to check caller's permissions. It will get passed to the execution function (specified in *function* property of command's constructor) * @param {string[]} [cmdArguments] - list of processed arguments passed in a Discord message * @returns *Promise* */ async start( message?: Message, cmdArguments?: ArgumentResolvable[] ): Promise { const memberPermissions: Readonly = message?.member?.permissions || new Permissions(0); if ( this.permissionCheck == "ALL" ? memberPermissions.has(this.permissions, true) : memberPermissions.any(this.permissions, true) ) { const fnResult = await this.function(message, cmdArguments); if (typeof fnResult == "string") { await message?.reply(fnResult); } else if (fnResult instanceof MessageEmbed) { await message?.channel.send(fnResult); } } else { throw new PermissionsError(this, message?.member); } } private static processPhrase(phrase?: string | string[]): string[] { if (Array.isArray(phrase)) { const buff = phrase.map((p) => { return p.split(" ").join("_"); }); buff.map((e, i, a) => { if (e == "" || e == " ") { a.splice(i, 1); } }); return buff; } else if (typeof phrase == "string" && phrase != "" && phrase != " ") { const buff = []; buff.push(phrase.split(" ").join("_")); return buff; } else { return []; } } private generateUsageFromArguments(): string { let usageTemplate: string = ""; this.arguments.map((e) => { usageTemplate += `[${e.name} (${e.type}${ e.optional ? ", optional" : "" })] `; }); return usageTemplate; } } export class CommandManager { list: Command[]; prefix: string; argumentSeparator: string; constructor(prefix: string, argumentSeparator?: string) { this.list = []; this.prefix = prefix; this.argumentSeparator = argumentSeparator || ","; } /** * Retrieves the command by name, alias or keyword * @param {string} phrase - command name, alias or keyword * @param {GetMode} [mode='ALL'] - specifies which types of command triggers will be used to find the command [*NO_PREFIX* - only keywords; *PREFIX* - names and aliases] * @returns *Command* | *null* */ get(phrase: string, mode?: GetMode): Command | null { if (!mode) mode = "ALL"; let command: Command | null = null; this.list.map((c) => { switch (mode) { case "PREFIX": if (c.name == phrase) { command = c; } c.aliases.map((a) => { if (a == phrase) { command = c; } }); break; case "NO_PREFIX": c.keywords.map((k) => { if (k == phrase) { command = c; } }); break; case "ALL": if (c.name == phrase) { command = c; } c.aliases.map((a) => { if (a == phrase) { command = c; } }); c.keywords.map((k) => { if (k == phrase) { command = c; } }); break; } }); return command; } /** * Adds the given command to the instance manager * @param {Command} command - *Command* or *HelpMessage* object * @returns *boolean* */ add(command: Command): boolean { try { if (!(command instanceof Command)) { throw new TypeError("Inavlid argument type"); } const nameOccurrence: PhraseOccurrenceData | null = this.findPhraseOccurrence(command.name); if (nameOccurrence) { throw new Error( `The name "${command.name}" has already been registered as ${nameOccurrence.type} in the "${nameOccurrence.command.name}" command.` ); } command.aliases.map((a, i, ar) => { const aliasOccurrence: PhraseOccurrenceData | null = this.findPhraseOccurrence(a); if (aliasOccurrence) { console.warn( `WARN! The name "${a}" is already registered as ${aliasOccurrence.type} in the "${aliasOccurrence.command.name}" command. It will be removed from the "${command.name}" command.` ); ar.splice(i, 1); } }); this.list.map((c) => { command.keywords.map((k, i, a) => { if (c.keywords.indexOf(k) != -1) { console.warn( `WARN! The name "${k}" is already a registered KEYWORD for the "${c.name}" command. It will be removed from the "${command.name}" command` ); a.splice(i, 1); } }); }); this.list.push(command); return true; } catch (e) { console.error(`[❌ ERROR] ${e.toString()}`); return false; } } /** * Fetches command and arguments from the given *Message* object * @param {Message} message - *Message* object * @returns *CommandMessagesStructure* | *null* */ fetch(message: Message): CommandMessageStructure | null { if (!message.author.bot) { const content = message.content.startsWith(this.prefix) ? message.content.replace(this.prefix, "") : message.content; const name = content.split(" ")[0]; const command = this.get(name, "ALL"); if (command) { const argumentsText = content.replace(name, ""); const argumentsList = argumentsText .split(this.argumentSeparator) .map((a) => { return a.replace(" ", ""); }); if ( (argumentsList[0] == "" || argumentsList[0] == " ") && argumentsList.length == 1 ) { argumentsList.splice(0, 1); } return { command: command, arguments: argumentsList, }; } else { return null; } } else { return null; } } private findPhraseOccurrence(phrase?: string): PhraseOccurrenceData | null { let returnValue: PhraseOccurrenceData | null = null; this.list.map((c) => { if (phrase == c.name) { returnValue = { command: c, type: "NAME", }; } else if (c.aliases.indexOf(phrase || "") != -1) { returnValue = { command: c, type: "ALIAS", }; } }); return returnValue; } }