commandbot
Version:
A framework that helps you create your own Discord bot easier.
549 lines (548 loc) • 26.2 kB
JavaScript
;
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";