405 lines
13 KiB
TypeScript
405 lines
13 KiB
TypeScript
import type { Message } from '../structures/message.ts'
|
|
import type { GuildTextBasedChannel } from '../structures/guildTextChannel.ts'
|
|
import { Client, ClientOptions } from '../client/mod.ts'
|
|
import {
|
|
CategoriesManager,
|
|
Command,
|
|
CommandContext,
|
|
CommandOptions,
|
|
CommandsManager,
|
|
parseCommand
|
|
} from './command.ts'
|
|
import { parseArgs } from '../utils/command.ts'
|
|
import { Extension, ExtensionsManager } from './extension.ts'
|
|
|
|
type PrefixReturnType = string | string[] | Promise<string | string[]>
|
|
|
|
/** Command Client options extending Client Options to provide a lot of Commands-related customizations */
|
|
export interface CommandClientOptions extends ClientOptions {
|
|
/** Global prefix(s) of the bot. */
|
|
prefix: string | string[]
|
|
/** Whether to enable mention prefix or not. */
|
|
mentionPrefix?: boolean
|
|
/** Method to get a Guild's custom prefix(s). */
|
|
getGuildPrefix?: (guildID: string) => PrefixReturnType
|
|
/** Method to get a User's custom prefix(s). */
|
|
getUserPrefix?: (userID: string) => PrefixReturnType
|
|
/** Method to get a Channel's custom prefix(s). */
|
|
getChannelPrefix?: (channelID: string) => PrefixReturnType
|
|
/** Method to check if certain Guild is blacklisted from using Commands. */
|
|
isGuildBlacklisted?: (guildID: string) => boolean | Promise<boolean>
|
|
/** Method to check if certain User is blacklisted from using Commands. */
|
|
isUserBlacklisted?: (guildID: string) => boolean | Promise<boolean>
|
|
/** Method to check if certain Channel is blacklisted from using Commands. */
|
|
isChannelBlacklisted?: (guildID: string) => boolean | Promise<boolean>
|
|
/** Allow spaces after prefix? Recommended with Mention Prefix ON. */
|
|
spacesAfterPrefix?: boolean
|
|
/** List of Bot's Owner IDs whom can access `ownerOnly` commands. */
|
|
owners?: string[]
|
|
/** Whether to allow Bots to use Commands or not, not allowed by default. */
|
|
allowBots?: boolean
|
|
/** Whether to allow Commands in DMs or not, allowed by default. */
|
|
allowDMs?: boolean
|
|
/** Whether Commands should be case-sensitive or not, not by default. */
|
|
caseSensitive?: boolean
|
|
}
|
|
|
|
/**
|
|
* Harmony Client with extended functionality for Message based Commands parsing and handling.
|
|
*
|
|
* See SlashClient (`Client#slash`) for more info about Slash Commands.
|
|
*/
|
|
export class CommandClient extends Client implements CommandClientOptions {
|
|
prefix: string | string[]
|
|
mentionPrefix: boolean
|
|
|
|
getGuildPrefix: (guildID: string) => PrefixReturnType
|
|
getUserPrefix: (userID: string) => PrefixReturnType
|
|
getChannelPrefix: (channelID: string) => PrefixReturnType
|
|
|
|
isGuildBlacklisted: (guildID: string) => boolean | Promise<boolean>
|
|
isUserBlacklisted: (guildID: string) => boolean | Promise<boolean>
|
|
isChannelBlacklisted: (guildID: string) => boolean | Promise<boolean>
|
|
|
|
spacesAfterPrefix: boolean
|
|
owners: string[]
|
|
allowBots: boolean
|
|
allowDMs: boolean
|
|
caseSensitive: boolean
|
|
|
|
extensions: ExtensionsManager = new ExtensionsManager(this)
|
|
commands: CommandsManager = new CommandsManager(this)
|
|
categories: CategoriesManager = new CategoriesManager(this)
|
|
|
|
constructor(options: CommandClientOptions) {
|
|
super(options)
|
|
this.prefix = options.prefix
|
|
this.mentionPrefix =
|
|
options.mentionPrefix === undefined ? false : options.mentionPrefix
|
|
|
|
this.getGuildPrefix =
|
|
options.getGuildPrefix === undefined
|
|
? (id: string) => this.prefix
|
|
: options.getGuildPrefix
|
|
this.getUserPrefix =
|
|
options.getUserPrefix === undefined
|
|
? (id: string) => this.prefix
|
|
: options.getUserPrefix
|
|
|
|
this.getChannelPrefix =
|
|
options.getChannelPrefix === undefined
|
|
? (id: string) => this.prefix
|
|
: options.getChannelPrefix
|
|
|
|
this.isUserBlacklisted =
|
|
options.isUserBlacklisted === undefined
|
|
? (id: string) => false
|
|
: options.isUserBlacklisted
|
|
this.isGuildBlacklisted =
|
|
options.isGuildBlacklisted === undefined
|
|
? (id: string) => false
|
|
: options.isGuildBlacklisted
|
|
this.isChannelBlacklisted =
|
|
options.isChannelBlacklisted === undefined
|
|
? (id: string) => false
|
|
: options.isChannelBlacklisted
|
|
|
|
this.spacesAfterPrefix =
|
|
options.spacesAfterPrefix === undefined
|
|
? false
|
|
: options.spacesAfterPrefix
|
|
|
|
this.owners = options.owners === undefined ? [] : options.owners
|
|
this.allowBots = options.allowBots === undefined ? false : options.allowBots
|
|
this.allowDMs = options.allowDMs === undefined ? true : options.allowDMs
|
|
this.caseSensitive =
|
|
options.caseSensitive === undefined ? false : options.caseSensitive
|
|
|
|
const self = this as any
|
|
if (self._decoratedCommands !== undefined) {
|
|
Object.values(self._decoratedCommands).forEach((entry: any) => {
|
|
this.commands.add(entry)
|
|
})
|
|
self._decoratedCommands = undefined
|
|
}
|
|
|
|
this.on(
|
|
'messageCreate',
|
|
async (msg: Message) => await this.processMessage(msg)
|
|
)
|
|
}
|
|
|
|
/** Processes a Message to Execute Command. */
|
|
async processMessage(msg: Message): Promise<any> {
|
|
if (!this.allowBots && msg.author.bot === true) return
|
|
|
|
const isUserBlacklisted = await this.isUserBlacklisted(msg.author.id)
|
|
if (isUserBlacklisted) return
|
|
|
|
const isChannelBlacklisted = await this.isChannelBlacklisted(msg.channel.id)
|
|
if (isChannelBlacklisted) return
|
|
|
|
if (msg.guild !== undefined) {
|
|
const isGuildBlacklisted = await this.isGuildBlacklisted(msg.guild.id)
|
|
if (isGuildBlacklisted) return
|
|
}
|
|
|
|
let prefix: string | string[] = []
|
|
if (typeof this.prefix === 'string') prefix = [...prefix, this.prefix]
|
|
else prefix = [...prefix, ...this.prefix]
|
|
|
|
const userPrefix = await this.getUserPrefix(msg.author.id)
|
|
if (userPrefix !== undefined) {
|
|
if (typeof userPrefix === 'string') prefix = [...prefix, userPrefix]
|
|
else prefix = [...prefix, ...userPrefix]
|
|
}
|
|
|
|
if (msg.guild !== undefined) {
|
|
const guildPrefix = await this.getGuildPrefix(msg.guild.id)
|
|
if (guildPrefix !== undefined) {
|
|
if (typeof guildPrefix === 'string') prefix = [...prefix, guildPrefix]
|
|
else prefix = [...prefix, ...guildPrefix]
|
|
}
|
|
}
|
|
|
|
prefix = [...new Set(prefix)]
|
|
|
|
let mentionPrefix = false
|
|
|
|
let usedPrefix = prefix
|
|
.filter((v) => msg.content.startsWith(v))
|
|
.sort((b, a) => a.length - b.length)[0]
|
|
if (usedPrefix === undefined && this.mentionPrefix) mentionPrefix = true
|
|
|
|
if (mentionPrefix) {
|
|
if (msg.content.startsWith(this.user?.mention as string) === true)
|
|
usedPrefix = this.user?.mention as string
|
|
else if (
|
|
msg.content.startsWith(this.user?.nickMention as string) === true
|
|
)
|
|
usedPrefix = this.user?.nickMention as string
|
|
else return
|
|
}
|
|
|
|
if (typeof usedPrefix !== 'string') return
|
|
prefix = usedPrefix
|
|
|
|
const parsed = parseCommand(this, msg, prefix)
|
|
if (parsed === undefined) return
|
|
const command = this.commands.fetch(parsed)
|
|
|
|
if (command === undefined) return
|
|
const category =
|
|
command.category !== undefined
|
|
? this.categories.get(command.category)
|
|
: undefined
|
|
|
|
// Guild whitelist exists, and if does and Command used in a Guild, is this Guild allowed?
|
|
// This is a bit confusing here, if these settings on a Command exist, and also do on Category, Command overrides them
|
|
if (
|
|
command.whitelistedGuilds === undefined &&
|
|
category?.whitelistedGuilds !== undefined &&
|
|
msg.guild !== undefined &&
|
|
category.whitelistedGuilds.includes(msg.guild.id) === false
|
|
)
|
|
return
|
|
if (
|
|
command.whitelistedGuilds !== undefined &&
|
|
msg.guild !== undefined &&
|
|
command.whitelistedGuilds.includes(msg.guild.id) === false
|
|
)
|
|
return
|
|
|
|
// Checks for Channel Whitelist
|
|
if (
|
|
command.whitelistedChannels === undefined &&
|
|
category?.whitelistedChannels !== undefined &&
|
|
category.whitelistedChannels.includes(msg.channel.id) === false
|
|
)
|
|
return
|
|
if (
|
|
command.whitelistedChannels !== undefined &&
|
|
command.whitelistedChannels.includes(msg.channel.id) === false
|
|
)
|
|
return
|
|
|
|
// Checks for Users Whitelist
|
|
if (
|
|
command.whitelistedUsers === undefined &&
|
|
category?.whitelistedUsers !== undefined &&
|
|
category.whitelistedUsers.includes(msg.author.id) === false
|
|
)
|
|
return
|
|
if (
|
|
command.whitelistedUsers !== undefined &&
|
|
command.whitelistedUsers.includes(msg.author.id) === false
|
|
)
|
|
return
|
|
|
|
const ctx: CommandContext = {
|
|
client: this,
|
|
name: parsed.name,
|
|
prefix,
|
|
args: parseArgs(command.args, parsed.args),
|
|
argString: parsed.argString,
|
|
message: msg,
|
|
author: msg.author,
|
|
command,
|
|
channel: msg.channel,
|
|
guild: msg.guild
|
|
}
|
|
|
|
// In these checks too, Command overrides Category if present
|
|
// Checks if Command is only for Owners
|
|
if (
|
|
(command.ownerOnly !== undefined || category === undefined
|
|
? command.ownerOnly
|
|
: category.ownerOnly) === true &&
|
|
!this.owners.includes(msg.author.id)
|
|
)
|
|
return this.emit('commandOwnerOnly', ctx)
|
|
|
|
// Checks if Command is only for Guild
|
|
if (
|
|
(command.guildOnly !== undefined || category === undefined
|
|
? command.guildOnly
|
|
: category.guildOnly) === true &&
|
|
msg.guild === undefined
|
|
)
|
|
return this.emit('commandGuildOnly', ctx)
|
|
|
|
// Checks if Command is only for DMs
|
|
if (
|
|
(command.dmOnly !== undefined || category === undefined
|
|
? command.dmOnly
|
|
: category.dmOnly) === true &&
|
|
msg.guild !== undefined
|
|
)
|
|
return this.emit('commandDmOnly', ctx)
|
|
|
|
if (
|
|
command.nsfw === true &&
|
|
(msg.guild === undefined ||
|
|
((msg.channel as unknown) as GuildTextBasedChannel).nsfw !== true)
|
|
)
|
|
return this.emit('commandNSFW', ctx)
|
|
|
|
const allPermissions =
|
|
command.permissions !== undefined
|
|
? command.permissions
|
|
: category?.permissions
|
|
|
|
if (
|
|
(command.botPermissions !== undefined ||
|
|
category?.botPermissions !== undefined ||
|
|
allPermissions !== undefined) &&
|
|
msg.guild !== undefined
|
|
) {
|
|
// TODO: Check Overwrites too
|
|
const me = await msg.guild.me()
|
|
const missing: string[] = []
|
|
|
|
let permissions =
|
|
command.botPermissions === undefined
|
|
? category?.permissions
|
|
: command.botPermissions
|
|
|
|
if (permissions !== undefined) {
|
|
if (typeof permissions === 'string') permissions = [permissions]
|
|
|
|
if (allPermissions !== undefined)
|
|
permissions = [...new Set(...permissions, ...allPermissions)]
|
|
|
|
for (const perm of permissions) {
|
|
if (me.permissions.has(perm) === false) missing.push(perm)
|
|
}
|
|
|
|
if (missing.length !== 0)
|
|
return this.emit('commandBotMissingPermissions', ctx, missing)
|
|
}
|
|
}
|
|
|
|
if (
|
|
(command.userPermissions !== undefined ||
|
|
category?.userPermissions !== undefined ||
|
|
allPermissions !== undefined) &&
|
|
msg.guild !== undefined
|
|
) {
|
|
let permissions =
|
|
command.userPermissions !== undefined
|
|
? command.userPermissions
|
|
: category?.userPermissions
|
|
|
|
if (permissions !== undefined) {
|
|
if (typeof permissions === 'string') permissions = [permissions]
|
|
|
|
if (allPermissions !== undefined)
|
|
permissions = [...new Set(...permissions, ...allPermissions)]
|
|
|
|
const missing: string[] = []
|
|
|
|
for (const perm of permissions) {
|
|
const has = msg.member?.permissions.has(perm)
|
|
if (has !== true) missing.push(perm)
|
|
}
|
|
|
|
if (missing.length !== 0)
|
|
return this.emit('commandUserMissingPermissions', ctx, missing)
|
|
}
|
|
}
|
|
|
|
if (command.args !== undefined) {
|
|
if (typeof command.args === 'boolean' && parsed.args.length === 0)
|
|
return this.emit('commandMissingArgs', ctx)
|
|
else if (
|
|
typeof command.args === 'number' &&
|
|
parsed.args.length < command.args
|
|
)
|
|
this.emit('commandMissingArgs', ctx)
|
|
}
|
|
|
|
try {
|
|
this.emit('commandUsed', ctx)
|
|
|
|
const beforeExecute = await command.beforeExecute(ctx)
|
|
if (beforeExecute === false) return
|
|
|
|
const result = await command.execute(ctx)
|
|
await command.afterExecute(ctx, result)
|
|
} catch (e) {
|
|
try {
|
|
await command.onError(ctx, e)
|
|
} catch (e) {
|
|
this.emit('commandError', ctx, e)
|
|
}
|
|
this.emit('commandError', ctx, e)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Command decorator. Decorates the function with optional metadata as a Command registered upon constructing class.
|
|
*/
|
|
export function command(options?: CommandOptions) {
|
|
return function (target: CommandClient | Extension, name: string) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
const c = target as any
|
|
if (c._decoratedCommands === undefined) c._decoratedCommands = {}
|
|
|
|
const prop = c[name]
|
|
|
|
if (typeof prop !== 'function')
|
|
throw new Error('@command decorator can only be used on class methods')
|
|
|
|
const command = new Command()
|
|
|
|
command.name = name
|
|
command.execute = prop
|
|
|
|
if (options !== undefined) Object.assign(command, options)
|
|
|
|
if (target instanceof Extension) command.extension = target
|
|
|
|
c._decoratedCommands[command.name] = command
|
|
}
|
|
}
|