UNPKG

detritus-client

Version:

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

915 lines (914 loc) 38.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.InteractionCommandClient = void 0; const path = require("path"); const detritus_utils_1 = require("detritus-utils"); const client_1 = require("./client"); const clusterclient_1 = require("./clusterclient"); const collections_1 = require("./collections"); const commandclient_1 = require("./commandclient"); const constants_1 = require("./constants"); const errors_1 = require("./errors"); const structures_1 = require("./structures"); const utils_1 = require("./utils"); const commandratelimit_1 = require("./commandratelimit"); const interaction_1 = require("./interaction"); /** * Interaction Command Client, hooks onto a ClusterClient or ShardClient to provide easier command handling * Flow is `onInteractionCheck` -> `onCommandCheck` * @category Clients */ class InteractionCommandClient extends detritus_utils_1.EventSpewer { constructor(token, options = {}) { super(); this._clientSubscriptions = []; this.checkCommands = true; this.commands = new collections_1.BaseSet(); this.commandsById = new collections_1.BaseCollection(); this.directories = new collections_1.BaseCollection(); this.ran = false; this.ratelimits = []; this.strictCommandCheck = true; options = Object.assign({ useClusterClient: true }, options); this.checkCommands = (options.checkCommands || options.checkCommands === undefined); this.ratelimiter = options.ratelimiter || new commandratelimit_1.CommandRatelimiter(); this.strictCommandCheck = (options.strictCommandCheck || options.strictCommandCheck === undefined); this.onCommandCheck = options.onCommandCheck || this.onCommandCheck; this.onCommandCancel = options.onCommandCancel || this.onCommandCancel; this.onInteractionCheck = options.onInteractionCheck || this.onInteractionCheck; this.onInteractionCancel = options.onInteractionCancel || this.onInteractionCancel; if (token instanceof commandclient_1.CommandClient) { 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, 'interactionCommandClient', { value: this }); if (this.client instanceof clusterclient_1.ClusterClient) { for (let [shardId, shard] of this.client.shards) { Object.defineProperty(shard, 'interactionCommandClient', { value: this }); } } 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)); } } Object.defineProperties(this, { _clientSubscriptions: { enumerable: false, writable: false }, ran: { configurable: true, writable: false }, onCommandCheck: { enumerable: false, writable: true }, onCommandCancel: { enumerable: false, writable: true }, onInteractionCheck: { enumerable: false, writable: true }, onInteractionCancel: { enumerable: false, writable: true }, }); } get canUpload() { if (this.manager) { // only upload on the first cluster process return this.manager.clusterId === 0; } return true; } get manager() { return (this.client instanceof clusterclient_1.ClusterClient) ? this.client.manager : null; } get rest() { return this.client.rest; } /* Generic Command Function */ add(options, run) { let command; if (options instanceof interaction_1.InteractionCommand) { command = options; } else { if (run !== undefined) { options.run = run; } // create a normal command class with the options given if (options._class === undefined) { command = new interaction_1.InteractionCommand(options); } else { // check for `.constructor` to make sure it's a class if (options._class.constructor) { command = new options._class(options); } else { // else it's just a function, `ts-node` outputs these command = options._class(options); } if (!command._file) { Object.defineProperty(command, '_file', { value: options._file }); } } } command._transferValuesToChildren(); if (!command.hasRun) { throw new Error('Command needs a run function'); } this.commands.add(command); const guildIds = (command.guildIds) ? command.guildIds.toArray() : []; if (command.global) { guildIds.unshift(constants_1.LOCAL_GUILD_ID); } for (let guildId of guildIds) { let commands; if (this.commandsById.has(guildId)) { commands = this.commandsById.get(guildId); } else { commands = new collections_1.BaseSet(); this.commandsById.set(guildId, commands); } commands.add(command); } 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 interaction_1.InteractionCommand) { 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.clear(); for (let [guildId, commands] of this.commandsById) { commands.clear(); this.commandsById.delete(guildId); } this.commandsById.clear(); 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 }); } await this.checkAndUploadCommands(); } /* Application Command Checking */ async checkApplicationCommands(guildId) { if (!this.client.ran) { return false; } const commands = await this.fetchApplicationCommands(guildId); return this.validateCommands(commands); } async checkAndUploadCommands(force = false) { if (!this.client.ran) { return; } for (let [guildId, localCommands] of this.commandsById) { const guildIdOrUndefined = (guildId === constants_1.LOCAL_GUILD_ID) ? undefined : guildId; if (!await this.checkApplicationCommands(guildIdOrUndefined) && (force || this.canUpload)) { const commands = await this.uploadApplicationCommands(guildIdOrUndefined); this.validateCommands(commands); if (this.manager && this.manager.hasMultipleClusters) { this.manager.sendIPC(constants_1.ClusterIPCOpCodes.FILL_INTERACTION_COMMANDS, { data: commands }); } } } } createApplicationCommandsFromRaw(data) { const collection = new collections_1.BaseCollection(); const shard = (this.client instanceof clusterclient_1.ClusterClient) ? this.client.shards.first() : this.client; for (let raw of data) { const command = new structures_1.ApplicationCommand(shard, raw); collection.set(command.id, command); } return collection; } async fetchApplicationCommands(guildId) { // add ability for ClusterManager checks if (!this.client.ran) { throw new Error('Client hasn\'t ran yet so we don\'t know our application id!'); } let data; if (this.manager && this.manager.hasMultipleClusters) { if (guildId) { data = await this.manager.sendRestRequest('fetchApplicationGuildCommands', [this.client.applicationId, guildId]); } else { data = await this.manager.sendRestRequest('fetchApplicationCommands', [this.client.applicationId]); } } else { if (guildId) { data = await this.rest.fetchApplicationGuildCommands(this.client.applicationId, guildId); } else { data = await this.rest.fetchApplicationCommands(this.client.applicationId); } } return this.createApplicationCommandsFromRaw(data); } async uploadApplicationCommands(guildId) { // add ability for ClusterManager if (!this.client.ran) { throw new Error('Client hasn\'t ran yet so we don\'t know our application id!'); } const localCommands = (this.commandsById.get(guildId || constants_1.LOCAL_GUILD_ID) || []).map((command) => { const data = command.toJSON(); data[constants_1.DiscordKeys.ID] = command.ids.get(guildId || constants_1.LOCAL_GUILD_ID); data[constants_1.DiscordKeys.IDS] = undefined; return data; }); const shard = (this.client instanceof clusterclient_1.ClusterClient) ? this.client.shards.first() : this.client; if (guildId) { return shard.rest.bulkOverwriteApplicationGuildCommands(this.client.applicationId, guildId, localCommands); } else { return shard.rest.bulkOverwriteApplicationCommands(this.client.applicationId, localCommands); } } validateCommands(commands) { if (!commands.length) { return true; } const guildId = commands.first().guildId || constants_1.LOCAL_GUILD_ID; const localCommands = this.commandsById.get(guildId); if (localCommands) { let matches = commands.length === localCommands.length; for (let [commandId, command] of commands) { const localCommand = localCommands.find((cmd) => cmd.name === command.name && cmd.type === command.type); if (localCommand) { localCommand.ids.set(guildId, command.id); if (matches && localCommand.hash !== command.hash) { matches = false; } } else { matches = false; } } return matches; } return false; } validateCommandsFromRaw(data) { const collection = this.createApplicationCommandsFromRaw(data); return this.validateCommands(collection); } /* end */ async parseArgs(context, data) { if (data.isSlashCommand) { return this.parseArgsFromOptions(context, context.command._options, data.options, data.resolved); } else if (data.isContextCommand) { return this.parseArgsFromContextMenu(data); } return [{}, null]; } async parseArgsFromContextMenu(data) { const args = {}; if (data.targetId && data.resolved) { switch (data.type) { case constants_1.ApplicationCommandTypes.MESSAGE: { if (data.resolved.messages) { args.message = data.resolved.messages.get(data.targetId); } } ; break; case constants_1.ApplicationCommandTypes.USER: { if (data.resolved.members) { args.member = data.resolved.members.get(data.targetId); } if (data.resolved.users) { args.user = data.resolved.users.get(data.targetId); } } ; break; } } return [args, null]; } async parseArgsFromOptions(context, commandOptions, options, resolved) { const args = {}; const errors = {}; /* We will get data like this { id: '', name: '', options: [ { name: '', options: [ { name: '', options: [ {name: '', type: int, value: any}, ], type: 1, } ], type: 2, }, ], type: 1, } { id: '', name: '', options: [ { name: '', options: [ { name: '', type: 1, } ], type: 2, }, ], type: 1, } { id: '', name: '', type: 1, } Non-required options wont be there so we must look through our command and fill out the defaults */ let hasError = false; if (options) { for (let [name, option] of options) { let commandOption; if (commandOptions && commandOptions.has(name)) { commandOption = commandOptions.get(name); } if (option.options) { const [childArgs, childErrors] = await this.parseArgsFromOptions(context, commandOption && commandOption._options, option.options, resolved); Object.assign(args, childArgs); if (childErrors) { hasError = true; Object.assign(errors, childErrors); } } else if (option.value !== undefined) { let value = option.value; if (resolved) { switch (option.type) { case constants_1.ApplicationCommandOptionTypes.CHANNEL: { if (resolved.channels) { value = resolved.channels.get(value) || value; } } ; break; case constants_1.ApplicationCommandOptionTypes.BOOLEAN: value = Boolean(value); break; case constants_1.ApplicationCommandOptionTypes.INTEGER: value = parseInt(value); break; case constants_1.ApplicationCommandOptionTypes.MENTIONABLE: { if (resolved.roles && resolved.roles.has(value)) { value = resolved.roles.get(value); } else if (resolved.members && resolved.members.has(value)) { value = resolved.members.get(value); } else if (resolved.users && resolved.users.has(value)) { value = resolved.users.get(value); } } ; break; case constants_1.ApplicationCommandOptionTypes.ROLE: { if (resolved.roles) { value = resolved.roles.get(value) || value; } } ; break; case constants_1.ApplicationCommandOptionTypes.USER: { if (resolved.members) { value = resolved.members.get(value) || value; } else if (resolved.users) { value = resolved.users.get(value) || value; } } ; break; } } const label = (commandOption) ? commandOption.label || name : name; if (commandOption) { if (commandOption.value && typeof (commandOption.value) === 'function') { try { args[label] = await Promise.resolve(commandOption.value(value, context)); } catch (error) { hasError = true; errors[label] = error; } } else { args[label] = value; } } else { args[label] = value; } } else if (commandOption && commandOption._options) { hasError = (await this.parseDefaultArgsFromOptions(context, commandOption._options, args, errors)) || hasError; } } } if (commandOptions) { hasError = (await this.parseDefaultArgsFromOptions(context, commandOptions, args, errors)) || hasError; } return [args, (hasError) ? errors : null]; } async parseDefaultArgsFromOptions(context, commandOptions, args = {}, errors = {}) { let hasError = false; for (let [name, commandOption] of commandOptions) { if (commandOption.isSubCommand || commandOption.isSubCommandGroup) { continue; } const label = commandOption.label || name; if (commandOption.default !== undefined && !(label in args) && !(label in errors)) { if (typeof (commandOption.default) === 'function') { try { args[label] = await Promise.resolve(commandOption.default(context)); } catch (error) { hasError = true; errors[label] = error; } } else { args[label] = commandOption.default; } } } return hasError; } setSubscriptions() { this.clearSubscriptions(); const subscriptions = this._clientSubscriptions; subscriptions.push(this.client.subscribe(constants_1.ClientEvents.INTERACTION_CREATE, this.handleInteractionCreate.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); if (this.checkCommands) { await this.checkAndUploadCommands(); } Object.defineProperty(this, 'ran', { value: true }); return this.client; } async handleInteractionCreate(event) { return this.handle(constants_1.ClientEvents.INTERACTION_CREATE, event); } async handle(name, event) { const { interaction } = event; if (interaction.type !== constants_1.InteractionTypes.APPLICATION_COMMAND) { return; } // assume the interaction is global for now const data = interaction.data; let command; const guildIds = (interaction.guildId) ? [interaction.guildId, constants_1.LOCAL_GUILD_ID] : [constants_1.LOCAL_GUILD_ID]; for (let guildId of guildIds) { if (this.commandsById.has(guildId)) { const localCommands = this.commandsById.get(guildId); if (this.strictCommandCheck) { command = localCommands.find((cmd) => cmd.ids.get(guildId) === data.id); } else { command = localCommands.find((cmd) => cmd.name === data.name && cmd.type === data.type); } if (command) { break; } } } if (!command) { return; } const invoker = command.getInvoker(data); if (!invoker) { return; } const context = new interaction_1.InteractionContext(this, interaction, command, invoker); if (typeof (this.onInteractionCheck) === 'function') { try { const shouldContinue = await Promise.resolve(this.onInteractionCheck(context)); if (!shouldContinue) { if (typeof (this.onInteractionCancel) === 'function') { return await Promise.resolve(this.onInteractionCancel(context)); } return; } } catch (error) { const payload = { command, context, error }; this.emit(constants_1.ClientEvents.COMMAND_ERROR, payload); return; } } 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)); } return; } } catch (error) { const payload = { command, context, error }; this.emit(constants_1.ClientEvents.COMMAND_ERROR, payload); return; } } if (this.ratelimits.length || (invoker.ratelimits && invoker.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 (invoker.onRatelimit) === 'function') { try { await Promise.resolve(invoker.onRatelimit(context, ratelimits, { global, now })); } catch (error) { // do something with this error? } } return; } } if (invoker.ratelimits && invoker.ratelimits.length) { const ratelimits = this.ratelimiter.getExceeded(context, invoker.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 (invoker.onRatelimit) === 'function') { try { await Promise.resolve(invoker.onRatelimit(context, ratelimits, { global, now })); } catch (error) { // do something with this error? } } return; } } } if (context.inDm) { // dm checks? maybe add ability to disable it in dm? if (invoker.disableDm) { if (typeof (invoker.onDmBlocked) === 'function') { try { await Promise.resolve(invoker.onDmBlocked(context)); } 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'); const payload = { command, context, error }; 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(invoker.permissionsClient) && invoker.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 invoker.permissionsClient) { if (!utils_1.PermissionTools.checkPermissions(total, permission)) { failed.push(permission); } } } } else { for (let permission of invoker.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 (invoker.onPermissionsFailClient) === 'function') { try { await Promise.resolve(invoker.onPermissionsFailClient(context, failed)); } 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 (!invoker.permissionsIgnoreClientOwner || !context.user.isClientOwner) { // check the user's permissions if (Array.isArray(invoker.permissions) && invoker.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 invoker.permissions) { if (!utils_1.PermissionTools.checkPermissions(total, permission)) { failed.push(permission); } } } } else { for (let permission of invoker.permissions) { failed.push(permission); } } if (failed.length) { const payload = { command, context, permissions: failed }; this.emit(constants_1.ClientEvents.COMMAND_PERMISSIONS_FAIL, payload); if (typeof (invoker.onPermissionsFail) === 'function') { try { await Promise.resolve(invoker.onPermissionsFail(context, failed)); } catch (error) { // do something with this error? } } return; } } } } if (typeof (invoker.onBefore) === 'function') { try { const shouldContinue = await Promise.resolve(invoker.onBefore(context)); if (!shouldContinue) { if (typeof (invoker.onCancel) === 'function') { await Promise.resolve(invoker.onCancel(context)); } return; } } catch (error) { const payload = { command, context, error }; this.emit(constants_1.ClientEvents.COMMAND_ERROR, payload); return; } } let timeout = null; try { if (invoker.triggerLoadingAfter !== undefined && 0 <= invoker.triggerLoadingAfter && !context.responded) { let data; if (invoker.triggerLoadingAsEphemeral) { data = { flags: constants_1.MessageFlags.EPHEMERAL }; } if (invoker.triggerLoadingAfter) { timeout = new detritus_utils_1.Timers.Timeout(); Object.defineProperty(context, 'loadingTimeout', { value: timeout }); timeout.start(invoker.triggerLoadingAfter, async () => { if (!context.responded) { try { if (typeof (invoker.onLoadingTrigger) === 'function') { await Promise.resolve(invoker.onLoadingTrigger(context)); } else { await context.respond(constants_1.InteractionCallbackTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, data); } } catch (error) { // do something maybe? } } }); } else { if (typeof (invoker.onLoadingTrigger) === 'function') { await Promise.resolve(invoker.onLoadingTrigger(context)); } else { await context.respond(constants_1.InteractionCallbackTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, data); } } } } catch (error) { // 404 interaction unknown most likely, do nothing } const [args, errors] = await this.parseArgs(context, data); try { if (errors) { if (typeof (invoker.onValueError) === 'function') { await Promise.resolve(invoker.onValueError(context, args, errors)); } 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; } if (typeof (invoker.onBeforeRun) === 'function') { const shouldRun = await Promise.resolve(invoker.onBeforeRun(context, args)); if (!shouldRun) { if (typeof (invoker.onCancelRun) === 'function') { await Promise.resolve(invoker.onCancelRun(context, args)); } return; } } try { if (typeof (invoker.run) === 'function') { await Promise.resolve(invoker.run(context, args)); } if (timeout) { timeout.stop(); } const payload = { args, command, context }; this.emit(constants_1.ClientEvents.COMMAND_RAN, payload); if (typeof (invoker.onSuccess) === 'function') { await Promise.resolve(invoker.onSuccess(context, args)); } } catch (error) { if (timeout) { timeout.stop(); } const payload = { args, command, context, error }; this.emit(constants_1.ClientEvents.COMMAND_RUN_ERROR, payload); if (typeof (invoker.onRunError) === 'function') { await Promise.resolve(invoker.onRunError(context, args, error)); } } } catch (error) { if (typeof (invoker.onError) === 'function') { await Promise.resolve(invoker.onError(context, args, error)); } const payload = { args, command, context, error }; this.emit(constants_1.ClientEvents.COMMAND_FAIL, 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.InteractionCommandClient = InteractionCommandClient;