"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 = __importDefault(require("discord.js"));
const events_1 = __importDefault(require("events"));
const mongoose_1 = __importDefault(require("mongoose"));
const path_1 = require("path");
const readdirp_1 = __importDefault(require("readdirp"));
const yargs_1 = __importDefault(require("yargs"));
const HelpCommand_1 = require("./HelpCommand");
const Logging_1 = require("./Logging");
const models = __importStar(require("./Models"));
const Reaction_1 = require("./Reaction");
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, }) {
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 = path_1.join(path_1.dirname(process.argv[1]), commandsDir);
this.commands = new discord_js_1.default.Collection();
this.v = verbose;
this.opts = {
prefix,
admins: new Set(admins),
testServers: new Set(testServers),
triggers: new discord_js_1.default.Collection(),
helpCommand,
blacklist: blacklist || [],
pauseCommand,
ignoreBots,
};
//* setting up built-in modules
// triggers
triggers.forEach(item => this.opts.triggers.set(item[0], item[1]));
// help command
if (helpCommand)
HelpCommand_1.init(this);
// logging
if (loggerOptions)
this.logger = new Logging_1.Logger(client, loggerOptions);
this.listening = false;
this.paused = false;
this.db = false;
if (this.v)
console.log("Command handler launching in verbose mode");
// 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;
}
/**
* 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
* @param {boolean} reload - Whether to clear the command list before reading (useful to reload the commands)
*/
async loadCommands(dir, reload = false) {
if (reload)
this.commands.clear();
if (this.v)
console.log(`Loading commands from: ${dir}`);
for await (const entry of readdirp_1.default(dir, {
fileFilter: ["*.js", "*.ts"],
})) {
if (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 new 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 new Error(`Command name ${command.opts.names[0]} is being used twice!`);
if (command.opts.adminOnly && this.opts.admins.size == 0)
throw new Error(`Command ${entry.path} is set to admin only, but no admins were defined.`);
// add the command to the collection
this.commands.set(command.opts.names[0], command);
}
if (!reload || this.v)
console.log(`Finished loading ${this.commands.size} commands.`);
if (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("message", 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.keyArray()) {
if (message.content.toLowerCase().includes(item)) {
const emoji = this.opts.triggers.get(item);
Reaction_1.React(message, 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);
await this.executeCommand(message, command);
});
}
/**
* Execute a command.
* (this is the function used internally for launching the commands)
* @param {Discord.Message} message - The message that contains the command
* @param {Command} command - The command to execute. (pro tip: combine with handler.getCommand)
* @returns void
*/
async executeCommand(message, command) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
// not a command
if (!command)
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();
// text is just all the args without the command name
const text = message.content.replace(this.opts.prefix + commandName, "");
//* error checking
// command is test servers only
if (command.opts.test &&
!this.opts.testServers.has(message.guild.id)) {
console.log(`${message.author.tag} tried to use test command: ${command.opts.names[0]}`);
return;
}
// command is admins only
if (command.opts.adminOnly &&
!this.opts.admins.has(message.author.id)) {
message.channel.send(((_a = this.opts.errMsg) === null || _a === void 0 ? void 0 : _a.noAdmin) || "You can't run this command!");
return;
}
// command not allowed in dms
if (message.channel.type === "dm" && command.opts.noDM) {
message.channel.send(((_b = this.opts.errMsg) === null || _b === void 0 ? void 0 : _b.noDM) ||
"You can't use this command in the dms");
return;
}
// user or guild is on blacklist
if (this.opts.blacklist.includes(message.author.id) ||
((_c = command.opts.blacklist) === null || _c === void 0 ? void 0 : _c.includes(message.author.id)) ||
this.opts.blacklist.includes((_d = message.guild) === null || _d === void 0 ? void 0 : _d.id) ||
((_e = command.opts.blacklist) === null || _e === void 0 ? void 0 : _e.includes((_f = message.guild) === null || _f === void 0 ? void 0 : _f.id))) {
message.channel.send(((_g = this.opts.errMsg) === null || _g === void 0 ? void 0 : _g.blacklist) ||
"You've been blacklisted from using this command");
return;
}
// too many args
if (command.opts.maxArgs &&
args.length > command.opts.maxArgs &&
command.opts.maxArgs > 0) {
message.channel.send(((_h = this.opts.errMsg) === null || _h === void 0 ? void 0 : _h.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) {
message.channel.send(((_j = this.opts.errMsg) === null || _j === void 0 ? void 0 : _j.tooFewArgs) ||
`Not enough args. For more info, see: ${this.opts.prefix}help ${commandName}`);
return;
}
//* command is on cooldown
if (message.channel.type != "dm" &&
(command.opts.cooldown > 0 ||
command.opts.globalCooldown > 0)) {
// const guild = this.cache.get(message.guild!.id);
const guild = (await models.guild.findById(message.guild.id));
if (guild) {
const CD = guild === null || guild === void 0 ? void 0 : guild.cooldowns.find(cd => cd.user == message.author.id &&
cd.command == command.opts.names[0]);
if (CD && CD.expires > Date.now()) {
const t = Utils_1.toTime(CD.expires - Date.now(), true);
message.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 = Utils_1.toTime(globalCD.expires - Date.now(), true);
message.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;
}
}
}
//* running the actual command
// coming soon
// const argv = arg(command.opts.argv || {}, { argv: args });
const argv = yargs_1.default(args).argv;
const res = await command.run({
client: this.client,
message,
args,
argv,
prefix: this.opts.prefix,
handler: this,
text,
logger: this.logger,
});
if (command.opts.react && res !== false)
Reaction_1.React(message, command.opts.react);
//* log the command
if (this.logger)
this.logger.log(message);
//* apply the cooldown (not if command falied)
if (res !== false &&
(command.opts.cooldown || command.opts.globalCooldown)) {
const guild = (await models.guild.findById(message.guild.id));
if (command.opts.cooldown) {
// adding the cooldown
guild === null || guild === void 0 ? void 0 : guild.cooldowns.push({
user: message.author.id,
command: command.opts.names[0],
expires: Date.now() + command.opts.cooldown,
});
// removing the cooldown after it expired
this.client.setTimeout(async () => {
const g = (await models.guild.findById(message.guild.id));
const i = g === null || g === void 0 ? void 0 : g.cooldowns.findIndex(cd => cd.user == message.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,
});
this.client.setTimeout(async () => {
const g = (await models.guild.findById(message.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;
}
/**
* 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 Utils_1.cleanDB();
}
catch (err) {
console.error("Handler failed to connect to MongoDB: ", err.message);
this.emit("dbConnectFailed", err);
}
}
/**
* A utility function to create nice embeds.
* @param title
* @param desc
* @param color
* @param thumbnail
*/
makeEmbed(title, desc, color, thumbnail) {
var _a, _b;
const emb = new discord_js_1.default.MessageEmbed({
title,
description: desc,
})
.setAuthor((_a = this.client.user) === null || _a === void 0 ? void 0 : _a.username, (_b = this.client.user) === null || _b === void 0 ? void 0 : _b.displayAvatarURL({ dynamic: true }))
.setColor(color || "BLURPLE");
if (thumbnail)
emb.setThumbnail(thumbnail);
return emb;
}
}
exports.Handler = Handler;
exports.default = Handler;
//# sourceMappingURL=Handler.js.map