UNPKG

commandbot

Version:

A framework that helps you create your own Discord bot easier.

549 lines (548 loc) 26.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CommandManager = void 0; const axios_1 = require("axios"); const discord_js_1 = require("discord.js"); const Parameter_js_1 = require("./Parameter.js"); const errors_js_1 = require("../errors.js"); const state_js_1 = require("../state.js"); const ChatCommand_js_1 = require("../commands/ChatCommand.js"); const ContextMenuCommand_js_1 = require("../commands/ContextMenuCommand.js"); const commandsTypes_js_1 = require("../commands/commandsTypes.js"); const SubCommand_js_1 = require("../commands/SubCommand.js"); const SubCommandGroup_js_1 = require("../commands/SubCommandGroup.js"); const Help_js_1 = require("../commands/Help.js"); const PrefixManager_js_1 = require("./PrefixManager.js"); const InputManager_js_1 = require("./InputManager.js"); /** * Object that stores the registered commands and is responsible for data exchanging with the Discord API * @class */ class CommandManager { /** * * @constructor * @param {Bot} client - client that this manager belongs to * @param {HelpMessageParams} helpMsg - parameters defining appearance of the help message * @param {?string} [prefix] - prefix used to respond to message interactions * @param {?string} [argSep=','] - a string used to split all incoming input data from Discord messages * @param {?string} [cmdSep='/'] - a string used to separate subcommand groups and subcommands */ constructor(client, helpMsg, prefix, argSep, cmdSep) { /** * List of commands registered in the manager * @type {Array<Command>} * @private * @readonly */ this._commands = []; /** * Cache of Discord API commands data * @type {Map<string, Map<string, RegisteredCommandObject>>} * @private * @readonly */ this._registerCache = new Map(); this._globalEntryName = "global"; if ((argSep && !commandsTypes_js_1.CommandRegExps.separator.test(argSep)) || (cmdSep && !commandsTypes_js_1.CommandRegExps.separator.test(cmdSep))) { throw new Error("Incorrect separators"); } this.client = client; this.prefix = new PrefixManager_js_1.PrefixManager(this, prefix); this.argumentSeparator = argSep || ","; this.commandSeparator = cmdSep || "/"; if (this.commandSeparator === this.argumentSeparator) { throw new Error("Command separator and argument separator have the same value"); } if (helpMsg.enabled === true) { this.help = new Help_js_1.HelpMessage(this, helpMsg); this._commands.push(this.help); } } /** * Discord API commands cache * @type {Map<string, Map<string, RegisteredCommandObject>>} */ get cache() { return this._registerCache; } /** * Number of commands registered in this manager * @type {number} */ get commandsCount() { return this._commands.length; } /** * Creates and registers command in the manager based on the given options * @param {T} type - a type of command that will be created and added to this manager * @param {CommandInit<T>} options - an object containing all properties required to create this type of command * @returns {Commands<T>} A computed command object that inherits from {@link Command} * @public * @remarks All commands have to be added to the instance **before starting the bot**. Adding commands while the bot is running is not possible and can cause issues. * * Command types * - [CHAT](https://grz4na.github.io/commandbot-docs/interfaces/ChatCommandInit.html) - message interactions using command prefixes or slash commands * - [USER](https://grz4na.github.io/commandbot-docs/interfaces/ContextMenuCommandInit.html) - right-click context menu interactions on users * - [MESSAGE](https://grz4na.github.io/commandbot-docs/interfaces/ContextMenuCommandInit.html) - right-click context menu interactions on messages */ add(type, options) { const command = type === "CHAT" ? new ChatCommand_js_1.ChatCommand(this, options) : type === "CONTEXT" ? new ContextMenuCommand_js_1.ContextMenuCommand(this, options) : null; if (!command) { throw new TypeError("Incorrect command type"); } if (state_js_1.applicationState.running) { console.warn(`[❌ ERROR] Cannot add command "${command.name}" while the application is running.`); return command; } if (command instanceof SubCommand_js_1.SubCommand || command instanceof SubCommandGroup_js_1.SubCommandGroup) { throw new Error("Registering subcommands and subcommand groups through the 'add' method is not allowed. Use NestedCommand.append or SubCommandGroup.append to register."); } this._commands.push(command); return command; } /** * Get command registered in this manager * @param {string} q - command name or alias * @param {?APICommandType} [t] - type of command you want to get from this manager (if *undefined* searches in all registered commands) * @returns {?Command} A command object * @public */ get(q, t) { var _a, _b; switch (t) { case "CHAT": const cmdList = this.list(t); return ((_b = (_a = cmdList.find((c) => c.name === q || (c.aliases && c.aliases.length > 0 && c.aliases.find((a) => a === q)))) !== null && _a !== void 0 ? _a : cmdList .filter((c) => c.hasSubCommands) .map((c) => c.children.map((ch) => { if (ch instanceof SubCommand_js_1.SubCommand) return ch; else return ch.children; })) .flat(2) .find((c) => { var _a; return (_a = c.aliases) === null || _a === void 0 ? void 0 : _a.find((a) => a === q); })) !== null && _b !== void 0 ? _b : null); case "NESTED": return (this.list(t).find((c) => c.name === q) || null); case "CONTEXT": return this.list(t).find((c) => c.name === q) || null; default: return this.list().find((c) => c.name === q) || null; } } /** * Fetches command object from the Discord API * @param {string} id - Discord command ID * @param {?Guild | string} [guild] - ID of guild that this command belongs to * @param {?boolean} [noCache=false] - whether not to use cached data * @returns {Promise<RegisteredCommandObject>} Discord command object * @public * @async */ getApi(id, guild, noCache) { return __awaiter(this, void 0, void 0, function* () { const guildId = guild instanceof discord_js_1.Guild ? guild.id : guild; if (!noCache) { const rqC = this.getCache(id, guildId); if (rqC) return rqC; } let rq; if (guildId) { rq = yield axios_1.default.get(`${CommandManager.baseApiUrl}/applications/${this.client.applicationId}/guilds/${guildId}/commands/${id}`, { headers: { Authorization: `Bot ${this.client.token}` }, }); } else { rq = yield axios_1.default.get(`${CommandManager.baseApiUrl}/applications/${this.client.applicationId}/commands/${id}`, { headers: { Authorization: `Bot ${this.client.token}` }, }); } if (rq.status === 200) { this.updateCache(rq.data); return rq.data; } else { throw new Error(`HTTP request failed with code ${rq.status}: ${rq.statusText}`); } }); } /** * Fetches command ID by name from the Discord APi * @param {string} name - name of the command * @param {string} type - command type you want to get ID for * @param {?string} [guild] - ID of guild that this command belongs to * @returns {string} Command ID from the Discord API * @public * @async */ getIdApi(name, type, guild) { return __awaiter(this, void 0, void 0, function* () { let map = yield this.listApi(guild); let result = null; map === null || map === void 0 ? void 0 : map.forEach((c) => { const typeC = c.type === 1 ? "CHAT_INPUT" : c.type === 2 ? "USER" : "MESSAGE"; if (c.name === name && typeC === type) { result = c.id; } }); return result; }); } list(f) { switch (f) { case "CHAT": return Object.freeze([...this._commands.filter((c) => c.type === "CHAT")]); case "CONTEXT": return Object.freeze([...this._commands.filter((c) => c.type === "CONTEXT")]); default: return Object.freeze([...this._commands]); } } /** * Lists commands registered in the Discord API * @param {Guild | string} [g] - Guild object or ID * @returns {Promise<Map<string, RegisteredCommandObject>>} List of commands from Discord API * @public * @async */ listApi(g) { return __awaiter(this, void 0, void 0, function* () { const guildId = g instanceof discord_js_1.Guild ? g.id : g; let rq; if (guildId) { rq = yield axios_1.default.get(`${CommandManager.baseApiUrl}/applications/${this.client.applicationId}/guilds/${guildId}/commands`, { headers: { Authorization: `Bot ${this.client.token}` }, }); } else { rq = yield axios_1.default.get(`${CommandManager.baseApiUrl}/applications/${this.client.applicationId}/commands`, { headers: { Authorization: `Bot ${this.client.token}` }, }); } if (rq.status === 200) { this.updateCache(rq.data, guildId); return this.arrayToMap(rq.data); } else { throw new Error(`HTTP request failed with code ${rq.status}: ${rq.statusText}`); } }); } /** * Process an interaction * @param {Interaction | Message} i - interaction object to fetch a command from * @returns {?InputManager} An InputManager containing all input data (command, arguments, target etc.) * @public */ fetch(i) { const prefix = this.prefix.get(i.guild || undefined); if (i instanceof discord_js_1.Interaction) { if (i.isCommand()) { const cmd = this.get(i.commandName, "CHAT"); if (cmd instanceof ChatCommand_js_1.ChatCommand) { if (cmd.hasSubCommands) { const subCmd = cmd.fetchSubcommand([...i.options.data], i); if (subCmd) return subCmd; } return new InputManager_js_1.InputManager(cmd, i, cmd.parameters.map((p, index) => { var _a, _b, _c, _d, _e, _f; if (p.type === "user" || p.type === "role" || p.type === "channel" || p.type === "mentionable") { return new Parameter_js_1.InputParameter(p, new Parameter_js_1.ObjectID((_c = (_b = (_a = i.options.data[index]) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.toString()) !== null && _c !== void 0 ? _c : "", p.type, (_d = i.guild) !== null && _d !== void 0 ? _d : undefined)); } else { return new Parameter_js_1.InputParameter(p, (_f = (_e = i.options.data[index]) === null || _e === void 0 ? void 0 : _e.value) !== null && _f !== void 0 ? _f : null); } })); } else { throw new errors_js_1.CommandNotFound(i.commandName); } } else if (i.isContextMenu()) { const cmd = this.get(i.commandName, "CONTEXT"); if (cmd) { const target = new Parameter_js_1.TargetID(i.targetId, i.targetType, i); return new InputManager_js_1.InputManager(cmd, i, [], target); } else { throw new errors_js_1.CommandNotFound(i.commandName); } } else { return null; } } else if (prefix && i instanceof discord_js_1.Message) { if (i.content.startsWith(prefix)) { if (i.content === prefix) return null; const cmdName = i.content.replace(prefix, "").split(" ")[0].split(this.commandSeparator)[0]; const cmd = this.get(cmdName, "CHAT"); if (cmd instanceof ChatCommand_js_1.ChatCommand) { const argsRaw = i.content .replace(`${prefix}${cmdName}`, "") .split(this.argumentSeparator) .map((a) => { if (a.startsWith(" ")) { return a.replace(" ", ""); } else { return a; } }); if (cmd.hasSubCommands) { const nesting = i.content.split(" ")[0].replace(`${prefix}${cmdName}${this.commandSeparator}`, "").split(this.commandSeparator); const subCmd = cmd.getSubcommand(nesting[1] ? nesting[1] : nesting[0], nesting[1] ? nesting[0] : undefined); if (subCmd) { const subArgsRaw = i.content .replace(`${prefix}${cmdName}${this.commandSeparator}${nesting.join(this.commandSeparator)}`, "") .split(this.argumentSeparator) .map((a) => { if (a.startsWith(" ")) { return a.replace(" ", ""); } else { return a; } }); return new InputManager_js_1.InputManager(subCmd, i, subCmd.parameters.map((p, index) => { var _a, _b, _c; if (p.type === "user" || p.type === "role" || p.type === "channel" || p.type === "mentionable") { return new Parameter_js_1.InputParameter(p, new Parameter_js_1.ObjectID((_a = subArgsRaw[index]) !== null && _a !== void 0 ? _a : "", p.type, (_b = i.guild) !== null && _b !== void 0 ? _b : undefined)); } else { return new Parameter_js_1.InputParameter(p, (_c = subArgsRaw[index]) !== null && _c !== void 0 ? _c : null); } })); } } return new InputManager_js_1.InputManager(cmd, i, cmd.parameters.map((p, index) => { var _a; if (p.type === "user" || p.type === "role" || p.type === "channel" || p.type === "mentionable") { return new Parameter_js_1.InputParameter(p, new Parameter_js_1.ObjectID(argsRaw[index], p.type, (_a = i.guild) !== null && _a !== void 0 ? _a : undefined)); } else { return new Parameter_js_1.InputParameter(p, argsRaw[index]); } })); } else if (cmd instanceof SubCommand_js_1.SubCommand) { const subArgsRaw = i.content .replace(`${prefix}${cmdName}`, "") .split(this.argumentSeparator) .map((a) => { if (a.startsWith(" ")) { return a.replace(" ", ""); } else { return a; } }); return new InputManager_js_1.InputManager(cmd, i, cmd.parameters.map((p, index) => { var _a, _b, _c; if (p.type === "user" || p.type === "role" || p.type === "channel" || p.type === "mentionable") { return new Parameter_js_1.InputParameter(p, new Parameter_js_1.ObjectID((_a = subArgsRaw[index]) !== null && _a !== void 0 ? _a : "", p.type, (_b = i.guild) !== null && _b !== void 0 ? _b : undefined)); } else { return new Parameter_js_1.InputParameter(p, (_c = subArgsRaw[index]) !== null && _c !== void 0 ? _c : null); } })); } else { throw new errors_js_1.CommandNotFound(cmdName); } } else { return null; } } else { return null; } } /** * Register all commands in this manager in the Discord API * @returns {Promise<void>} * @public * @async */ register() { return __awaiter(this, void 0, void 0, function* () { const globalCommands = this._commands .filter((c) => { if (c.isBaseCommandType("GUILD") && (!Array.isArray(c.guilds) || c.guilds.length === 0)) { if (c.isCommandType("CHAT") && c.slash === false) { return false; } else { return true; } } }) .map((c) => c.toObject()); const guildCommands = new Map(); this._commands .filter((c) => c.isBaseCommandType("GUILD") && Array.isArray(c.guilds) && c.guilds.length > 0) .map((c) => { var _a; c.isBaseCommandType("GUILD") && ((_a = c.guilds) === null || _a === void 0 ? void 0 : _a.map((gId) => { if (!this.client.client.guilds.cache.get(gId)) { throw new Error(`"${gId}" is not a valid ID for this client.`); } const existingEntry = guildCommands.get(gId); if (!existingEntry) { guildCommands.set(gId, [c.toObject()]); } else { guildCommands.set(gId, [...existingEntry, c.toObject()]); } })); }); yield axios_1.default .put(`${CommandManager.baseApiUrl}/applications/${this.client.applicationId}/commands`, globalCommands, { headers: { Authorization: `Bot ${this.client.token}` }, }) .then((r) => { if (r.status === 429) { console.error("[❌ ERROR] Failed to register application commands. You are being rate limited."); } }) .catch((e) => console.error(e)); yield guildCommands.forEach((g, k) => __awaiter(this, void 0, void 0, function* () { yield axios_1.default .put(`${CommandManager.baseApiUrl}/applications/${this.client.applicationId}/guilds/${k}/commands`, g, { headers: { Authorization: `Bot ${this.client.token}` }, }) .then((r) => { if (r.status === 429) { console.error(`[❌ ERROR] Failed to register application commands for guild ${k}. You are being rate limited.`); } }) .catch((e) => console.error(e)); })); }); } /** * Set permissions using Discord Permissions API * @param {string} id - command ID * @param {CommandPermission[]} permissions - permissions to set * @param {Guild | string} [g] - Guild ID or object (if command is in a guild) * @returns {Promise<void>} * @public * @async * @experimental This functionality hasn't been polished and fully tested yet. Using it might lead to errors and application crashes. */ setPermissionsApi(id, permissions, g) { return __awaiter(this, void 0, void 0, function* () { if (typeof g === "string" && !this.client.client.guilds.cache.get(g)) throw new Error(`${g} is not a valid guild id`); const response = yield axios_1.default.put(`${CommandManager.baseApiUrl}/applications/${this.client.applicationId}/${g ? (g instanceof discord_js_1.Guild ? `guilds/${g.id}` : g) : ""}commands/${id}/permissions`, { permissions: permissions, }, { headers: { Authorization: `Bot ${this.client.token}`, }, }); if (response.status !== 200) { throw new Error(`HTTP request failed with code ${response.status}: ${response.statusText}`); } }); } /** * Get permissions from Discord Permissions API for a specified command * @param {string} id - command ID * @param {Guild | string} [g] - Guild ID or object (if command is in a guild) * @public * @async * @experimental This functionality hasn't been polished and fully tested yet. Using it might lead to errors and application crashes. */ getPermissionsApi(id, g) { return __awaiter(this, void 0, void 0, function* () { if (typeof g === "string" && !this.client.client.guilds.cache.get(g)) throw new Error(`${g} is not a valid guild id`); const response = yield axios_1.default.get(`${CommandManager.baseApiUrl}/applications/${this.client.applicationId}/${g ? (g instanceof discord_js_1.Guild ? `guilds/${g.id}` : g) : ""}commands/${id}/permissions`, { headers: { Authorization: `Bot ${this.client.token}`, }, }); if (response.status !== 200) { throw new Error(`HTTP request failed with code ${response.status}: ${response.statusText}`); } return response.data; }); } /** * * @param {Array<RegisteredCommandObject>} commands - list of commands to cache * @param {?string} guildId - guild ID * @returns {void} * @private */ updateCache(commands, guildId) { var _a; if (Array.isArray(commands)) { this._registerCache.set(guildId || this._globalEntryName, this.arrayToMap(commands)); return; } else { (_a = this._registerCache.get(guildId || this._globalEntryName)) === null || _a === void 0 ? void 0 : _a.set(commands.id, commands); } } /** * Retrieves cache from the manager * @param {string} q * @param {?string} guildId * @returns {?RegisteredCommandObject} */ getCache(q, guildId) { var _a; return ((_a = this._registerCache.get(guildId || this._globalEntryName)) === null || _a === void 0 ? void 0 : _a.get(q)) || null; } /** * Performs internal data type conversions * @param {Array<RegisteredCommandObject>} a * @returns {Map<string, RegisteredCommandObject>} */ arrayToMap(a) { const map = new Map(); a.map((rc) => { map.set(rc.id, rc); }); return map; } /** * @param {any} c - object to check * @returns {boolean} Whether this object is a {@link Command} object * @public * @static */ static isCommand(c) { return "name" in c && "type" in c && "default_permission" in c && (c.type === "CHAT" || c.type === "CONTEXT"); } } exports.CommandManager = CommandManager; /** * Discord API URL * @type {string} * @public * @static * @readonly */ CommandManager.baseApiUrl = "https://discord.com/api/v8";