UNPKG

detritus-client

Version:

A Typescript NodeJS library to interact with Discord's API, both Rest and Gateway.

791 lines (790 loc) 33.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CommandClient = void 0; const path = require("path"); const detritus_utils_1 = require("detritus-utils"); const client_1 = require("./client"); const clusterclient_1 = require("./clusterclient"); const constants_1 = require("./constants"); const errors_1 = require("./errors"); const interactioncommandclient_1 = require("./interactioncommandclient"); const utils_1 = require("./utils"); const command_1 = require("./command/command"); const context_1 = require("./command/context"); const commandratelimit_1 = require("./commandratelimit"); const collections_1 = require("./collections"); const structures_1 = require("./structures"); /** * Command Client, hooks onto a ClusterClient or ShardClient to provide easier command handling * Flow is `onMessageCheck` -> `onPrefixCheck` -> `onCommandCheck` * @category Clients */ class CommandClient extends detritus_utils_1.EventSpewer { constructor(token, options = {}) { super(); this._clientSubscriptions = []; this.activateOnEdits = false; this.directories = new collections_1.BaseCollection(); this.ignoreMe = true; this.maxEditDuration = 5 * 60 * 1000; this.mentionsEnabled = true; this.ran = false; this.ratelimits = []; options = Object.assign({ useClusterClient: true }, options); if (token instanceof interactioncommandclient_1.InteractionCommandClient) { token = token.client; } if (process.env.CLUSTER_MANAGER === 'true') { options.useClusterClient = true; if (token instanceof clusterclient_1.ClusterClient) { if (process.env.CLUSTER_TOKEN !== token.token) { throw new Error('Cluster Client must have matching tokens with the Manager!'); } } else { token = process.env.CLUSTER_TOKEN; } } let client; if (typeof (token) === 'string') { if (options.useClusterClient) { client = new clusterclient_1.ClusterClient(token, options); } else { client = new client_1.ShardClient(token, options); } } else { client = token; } if (!client || !(client instanceof clusterclient_1.ClusterClient || client instanceof client_1.ShardClient)) { throw new Error('Token has to be a string or an instance of a client'); } this.client = client; Object.defineProperty(this.client, 'commandClient', { value: this }); if (this.client instanceof clusterclient_1.ClusterClient) { for (let [shardId, shard] of this.client.shards) { Object.defineProperty(shard, 'commandClient', { value: this }); } } this.activateOnEdits = !!options.activateOnEdits || this.activateOnEdits; this.commands = []; this.ignoreMe = options.ignoreMe || this.ignoreMe; this.maxEditDuration = +(options.maxEditDuration || this.maxEditDuration); this.mentionsEnabled = !!(options.mentionsEnabled || options.mentionsEnabled === undefined); this.prefixes = Object.freeze({ custom: new collections_1.BaseSet(), mention: new collections_1.BaseSet(), }); this.ratelimiter = options.ratelimiter || new commandratelimit_1.CommandRatelimiter(); this.replies = new collections_1.BaseCollection({ expire: this.maxEditDuration }); this.onCommandCheck = options.onCommandCheck || this.onCommandCheck; this.onCommandCancel = options.onCommandCancel || this.onCommandCancel; this.onMessageCheck = options.onMessageCheck || this.onMessageCheck; this.onMessageCancel = options.onMessageCancel || this.onMessageCancel; this.onPrefixCheck = options.onPrefixCheck || this.onPrefixCheck; if (options.prefix !== undefined) { if (options.prefixes === undefined) { options.prefixes = []; } options.prefixes.push(options.prefix); } if (options.prefixes !== undefined) { options.prefixes.sort((x, y) => y.length - x.length); for (let prefix of options.prefixes) { prefix = prefix.trim(); if (options.prefixSpace) { prefix += ' '; } this.prefixes.custom.add(prefix); } } if (this.client.ran) { this.addMentionPrefixes(); } if (options.ratelimit) { this.ratelimits.push(new commandratelimit_1.CommandRatelimit(options.ratelimit)); } if (options.ratelimits) { for (let rOptions of options.ratelimits) { if (typeof (rOptions.type) === 'string') { const rType = (rOptions.type || '').toLowerCase(); if (this.ratelimits.some((ratelimit) => ratelimit.type === rType)) { throw new Error(`Ratelimit with type ${rType} already exists`); } } this.ratelimits.push(new commandratelimit_1.CommandRatelimit(rOptions)); } } if (!this.prefixes.custom.size && !this.mentionsEnabled) { throw new Error('You must pass in prefixes or enable mentions!'); } Object.defineProperties(this, { _clientSubscriptions: { enumerable: false, writable: false }, activateOnEdits: { configurable: true, writable: false }, commands: { writable: false }, maxEditDuration: { configurable: true, writable: false }, mentionsEnabled: { configurable: true, writable: false }, prefixes: { writable: false }, prefixSpace: { configurable: true, writable: false }, ran: { configurable: true, writable: false }, onCommandCheck: { enumerable: false, writable: true }, onCommandCancel: { enumerable: false, writable: true }, onMessageCheck: { enumerable: false, writable: true }, onMessageCancel: { enumerable: false, writable: true }, onPrefixCheck: { enumerable: false, writable: true }, }); } get rest() { return this.client.rest; } /* Set Options */ setActivateOnEdits(value) { Object.defineProperty(this, 'activateOnEdits', { value }); } setMaxEditDuration(value) { Object.defineProperty(this, 'maxEditDuration', { value }); } setMentionsEnabled(value) { Object.defineProperty(this, 'mentionsEnabled', { value }); } setPrefixSpace(value) { Object.defineProperty(this, 'prefixSpace', { value }); } /* Generic Command Function */ add(options, run) { let command; if (options instanceof command_1.Command) { command = options; } else { if (typeof (options) === 'string') { options = { name: options, run }; } else { if (run !== undefined) { options.run = run; } } // create a normal command class with the options given if (options._class === undefined) { command = new command_1.Command(this, options); } else { // check for `.constructor` to make sure it's a class if (options._class.constructor) { command = new options._class(this, options); } else { // else it's just a function, `ts-node` outputs these command = options._class(this, options); } if (!command._file) { Object.defineProperty(command, '_file', { value: options._file }); } } } if (typeof (command.run) !== 'function') { throw new Error('Command needs a run function'); } for (let name of command.names) { if (this.commands.some((c) => c.check(name))) { throw new Error(`Alias/name \`${name}\` already exists.`); } } this.commands.push(command); this.commands.sort((x, y) => y.priority - x.priority); if (!this._clientSubscriptions.length) { this.setSubscriptions(); } return this; } addMultiple(commands = []) { for (let command of commands) { this.add(command); } return this; } async addMultipleIn(directory, options = {}) { options = Object.assign({ subdirectories: true }, options); if (!options.isAbsolute) { if (require.main) { // require.main.path exists but typescript doesn't let us use it.. directory = path.join(path.dirname(require.main.filename), directory); } } this.directories.set(directory, { subdirectories: !!options.subdirectories }); const files = await utils_1.getFiles(directory, options.subdirectories); const errors = {}; const addCommand = (imported, filepath) => { if (!imported) { return; } if (typeof (imported) === 'function') { this.add({ _file: filepath, _class: imported, name: '' }); } else if (imported instanceof command_1.Command) { Object.defineProperty(imported, '_file', { value: filepath }); this.add(imported); } else if (typeof (imported) === 'object' && Object.keys(imported).length) { if (Array.isArray(imported)) { for (let child of imported) { addCommand(child, filepath); } } else { if ('name' in imported) { this.add({ ...imported, _file: filepath }); } } } }; for (let file of files) { if (!file.endsWith((constants_1.IS_TS_NODE) ? '.ts' : '.js')) { continue; } const filepath = path.resolve(directory, file); try { let importedCommand = require(filepath); if (typeof (importedCommand) === 'object' && importedCommand.__esModule) { importedCommand = importedCommand.default; } addCommand(importedCommand, filepath); } catch (error) { errors[filepath] = error; } } if (Object.keys(errors).length) { throw new errors_1.ImportedCommandsError(errors); } return this; } clear() { for (let command of this.commands) { if (command._file) { const requirePath = require.resolve(command._file); if (requirePath) { delete require.cache[requirePath]; } } } this.commands.length = 0; this.clearSubscriptions(); } clearSubscriptions() { while (this._clientSubscriptions.length) { const subscription = this._clientSubscriptions.shift(); if (subscription) { subscription.remove(); } } } async resetCommands() { this.clear(); for (let [directory, options] of this.directories) { await this.addMultipleIn(directory, { isAbsolute: true, ...options }); } } /* end */ addMentionPrefixes() { let userId = null; if (this.client instanceof clusterclient_1.ClusterClient) { for (let [shardId, shard] of this.client.shards) { if (shard.user) { userId = shard.user.id; break; } } } else if (this.client instanceof client_1.ShardClient) { if (this.client.user) { userId = this.client.user.id; } } if (userId) { this.prefixes.mention.clear(); this.prefixes.mention.add(`<@${userId}>`); this.prefixes.mention.add(`<@!${userId}>`); } } async getAttributes(context) { let content = context.message.content.trim(); let contentLower = content.toLowerCase(); if (!content) { return null; } let prefix = ''; if (this.mentionsEnabled) { if (!this.prefixes.mention.length) { this.addMentionPrefixes(); } for (let mention of this.prefixes.mention) { if (contentLower.startsWith(mention)) { prefix = mention; break; } } } if (!prefix) { const customPrefixes = await Promise.resolve(this.getPrefixes(context)); for (let custom of customPrefixes) { if (contentLower.startsWith(custom)) { prefix = custom; break; } } } if (prefix) { content = content.substring(prefix.length).trim(); return { content, prefix }; } return null; } getCommand(attributes) { if (attributes.content) { const insensitive = attributes.content.toLowerCase(); for (let command of this.commands) { const name = command.getName(insensitive); if (name) { attributes.content = attributes.content.substring(name.length).trim(); return command; } } } return null; } async getPrefixes(context) { if (typeof (this.onPrefixCheck) === 'function') { const prefixes = await Promise.resolve(this.onPrefixCheck(context)); if (prefixes === this.prefixes.custom) { return prefixes; } let sorted; if (prefixes instanceof Set || prefixes instanceof collections_1.BaseSet) { sorted = Array.from(prefixes); } else if (typeof (prefixes) === 'string') { sorted = [prefixes]; } else if (Array.isArray(prefixes)) { sorted = prefixes; } else { throw new Error('Invalid Prefixes Type Received'); } return new collections_1.BaseSet(sorted.sort((x, y) => y.length - x.length)); } return this.prefixes.custom; } setSubscriptions() { this.clearSubscriptions(); const subscriptions = this._clientSubscriptions; subscriptions.push(this.client.subscribe(constants_1.ClientEvents.MESSAGE_CREATE, this.handleMessageCreate.bind(this))); subscriptions.push(this.client.subscribe(constants_1.ClientEvents.MESSAGE_DELETE, this.handleDelete.bind(this, constants_1.ClientEvents.MESSAGE_DELETE))); subscriptions.push(this.client.subscribe(constants_1.ClientEvents.MESSAGE_UPDATE, this.handleMessageUpdate.bind(this))); } /* Kill/Run */ kill() { this.client.kill(); this.emit(constants_1.ClientEvents.KILLED); this.clearSubscriptions(); this.removeAllListeners(); } async run(options = {}) { if (this.ran) { return this.client; } if (options.directories) { for (let directory of options.directories) { await this.addMultipleIn(directory); } } await this.client.run(options); this.addMentionPrefixes(); Object.defineProperty(this, 'ran', { value: true }); return this.client; } storeReply(messageId, command, context, reply) { if (this.maxEditDuration && reply instanceof structures_1.Message) { this.replies.set(messageId, { command, context, reply }); } } /* Handler */ async handleMessageCreate(event) { return this.handle(constants_1.ClientEvents.MESSAGE_CREATE, event); } async handleMessageUpdate(event) { if (event.isEmbedUpdate) { return; } return this.handle(constants_1.ClientEvents.MESSAGE_UPDATE, event); } async handle(name, event) { const { message } = event; // message will only be null on embed updates if (!message || (this.ignoreMe && message.fromMe)) { return; } let typing = null; if (name === constants_1.ClientEvents.MESSAGE_CREATE) { ({ typing } = event); } const context = new context_1.Context(message, typing, this); if (typeof (this.onMessageCheck) === 'function') { try { const shouldContinue = await Promise.resolve(this.onMessageCheck(context)); if (!shouldContinue) { if (typeof (this.onMessageCancel) === 'function') { return await Promise.resolve(this.onMessageCancel(context)); } const error = new Error('Message check returned false'); const payload = { context, error }; this.emit(constants_1.ClientEvents.COMMAND_NONE, payload); return; } } catch (error) { const payload = { context, error }; this.emit(constants_1.ClientEvents.COMMAND_ERROR, payload); return; } } if (name === constants_1.ClientEvents.MESSAGE_UPDATE) { if (!this.activateOnEdits) { return; } const { differences } = event; if (!differences || !differences.content) { return; } } let attributes = null; try { if (!message.fromUser) { throw new Error('Message is not from a user.'); } if (message.isEdited) { const difference = message.editedAtUnix - message.timestampUnix; if (this.maxEditDuration < difference) { throw new Error('Edit timestamp is higher than max edit duration'); } } attributes = await this.getAttributes(context); if (!attributes) { throw new Error('Does not start with any allowed prefixes'); } } catch (error) { const payload = { context, error }; this.emit(constants_1.ClientEvents.COMMAND_NONE, payload); return; } const command = this.getCommand(attributes); if (command) { context.command = command; if (typeof (this.onCommandCheck) === 'function') { try { const shouldContinue = await Promise.resolve(this.onCommandCheck(context, command)); if (!shouldContinue) { if (typeof (this.onCommandCancel) === 'function') { return await Promise.resolve(this.onCommandCancel(context, command)); } const error = new Error('Command check returned false'); const payload = { context, error }; this.emit(constants_1.ClientEvents.COMMAND_NONE, payload); return; } } catch (error) { const payload = { context, error }; this.emit(constants_1.ClientEvents.COMMAND_ERROR, payload); return; } } } else { const error = new Error('Unknown Command'); const payload = { context, error }; this.emit(constants_1.ClientEvents.COMMAND_NONE, payload); return; } if (!command.responseOptional && !message.canReply) { const error = new Error('Cannot send messages in this channel'); const payload = { command, context, error }; this.emit(constants_1.ClientEvents.COMMAND_ERROR, payload); return; } if (this.ratelimits.length || command.ratelimits.length) { const now = Date.now(); { const ratelimits = this.ratelimiter.getExceeded(context, this.ratelimits, now); if (ratelimits.length) { const global = true; const payload = { command, context, global, ratelimits, now }; this.emit(constants_1.ClientEvents.COMMAND_RATELIMIT, payload); if (typeof (command.onRatelimit) === 'function') { try { await Promise.resolve(command.onRatelimit(context, ratelimits, { global, now })); } catch (error) { // do something with this error? } } return; } } { const ratelimits = this.ratelimiter.getExceeded(context, command.ratelimits, now); if (ratelimits.length) { const global = false; const payload = { command, context, global, ratelimits, now }; this.emit(constants_1.ClientEvents.COMMAND_RATELIMIT, payload); if (typeof (command.onRatelimit) === 'function') { try { const reply = await Promise.resolve(command.onRatelimit(context, ratelimits, { global, now })); this.storeReply(message.id, command, context, reply); } catch (error) { // do something with this error? } } return; } } } if (context.inDm) { if (command.disableDm) { if (typeof (command.onDmBlocked) === 'function') { try { const reply = await Promise.resolve(command.onDmBlocked(context)); this.storeReply(message.id, command, context, reply); } catch (error) { const payload = { command, context, error }; this.emit(constants_1.ClientEvents.COMMAND_ERROR, payload); } } else { const error = new Error('Command with DMs disabled used in DM'); if (command.disableDmReply) { const payload = { command, context, error }; this.emit(constants_1.ClientEvents.COMMAND_ERROR, payload); } else { try { const reply = await message.reply(`Cannot use \`${command.name}\` in DMs.`); this.storeReply(message.id, command, context, reply); const payload = { command, context, error, reply }; this.emit(constants_1.ClientEvents.COMMAND_ERROR, payload); } catch (e) { const payload = { command, context, error, extra: e }; this.emit(constants_1.ClientEvents.COMMAND_ERROR, payload); } } } return; } } else { // check the bot's permissions in the server // should never be ignored since it's most likely the bot will rely on this permission to do whatever action if (Array.isArray(command.permissionsClient) && command.permissionsClient.length) { const failed = []; const channel = context.channel; const member = context.me; if (channel && member) { const total = member.permissionsIn(channel); if (!member.isOwner && !utils_1.PermissionTools.checkPermissions(total, constants_1.Permissions.ADMINISTRATOR)) { for (let permission of command.permissionsClient) { if (!utils_1.PermissionTools.checkPermissions(total, permission)) { failed.push(permission); } } } } else { for (let permission of command.permissionsClient) { failed.push(permission); } } if (failed.length) { const payload = { command, context, permissions: failed }; this.emit(constants_1.ClientEvents.COMMAND_PERMISSIONS_FAIL_CLIENT, payload); if (typeof (command.onPermissionsFailClient) === 'function') { try { const reply = await Promise.resolve(command.onPermissionsFailClient(context, failed)); this.storeReply(message.id, command, context, reply); } catch (error) { // do something with this error? } } return; } } // if command doesn't specify it should ignore the client owner, or if the user isn't a client owner // continue to permission checking if (!command.permissionsIgnoreClientOwner || !context.user.isClientOwner) { // check the user's permissions if (Array.isArray(command.permissions) && command.permissions.length) { const failed = []; const channel = context.channel; const member = context.member; if (channel && member) { const total = member.permissionsIn(channel); if (!member.isOwner && !utils_1.PermissionTools.checkPermissions(total, constants_1.Permissions.ADMINISTRATOR)) { for (let permission of command.permissions) { if (!utils_1.PermissionTools.checkPermissions(total, permission)) { failed.push(permission); } } } } else { for (let permission of command.permissions) { failed.push(permission); } } if (failed.length) { const payload = { command, context, permissions: failed }; this.emit(constants_1.ClientEvents.COMMAND_PERMISSIONS_FAIL, payload); if (typeof (command.onPermissionsFail) === 'function') { try { const reply = await Promise.resolve(command.onPermissionsFail(context, failed)); this.storeReply(message.id, command, context, reply); } catch (error) { // do something with this error? } } return; } } } } if (typeof (command.onBefore) === 'function') { try { const shouldContinue = await Promise.resolve(command.onBefore(context)); if (!shouldContinue) { if (typeof (command.onCancel) === 'function') { const reply = await Promise.resolve(command.onCancel(context)); this.storeReply(message.id, command, context, reply); } return; } } catch (error) { const payload = { command, context, error }; this.emit(constants_1.ClientEvents.COMMAND_ERROR, payload); return; } } const prefix = context.prefix = attributes.prefix; const { errors, parsed: args } = await command.getArgs(attributes, context); if (Object.keys(errors).length) { if (typeof (command.onTypeError) === 'function') { const reply = await Promise.resolve(command.onTypeError(context, args, errors)); this.storeReply(message.id, command, context, reply); } const error = new Error('Command errored out while converting args'); const payload = { command, context, error, extra: errors }; this.emit(constants_1.ClientEvents.COMMAND_ERROR, payload); return; } try { if (typeof (command.onBeforeRun) === 'function') { const shouldRun = await Promise.resolve(command.onBeforeRun(context, args)); if (!shouldRun) { if (typeof (command.onCancelRun) === 'function') { const reply = await Promise.resolve(command.onCancelRun(context, args)); this.storeReply(message.id, command, context, reply); } return; } } let timeout = null; try { if (command.triggerTypingAfter !== -1) { if (command.triggerTypingAfter) { timeout = new detritus_utils_1.Timers.Timeout(); Object.defineProperty(context, 'typingTimeout', { value: timeout }); timeout.start(command.triggerTypingAfter, async () => { try { await context.triggerTyping(); } catch (error) { // do something maybe? } }); } else { await context.triggerTyping(); } } if (typeof (command.run) === 'function') { const reply = await Promise.resolve(command.run(context, args)); this.storeReply(message.id, command, context, reply); } if (timeout) { timeout.stop(); } const payload = { args, command, context, prefix }; this.emit(constants_1.ClientEvents.COMMAND_RAN, payload); if (typeof (command.onSuccess) === 'function') { await Promise.resolve(command.onSuccess(context, args)); } } catch (error) { if (timeout) { timeout.stop(); } const payload = { args, command, context, error, prefix }; this.emit(constants_1.ClientEvents.COMMAND_RUN_ERROR, payload); if (typeof (command.onRunError) === 'function') { const reply = await Promise.resolve(command.onRunError(context, args, error)); this.storeReply(message.id, command, context, reply); } } } catch (error) { if (typeof (command.onError) === 'function') { await Promise.resolve(command.onError(context, args, error)); } const payload = { args, command, context, error, prefix }; this.emit(constants_1.ClientEvents.COMMAND_FAIL, payload); } } async handleDelete(name, deletePayload) { const messageId = deletePayload.raw.id; if (this.replies.has(messageId)) { const { command, context, reply } = this.replies.get(messageId); this.replies.delete(messageId); const payload = { command, context, reply }; this.emit(constants_1.ClientEvents.COMMAND_DELETE, payload); } else { for (let [commandId, { command, context, reply }] of this.replies) { if (reply.id === messageId) { this.replies.delete(commandId); const payload = { command, context, reply }; this.emit(constants_1.ClientEvents.COMMAND_RESPONSE_DELETE, payload); } } } } on(event, listener) { super.on(event, listener); return this; } once(event, listener) { super.once(event, listener); return this; } subscribe(event, listener) { return super.subscribe(event, listener); } } exports.CommandClient = CommandClient;