623 lines
18 KiB
TypeScript
623 lines
18 KiB
TypeScript
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<string, unknown> | 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<boolean> {
|
|
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<Command> {
|
|
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<Command[]> {
|
|
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<string, Command> = new Collection()
|
|
disabled: Set<string> = 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<string, Command> {
|
|
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<string, Command> {
|
|
return this.list.filter(
|
|
(cmd) => cmd.category !== undefined && cmd.category === category
|
|
)
|
|
}
|
|
}
|
|
|
|
export class CategoriesManager {
|
|
client: CommandClient
|
|
list: Collection<string, CommandCategory> = new Collection()
|
|
|
|
constructor(client: CommandClient) {
|
|
this.client = client
|
|
}
|
|
|
|
/** Get a Collection of Categories */
|
|
all(): Collection<string, CommandCategory> {
|
|
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
|
|
}
|
|
}
|