alvarocastro/diamondbot

View on GitHub
core/src/bot.js

Summary

Maintainability
A
2 hrs
Test Coverage
import { Client, Collection } from 'discord.js';
import Memory from './memory';

/**
 * The main hub for interacting with the bot and the Discord API.
 */
export default class Bot {
    events = [];
    commands = new Collection();
    aliases = new Collection();

    /**
     * @param {Object} [options] Options for the bot.
     * @param {Array} [options.commands] Array of commands.
     * @param {Array} [options.events] Array of events.
     */
    constructor (options = {}) {
        options = Object.assign({
            commands: [],
            events: [],
            owner: null
        }, options);

        this.owner = options.owner;

        this.client = new Client();
        this.memory = new Memory();

        this.client.on('message', (message) => {
            this.handleMessage(message);
        });

        for (const command of options.commands) {
            this.addCommand(command);
        }
        for (const event of options.events) {
            this.addEvent(event);
        }
    }

    /**
     * Tells the bot to register and track a custom ChatEvent.
     * @method addEvent
     * @param {ChatEvent} event
     */
    addEvent (event) {
        console.log(`Registering event ${event.name}`);
        event.bot = this;
        this.events.push(event);
    }

    /**
     * Tells the bot to register a custom ChatCommand.
     * @method addCommand
     * @param {ChatCommand} command
     */
    addCommand (command) {
        command.bot = this;
        console.log(`Registering command "${command.name}"`);
        this.commands.set(command.name, command);
        for (const alias of command.aliases) {
            console.log(`Registering alias "${alias}" of command "${command.name}"`);
            this.aliases.set(alias, command.name);
        }
    }

    /**
     * Resolves a possible command name into a command object using the registered aliases and commands.
     * @method addEvent
     * @param {String} name Command name/alias.
     * @return {ChatCommand}
     */
    resolveCommand (name) {
        let cname;
        if (this.commands.has(name)) {
            cname = name;
        } else {
            console.log(`Command "${name}" not found`);
            if (this.aliases.has(name)) {
                cname = this.aliases.get(name);
                console.log(`"${name}" is alias of "${cname}"`);
            }
        }

        if (!cname) {
            return false;
        }

        return this.commands.get(cname);
    }

    /**
     * Utility getter for the bot's user.
     * @type {ClientUser}
     */
    get user () {
        return this.client.user;
    }

    /**
     * Utility method to get an emoji from all the emojis available to the bot.
     * @method emoji
     * @param {String} name Case sensitive name of the emoji
     * @return {GuildEmoji}
     * @example
     *     const pog = bot.emoji('PogChamp');
     *     console.log(`PogChamp is ${pog}`);
     */
    emoji (name) {
        return this.client.emojis.cache.find(emoji => emoji.name === name) || '';
    }

    /**
     * Utility method to repeatedly call a function with a fixed or random delay between calls.
     * @method setInterval
     * @param {Function} fn Function to call, receives the bot instance as first argument.
     * @param {(Number|Number[])} time Number of milliseconds to wait between calls. Pass an array [min, max] to wait a random amount of time.
     * @param {Boolean} [immediate=true] Trigger the function at the start of the interval.
     */
    setInterval (fn, time, immediate = true) {
        if (immediate) {
            fn(this);
        }
        const newtime = Array.isArray(time) ? Math.floor(Math.random() * (time[1] - time[0])) + time[0] : time;
        setTimeout(() => {
            this.setInterval(fn, time, true);
        }, newtime);
    }

    /**
     * Login the bot using the Discord token.
     * @method login
     * @async
     * @param {String} token Discord access token.
     */
    async login (token) {
        await new Promise(async (resolve, reject) => {
            await this.client.login(token);

            this.client.on('ready', () => {
                resolve();
            });
        });
    }

    /**
     * Parses messages and calls a command if it matches the prefix and command name. Also executes custom events.
     * @method handleMessage
     * @async
     * @param {Message} message Message received.
     */
    async handleMessage (message) {
        // Prevent bots talk
        if (message.author.bot) {
            return;
        }

        const memory = await this.memory.for(message.channel.guild.id);

        for (const event of this.events) {
            if (await event.check(message, memory)) {
                event.action(message, memory);
            }
        }

        const prefix = memory.get(['config', 'prefix']);

        // Check if message starts with prefix
        if (!message.content.startsWith(prefix)) {
            // console.log('[handleMessage] Not a command');
            return;
        }

        // Split the message by spaces, ignoring the prefix chars at the beginning
        let [commandName, ...args] = message.content.substr(prefix.length).split(/ +/);
        commandName = commandName.toLowerCase();

        // console.log(`[handleMessage] Received command "${command}" with args "${args.join(", ")}"`);

        const command = this.resolveCommand(commandName);

        // Check if the command exists
        if (!command) {
            console.log(`[handleMessage] Command "${commandName}" not found`);
            return;
        }

        try {
            console.log(`[handleMessage] Checking if command can run`);
            const allowed = await command.check(message, args, memory);

            if (!allowed) {
                console.log(`[handleMessage] Command not allowed`);
                return;
            }

            console.log(`[handleMessage] Running command "${commandName}"!`);
            await command.exec(message, args, memory);
        } catch (e) {
            console.error('[handleMessage] ERROR', e);
            message.reply('There was an error trying to execute that command!');
        }
    }
}