UNPKG

@beni69/cmd

Version:

The command handler from my discord bot

489 lines 21.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Handler = void 0; const discord_js_1 = require("discord.js"); const events_1 = __importDefault(require("events")); const mongoose_1 = __importDefault(require("mongoose")); const ms_1 = __importDefault(require("ms")); const path_1 = require("path"); const readdirp_1 = __importDefault(require("readdirp")); const HelpCommand_1 = require("./HelpCommand"); const Logging_1 = require("./Logging"); const models = __importStar(require("./Models")); const Trigger_1 = __importDefault(require("./Trigger")); const Utils_1 = require("./Utils"); class Handler extends events_1.default { /** * Create a new command handler * @param {HandlerConstructor} opts - Put all options in this object. Only the client, prefix and commands directory are requred, everthing else is optional. */ constructor({ client, prefix, commandsDir, verbose = false, admins = [], testServers = [], triggers = [], helpCommand, logging: loggerOptions, mongodb, blacklist, pauseCommand, ignoreBots = false, testMode = false, }) { super(); this.listening = false; this.paused = false; if (client.readyAt === null) throw new Error("The client must be ready when you create the handler."); this.client = client; this.commandsDir = (0, path_1.join)((0, path_1.dirname)(process.argv[1]), commandsDir); this.commands = new discord_js_1.Collection(); this.v = verbose; this.opts = { prefix, admins: new Set(admins), testServers: new Set(testServers), triggers: new discord_js_1.Collection(), helpCommand, blacklist: blacklist || [], pauseCommand, ignoreBots, testMode, }; //* setting up built-in modules // triggers triggers.forEach(item => this.opts.triggers.set(item[0], item[1])); // help command if (helpCommand) (0, HelpCommand_1.init)(this); // logging if (loggerOptions) this.logger = new Logging_1.Logger(client, loggerOptions); this.listening = false; this.paused = false; this.db = false; this.v && console.log("Command handler launching in verbose mode"); this.v && testMode && console.log("test mode: on"); // load the commands this.loadCommands(this.commandsDir); // connect to db and set up sync if (mongodb) this.dbConnect(mongodb); } get isPaused() { return this.paused; } set pause(v) { this.paused = v; } get getOpts() { return this.opts; } get getCommands() { return this.commands; } get getLogger() { return this.logger; } /** * **This will be called internally, you dont have to run this yourself** * Recursively reads a directory and loads all .js and .ts files * (if these files don't export a command they will just be ignored) * @param {string} dir - The directory to use */ async loadCommands(dir) { var _a, _b, _c, _d; const globalSlash = []; const testSlash = []; this.v && console.log(`Loading commands from: ${dir}`); for await (const entry of (0, readdirp_1.default)(dir, { fileFilter: ["*.js", "*.ts"], })) { this.v && console.log(`Loading command: ${entry.basename}`); // import the actual file const command = (await Promise.resolve().then(() => __importStar(require(entry.fullPath)))).command; if (!command) continue; if (!command.opts.category) { const r = new RegExp(/\\|\//, "g"); command.opts.category = entry.path.split(r).length > 1 ? entry.path.split(r).shift() : "No category"; } // error checking if (command === undefined) throw this.Error(`Couldn't import command from ${entry.path}. Make sure you are exporting a command variable that is a new Command`); if (this.getCommand(command.opts.names) !== undefined) throw this.Error(`Command name ${command.opts.names[0]} is being used twice!`); if (command.opts.adminOnly && this.opts.admins.size === 0) throw this.Error(`Command ${entry.path} is set to admin only, but no admins were defined.`); if (command.opts.test && this.opts.testServers.size === 0) throw this.Error(`Command ${entry.path} is set to test servers only but no test servers were defined.`); //* adding to commands this.commands.set(command.opts.names[0], command); //* registering the slash command if (!command.opts.noSlash) { if (!command.opts.description) throw this.Error(`${entry.path}: a description is required for slash commands! (and still recommended otherwise)`); if (this.opts.testMode || command.opts.test) { // only register in the test servers testSlash.push({ name: command.opts.names[0].toLowerCase(), description: command.opts.description, options: (_a = command.opts.options) !== null && _a !== void 0 ? _a : [], }); } else { // register globally globalSlash.push({ name: command.opts.names[0].toLowerCase(), description: command.opts.description, options: (_b = command.opts.options) !== null && _b !== void 0 ? _b : [], }); } } } this.v && console.log("Registering slash commands..."); // check if the registered commands are the same if (testSlash.length) { for (const GID of this.opts.testServers) { const guild = await this.client.guilds.fetch(GID); const c = await guild.commands.fetch(); if ((0, Utils_1.slashCommandsChanged)(c, testSlash)) await guild.commands.set(testSlash); else this.v && console.log(`skipping test slash commands in guild ${guild.name}`); } } else this.v && console.log("no test slash commands to register"); if (globalSlash.length) { const c = await ((_c = this.client.application) === null || _c === void 0 ? void 0 : _c.commands.fetch()); if ((0, Utils_1.slashCommandsChanged)(c, globalSlash)) await ((_d = this.client.application) === null || _d === void 0 ? void 0 : _d.commands.set(globalSlash)); else this.v && console.log("skipping global slash commands"); } else this.v && console.log("no global slash commands to register"); console.log(`Loaded ${this.commands.size} commands.`); this.v && console.log("Commands:", this.commands.map(item => item.opts.names[0])); // start listening to messages this.listen(); this.emit("ready"); } /** * Listen for messages */ listen() { // listen only once if (this.listening) return; this.listening = true; this.client.on("interactionCreate", async (interaction) => { if (!interaction.isCommand()) return; //* saving guild to db if (this.db && !(await models.guild.findById(interaction.guild.id))) { const g = new models.guild({ _id: interaction.guild.id, cooldowns: [], globalCooldowns: [], }); await g.save(); } const command = this.getCommand(interaction.commandName); if (!command || command.opts.noSlash) return; const trigger = new Trigger_1.default(this, command, interaction); if (await this.validateCommand(trigger)) await this.executeCommand(trigger); }); this.client.on("messageCreate", async (message) => { var _a; if ((this.paused && message.content != this.opts.prefix + this.opts.pauseCommand) || message.author.id === ((_a = this.client.user) === null || _a === void 0 ? void 0 : _a.id)) return; //* saving guild to db if (this.db && !(await models.guild.findById(message.guild.id))) { const g = new models.guild({ _id: message.guild.id, cooldowns: [], globalCooldowns: [], }); await g.save(); } //* reaction triggers for (const item of [...this.opts.triggers.keys()]) { if (message.content.toLowerCase().includes(item)) { const emoji = this.opts.triggers.get(item); message.react(emoji); } } //* prep to execute actual command if (!message.content.startsWith(this.opts.prefix)) return; const args = message.content .slice(this.opts.prefix.length) .trim() .split(/\s+/); // removes first item of args and that is the command name const commandName = args.shift().toLowerCase(); const command = this.getCommand(commandName); if (!command || command.opts.noClassic) return; const trigger = new Trigger_1.default(this, command, message); if (await this.validateCommand(trigger)) await this.executeCommand(trigger); }); } /** * Validate whether a user can execute a command in the provided context */ async validateCommand(trigger) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; const { command } = trigger; //* Classic only if (trigger.isClassic()) { const args = trigger.source.content .slice(this.opts.prefix.length) .trim() .split(/\s+/); const commandName = args.shift().toLowerCase(); // too many args if (command.opts.maxArgs && args.length > command.opts.maxArgs && command.opts.maxArgs > 0) { trigger.channel.send(((_a = this.opts.errMsg) === null || _a === void 0 ? void 0 : _a.tooManyArgs) || `Too many args. For more info, see: ${this.opts.prefix}help ${commandName}`); return; } // not enough args if (command.opts.minArgs && args.length < command.opts.minArgs) { trigger.channel.send(((_b = this.opts.errMsg) === null || _b === void 0 ? void 0 : _b.tooFewArgs) || `Not enough args. For more info, see: ${this.opts.prefix}help ${commandName}`); return; } } // command is test servers only if (command.opts.test && !this.opts.testServers.has(trigger.guild.id)) { console.log(`${trigger.author.tag} tried to use test command: ${command.opts.names[0]}`); return; } // command is admins only if (command.opts.adminOnly && !this.opts.admins.has(trigger.author.id)) { trigger.channel.send(((_c = this.opts.errMsg) === null || _c === void 0 ? void 0 : _c.noAdmin) || "You can't run this command!"); return; } // command not allowed in dms if (trigger.channel.type === "DM" && command.opts.noDM) { trigger.channel.send(((_d = this.opts.errMsg) === null || _d === void 0 ? void 0 : _d.noDM) || "You can't use this command in the dms"); return; } // user or guild is on blacklist if (this.opts.blacklist.includes(trigger.author.id) || ((_e = command.opts.blacklist) === null || _e === void 0 ? void 0 : _e.includes(trigger.author.id)) || this.opts.blacklist.includes((_f = trigger.guild) === null || _f === void 0 ? void 0 : _f.id) || ((_g = command.opts.blacklist) === null || _g === void 0 ? void 0 : _g.includes((_h = trigger.guild) === null || _h === void 0 ? void 0 : _h.id))) { trigger.channel.send(((_j = this.opts.errMsg) === null || _j === void 0 ? void 0 : _j.blacklist) || "You've been blacklisted from using this command"); return; } //* command is on cooldown if (trigger.channel.type != "DM" && (command.opts.cooldown > 0 || command.opts.globalCooldown > 0)) { if (trigger.isClassic() && trigger.source.webhookId) return; const guild = (await models.guild.findById(trigger.guild.id)); if (guild) { const CD = guild === null || guild === void 0 ? void 0 : guild.cooldowns.find(cd => cd.user == trigger.author.id && cd.command == command.opts.names[0]); if (CD && CD.expires > Date.now()) { const t = (0, ms_1.default)(CD.expires - Date.now(), { long: true }); trigger.channel.send(((_k = this.opts.errMsg) === null || _k === void 0 ? void 0 : _k.cooldown) || `This command is on cooldown for another ${t}.`); return; } const globalCD = guild === null || guild === void 0 ? void 0 : guild.globalCooldowns.find(cd => cd.command == command.opts.names[0]); if (globalCD && globalCD.expires > Date.now()) { const t = (0, ms_1.default)(globalCD.expires - Date.now(), { long: true }); trigger.channel.send(((_l = this.opts.errMsg) === null || _l === void 0 ? void 0 : _l.globalCooldown) || `This command is on cooldown for the entire server for another ${t}.`); return; } } } return true; } /** * Execute a command. * (this is the function used internally for launching the commands) * @param {Trigger} trigger - The trigger that lauhched the command * @returns true if successful, false if command falied */ async executeCommand(trigger) { const { command } = trigger; // not a command if (!command) return false; const args = trigger.content .slice(this.opts.prefix.length) .trim() .split(/\s+/); // removes first item of args and that is the command name const commandName = args.shift().toLowerCase(); // text is just all the args without the command name const text = trigger.content .substring(this.opts.prefix.length + commandName.length) .trim(); const { argv } = trigger; const { deferred, ephemeral } = command.opts; // defer interaction if (trigger.isSlash() && deferred) await trigger.source.deferReply({ ephemeral }); //* running the actual command const res = await command.run({ client: this.client, trigger: trigger, args, argv, prefix: this.opts.prefix, handler: this, text, logger: this.logger, }); if (trigger.isClassic() && command.opts.react && res) trigger.source.react(command.opts.react); //* log the command if (this.logger) this.logger.log(trigger); //* apply the cooldown (only on a successful command) if (res && (command.opts.cooldown || command.opts.globalCooldown)) { const guild = (await models.guild.findById(trigger.guild.id)); if (command.opts.cooldown) { // adding the cooldown guild === null || guild === void 0 ? void 0 : guild.cooldowns.push({ user: trigger.author.id, command: command.opts.names[0], expires: Date.now() + command.opts.cooldown, }); // removing the cooldown after it expired setTimeout(async () => { const g = (await models.guild.findById(trigger.guild.id)); const i = g === null || g === void 0 ? void 0 : g.cooldowns.findIndex(cd => cd.user == trigger.author.id && cd.command == command.opts.names[0]); if (i === -1) return; g === null || g === void 0 ? void 0 : g.cooldowns.splice(i, 1); await g.updateOne({ cooldowns: g.cooldowns }); }, command.opts.cooldown); } if (command.opts.globalCooldown) { guild === null || guild === void 0 ? void 0 : guild.globalCooldowns.push({ command: command.opts.names[0], expires: Date.now() + command.opts.globalCooldown, }); setTimeout(async () => { const g = (await models.guild.findById(trigger.guild.id)); const i = g === null || g === void 0 ? void 0 : g.globalCooldowns.findIndex(cd => cd.command == command.opts.names[0]); if (i === -1) return; g === null || g === void 0 ? void 0 : g.globalCooldowns.splice(i, 1); await g.updateOne({ globalCooldowns: g.globalCooldowns, }); }, command.opts.globalCooldown); } await guild.updateOne({ cooldowns: guild.cooldowns, globalCooldowns: guild.globalCooldowns, }); } return res !== null && res !== void 0 ? res : false; } /** * Find a command from any of its aliases * (this is the function used internally for finding commands) * @param {string} name - Name or names of a command * @returns The command or undefined if no command was found */ getCommand(name) { if (typeof name === "string") return (this.commands.get(name) || this.commands.find(c => c.opts.names.includes(name))); let found = undefined; name.forEach(item => { const res = this.getCommand(item); if (res !== undefined) found = res; }); return found; } /** * Connect to the database (for cooldowns) * @param {string} uri - MongoDB connection string */ async dbConnect(uri) { try { await mongoose_1.default.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, useCreateIndex: true, }); console.log("Handler connected to DB"); this.db = true; this.emit("dbConnected"); await (0, Utils_1.cleanDB)(); } catch (err) { console.error("Handler failed to connect to MongoDB: ", err.message); this.emit("dbConnectFailed", err); } } Error(msg) { return new Error(msg); } /** * A utility function to create nice embeds. * @param title * @param desc * @param color * @param thumbnail */ makeEmbed(title, desc, color, thumbnail) { var _a; const emb = new discord_js_1.MessageEmbed({ title, description: desc, }) .setAuthor(this.client.user.username, (_a = this.client.user) === null || _a === void 0 ? void 0 : _a.displayAvatarURL({ dynamic: true })) .setColor(color || "BLURPLE"); if (thumbnail) emb.setThumbnail(thumbnail); return emb; } } exports.Handler = Handler; exports.default = Handler; //# sourceMappingURL=Handler.js.map