Source: Handler.js

"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