@beni69/cmd
Version:
The command handler from my discord bot
489 lines • 21.6 kB
JavaScript
;
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