import type { Guild } from '../structures/guild.ts' import type { Message } from '../structures/message.ts' import type { TextChannel } from '../structures/textChannel.ts' import type { User } from '../structures/user.ts' import { Collection } from '../utils/collection.ts' import type { CommandClient } from './client.ts' import type { Extension } from './extension.ts' import { join, walk } from '../../deps.ts' import type { Args } from '../utils/command.ts' export interface CommandContext { /** The Client object */ client: CommandClient /** Message which was parsed for Command */ message: Message /** The Author of the Message */ author: User /** The Channel in which Command was used */ channel: TextChannel /** Prefix which was used */ prefix: string /** Object of Command which was used */ command: Command /** Name of Command which was used */ name: string /** Array of Arguments used with Command */ args: Record | null /** Complete Raw String of Arguments */ argString: string /** Guild which the command has called */ guild?: Guild } export interface CommandOptions { /** Name of the Command */ name?: string /** Description of the Command */ description?: string /** Category of the Command */ category?: string /** Array of Aliases of Command, or only string */ aliases?: string | string[] /** Extension (Parent) of the Command */ extension?: Extension /** Usage of Command, only Argument Names */ usage?: string | string[] /** Usage Example of Command, only Arguments (without Prefix and Name) */ examples?: string | string[] /** Does the Command take Arguments? Maybe number of required arguments? Or list of arguments? */ args?: Args[] /** Permissions(s) required by both User and Bot in order to use Command */ permissions?: string | string[] /** Permission(s) required for using Command */ userPermissions?: string | string[] /** Permission(s) bot will need in order to execute Command */ botPermissions?: string | string[] /** Role(s) user will require in order to use Command. List or one of ID or name */ roles?: string | string[] /** Whitelisted Guilds. Only these Guild(s) can execute Command. (List or one of IDs) */ whitelistedGuilds?: string | string[] /** Whitelisted Channels. Command can be executed only in these channels. (List or one of IDs) */ whitelistedChannels?: string | string[] /** Whitelisted Users. Command can be executed only by these Users (List or one of IDs) */ whitelistedUsers?: string | string[] /** Whether the Command can only be used in NSFW channel or not */ nsfw?: boolean /** Whether the Command can only be used in Guild (if allowed in DMs) */ guildOnly?: boolean /** Whether the Command can only be used in Bot's DMs (if allowed) */ dmOnly?: boolean /** Whether the Command can only be used by Bot Owners */ ownerOnly?: boolean } export class Command implements CommandOptions { static meta?: CommandOptions name: string = '' description?: string category?: string aliases?: string | string[] extension?: Extension usage?: string | string[] examples?: string | string[] args?: Args[] permissions?: string | string[] userPermissions?: string | string[] botPermissions?: string | string[] roles?: string | string[] whitelistedGuilds?: string | string[] whitelistedChannels?: string | string[] whitelistedUsers?: string | string[] nsfw?: boolean guildOnly?: boolean dmOnly?: boolean ownerOnly?: boolean /** Method called when the command errors */ onError(ctx: CommandContext, error: Error): any {} /** Method executed before executing actual command. Returns bool value - whether to continue or not (optional) */ beforeExecute(ctx: CommandContext): boolean | Promise { return true } /** Actual command code, which is executed when all checks have passed. */ execute(ctx: CommandContext): any {} /** Method executed after executing command, passes on CommandContext and the value returned by execute too. (optional) */ afterExecute(ctx: CommandContext, executeResult: any): any {} toString(): string { return `Command: ${this.name}${ this.extension !== undefined && this.extension.name !== '' ? ` [${this.extension.name}]` : this.category !== undefined ? ` [${this.category}]` : '' }` } } export class CommandCategory { /** Name of the Category. */ name: string = '' /** Permissions(s) required by both User and Bot in order to use Category Commands */ permissions?: string | string[] /** Permission(s) required for using Category Commands */ userPermissions?: string | string[] /** Permission(s) bot will need in order to execute Category Commands */ botPermissions?: string | string[] /** Role(s) user will require in order to use Category Commands. List or one of ID or name */ roles?: string | string[] /** Whitelisted Guilds. Only these Guild(s) can execute Category Commands. (List or one of IDs) */ whitelistedGuilds?: string | string[] /** Whitelisted Channels. Category Commands can be executed only in these channels. (List or one of IDs) */ whitelistedChannels?: string | string[] /** Whitelisted Users. Category Commands can be executed only by these Users (List or one of IDs) */ whitelistedUsers?: string | string[] /** Whether the Category Commands can only be used in Guild (if allowed in DMs) */ guildOnly?: boolean /** Whether the Category Commands can only be used in Bot's DMs (if allowed) */ dmOnly?: boolean /** Whether the Category Commands can only be used by Bot Owners */ ownerOnly?: boolean } export class CommandBuilder extends Command { setName(name: string): CommandBuilder { this.name = name return this } setDescription(description?: string): CommandBuilder { this.description = description return this } setCategory(category?: string): CommandBuilder { this.category = category return this } setAlias(alias: string | string[]): CommandBuilder { this.aliases = alias return this } addAlias(alias: string | string[]): CommandBuilder { if (this.aliases === undefined) this.aliases = [] if (typeof this.aliases === 'string') this.aliases = [this.aliases] this.aliases = [ ...new Set( ...this.aliases, ...(typeof alias === 'string' ? [alias] : alias) ) ] return this } setExtension(extension?: Extension): CommandBuilder { this.extension = extension return this } setUsage(usage: string | string[]): CommandBuilder { this.usage = usage return this } addUsage(usage: string | string[]): CommandBuilder { if (this.usage === undefined) this.usage = [] if (typeof this.usage === 'string') this.usage = [this.usage] this.aliases = [ ...new Set( ...this.usage, ...(typeof usage === 'string' ? [usage] : usage) ) ] return this } setExample(examples: string | string[]): CommandBuilder { this.examples = examples return this } addExample(examples: string | string[]): CommandBuilder { if (this.examples === undefined) this.examples = [] if (typeof this.examples === 'string') this.examples = [this.examples] this.examples = [ ...new Set( ...this.examples, ...(typeof examples === 'string' ? [examples] : examples) ) ] return this } setPermissions(perms?: string | string[]): CommandBuilder { this.permissions = perms return this } setUserPermissions(perms?: string | string[]): CommandBuilder { this.userPermissions = perms return this } setBotPermissions(perms?: string | string[]): CommandBuilder { this.botPermissions = perms return this } setRoles(roles: string | string[]): CommandBuilder { this.roles = roles return this } setWhitelistedGuilds(list: string | string[]): CommandBuilder { this.whitelistedGuilds = list return this } setWhitelistedUsers(list: string | string[]): CommandBuilder { this.whitelistedUsers = list return this } setWhitelistedChannels(list: string | string[]): CommandBuilder { this.whitelistedChannels = list return this } setGuildOnly(value: boolean = true): CommandBuilder { this.guildOnly = value return this } setNSFW(value: boolean = true): CommandBuilder { this.nsfw = value return this } setOwnerOnly(value: boolean = true): CommandBuilder { this.ownerOnly = value return this } onBeforeExecute(fn: (ctx: CommandContext) => boolean | any): CommandBuilder { this.beforeExecute = fn return this } onExecute(fn: (ctx: CommandContext) => any): CommandBuilder { this.execute = fn return this } onAfterExecute( fn: (ctx: CommandContext, executeResult?: any) => any ): CommandBuilder { this.afterExecute = fn return this } } export class CommandsLoader { client: CommandClient #importSeq: { [name: string]: number } = {} constructor(client: CommandClient) { this.client = client } /** * Load a Command from file. * * NOTE: Relative paths resolve from cwd * * @param filePath Path of Command file. * @param exportName Export name. Default is the "default" export. */ async load( filePath: string, exportName: string = 'default', onlyRead?: boolean ): Promise { const stat = await Deno.stat(filePath).catch(() => undefined) if (stat === undefined || stat.isFile !== true) throw new Error(`File not found on path ${filePath}`) let seq: number | undefined if (this.#importSeq[filePath] !== undefined) seq = this.#importSeq[filePath] const mod = await import( // eslint-disable-next-line @typescript-eslint/restrict-plus-operands 'file:///' + join(Deno.cwd(), filePath) + (seq === undefined ? '' : `#${seq}`) ) if (this.#importSeq[filePath] === undefined) this.#importSeq[filePath] = 0 else this.#importSeq[filePath]++ const Cmd = mod[exportName] if (Cmd === undefined) throw new Error(`Command not exported as ${exportName} from ${filePath}`) let cmd: Command try { if (Cmd instanceof Command) cmd = Cmd else cmd = new Cmd() if (!(cmd instanceof Command)) throw new Error('failed') } catch (e) { throw new Error(`Failed to load Command from ${filePath}`) } if (onlyRead !== true) this.client.commands.add(cmd) return cmd } /** * Load commands from a Directory. * * NOTE: Relative paths resolve from cwd * * @param path Path of the directory. * @param options Options to configure loading. */ async loadDirectory( path: string, options?: { recursive?: boolean exportName?: string maxDepth?: number exts?: string[] onlyRead?: boolean } ): Promise { const commands: Command[] = [] for await (const entry of walk(path, { maxDepth: options?.maxDepth, exts: options?.exts, includeDirs: false })) { if (entry.isFile !== true) continue const cmd = await this.load( entry.path, options?.exportName, options?.onlyRead ) commands.push(cmd) } return commands } } export class CommandsManager { client: CommandClient list: Collection = new Collection() disabled: Set = new Set() loader: CommandsLoader constructor(client: CommandClient) { this.client = client this.loader = new CommandsLoader(client) } /** Number of loaded Commands */ get count(): number { return this.list.size } /** Filter out Commands by name/alias */ filter(search: string, subPrefix?: string): Collection { if (this.client.caseSensitive === false) search = search.toLowerCase() return this.list.filter((cmd: Command): boolean => { if (subPrefix !== undefined) { if ( this.client.caseSensitive === true ? subPrefix !== cmd.extension?.subPrefix : subPrefix.toLowerCase() !== cmd.extension?.subPrefix?.toLowerCase() ) { return false } } else if ( subPrefix === undefined && cmd.extension?.subPrefix !== undefined ) { return false } const name = this.client.caseSensitive === true ? cmd.name : cmd.name.toLowerCase() if (name === search) { return true } else if (cmd.aliases !== undefined) { let aliases: string[] if (typeof cmd.aliases === 'string') aliases = [cmd.aliases] else aliases = cmd.aliases if (this.client.caseSensitive === false) aliases = aliases.map((e) => e.toLowerCase()) return aliases.includes(search) } else { return false } }) } /** Find a Command by name/alias */ find(search: string, subPrefix?: string): Command | undefined { const filtered = this.filter(search, subPrefix) return filtered.first() } /** Fetch a Command including disable checks and subPrefix implementation */ fetch(parsed: ParsedCommand, bypassDisable?: boolean): Command | undefined { let cmd = this.find(parsed.name) if (cmd?.extension?.subPrefix !== undefined) cmd = undefined if (cmd === undefined && parsed.args.length > 0) { cmd = this.find(parsed.args[0], parsed.name) if (cmd === undefined || cmd.extension?.subPrefix === undefined) return if ( this.client.caseSensitive === true ? cmd.extension.subPrefix !== parsed.name : cmd.extension.subPrefix.toLowerCase() !== parsed.name.toLowerCase() ) return parsed.args.shift() } if (cmd === undefined) return if (this.isDisabled(cmd) && bypassDisable !== true) return return cmd } /** Check whether a Command exists or not */ exists(search: Command | string, subPrefix?: string): boolean { let exists = false if (typeof search === 'string') return this.find(search, subPrefix) !== undefined else { exists = this.find( search.name, subPrefix === undefined ? search.extension?.subPrefix : subPrefix ) !== undefined if (search.aliases !== undefined) { const aliases: string[] = typeof search.aliases === 'string' ? [search.aliases] : search.aliases exists = aliases .map((alias) => this.find(alias) !== undefined) .find((e) => e) ?? false } return exists } } /** Add a Command */ add(cmd: Command | typeof Command): boolean { if (!(cmd instanceof Command)) { const CmdClass = cmd cmd = new CmdClass() Object.assign(cmd, CmdClass.meta ?? {}) } if (this.exists(cmd, cmd.extension?.subPrefix)) throw new Error( `Failed to add Command '${cmd.toString()}' with name/alias already exists.` ) if (cmd.name === '') throw new Error('Command has no name') this.list.set( `${cmd.name}-${ this.list.filter((e) => this.client.caseSensitive === true ? e.name === cmd.name : e.name.toLowerCase() === cmd.name.toLowerCase() ).size }`, cmd ) return true } /** Delete a Command */ delete(cmd: string | Command): boolean { const find = typeof cmd === 'string' ? this.find(cmd) : cmd if (find === undefined) return false else return this.list.delete(find.name) } /** Check whether a Command is disabled or not */ isDisabled(name: string | Command): boolean { const cmd = typeof name === 'string' ? this.find(name) : name if (cmd === undefined) return false const exists = this.exists(name) if (!exists) return false return this.disabled.has(cmd.name) } /** Disable a Command */ disable(name: string | Command): boolean { const cmd = typeof name === 'string' ? this.find(name) : name if (cmd === undefined) return false if (this.isDisabled(cmd)) return false this.disabled.add(cmd.name) return true } /** Get all commands of a Category */ category(category: string): Collection { return this.list.filter( (cmd) => cmd.category !== undefined && cmd.category === category ) } } export class CategoriesManager { client: CommandClient list: Collection = new Collection() constructor(client: CommandClient) { this.client = client } /** Get a Collection of Categories */ all(): Collection { return this.list } /** Get a list of names of Categories added */ names(): string[] { return [...this.list.keys()] } /** Check if a Category exists or not */ has(category: string | CommandCategory): boolean { return this.list.has( typeof category === 'string' ? category : category.name ) } /** Get a Category by name */ get(name: string): CommandCategory | undefined { return this.list.get(name) } /** Add a Category to the Manager */ add(category: CommandCategory): CategoriesManager { if (this.has(category)) throw new Error(`Category ${category.name} already exists`) this.list.set(category.name, category) return this } /** Remove a Category from the Manager */ remove(category: string | CommandCategory): boolean { if (!this.has(category)) return false this.list.delete(typeof category === 'string' ? category : category.name) return true } } /** Parsed Command object */ export interface ParsedCommand { name: string args: string[] argString: string } /** Parses a Command to later look for. */ export const parseCommand = ( client: CommandClient, msg: Message, prefix: string ): ParsedCommand | undefined => { let content = msg.content.slice(prefix.length) if (client.spacesAfterPrefix === true) content = content.trim() const args = content.split(' ') const name = args.shift() if (name === undefined) return const argString = content.slice(name.length).trim() return { name, args, argString } }