Source

classes/permission-command.js

const Discord = require('discord.js');
const { Command, CommandoMessage, CommandoClientOptions, CommandInfo } = require('discord.js-commando');
const BotGuild = require('../db/mongo/BotGuild');
const BotGuildModel = require('./bot-guild');
const discordServices = require('../discord-services');
const winston = require('winston');

/**
 * The PermissionCommand is a custom command that extends the discord js commando Command class.
 * This Command subclass adds role and channel permission checks before the command is run. It also
 * removes the message used to call the command.
 * @extends Command
 */
class PermissionCommand extends Command {
    
    /**
     * Our custom command information for validation
     * @typedef {Object} CommandPermissionInfo
     * @property {string} role - the role this command can be run by
     * @property {string} channel - the channel ID where this command can be run
     * @property {string} roleMessage - the message to be sent for an incorrect role
     * @property {string} channelMessage - the message to be sent for an incorrect channel
     * @property {Boolean} dmOnly - true if this command can only be used on a DM
     */

    /**
     * Constructor for our custom command, calls the parent constructor.
     * @param {CommandoClientOptions} client - the client the command is for 
     * @param {CommandInfo} info - the information for this commando command 
     * @param {CommandPermissionInfo} permissionInfo - the custom information for this command 
     */
    constructor(client, info, permissionInfo) {
        super(client, info);

        /**
         * The permission info
         * @type {CommandPermissionInfo}
         * @private
         */
        this.permissionInfo = this.validateInfo(permissionInfo);
    }

    /**
     * Adds default values if not found on the object.
     * @param {CommandPermissionInfo} permissionInfo 
     * @returns {CommandPermissionInfo}
     * @private
     */
    validateInfo(permissionInfo) {
        // Make sure permissionInfo is an object, if not given then create empty object
        if (typeof permissionInfo != 'object') permissionInfo = {};
        if (!permissionInfo?.channelMessage) permissionInfo.channelMessage = 'Hi, the command you just used is not available on that channel!';
        if (!permissionInfo?.roleMessage) permissionInfo.roleMessage = 'Hi, the command you just used is not available to your current role!';
        permissionInfo.dmOnly = permissionInfo?.dmOnly ?? false;
        return permissionInfo;
    }


    /**
     * Run command used by Command class. Has the permission checks and runs the child runCommand method.
     * @param {Discord.Message} message 
     * @param {Object|string|string[]} args 
     * @param {boolean} fromPattern 
     * @param {Promise<?Message|?Array<Message>>} result
     * @override
     * @private
     */
    async run(message, args, fromPattern, result){

        // delete the message
        discordServices.deleteMessage(message);

        /** @type {BotGuildModel} */
        let botGuild;
        if (message?.guild) botGuild = await BotGuild.findById(message.guild.id);
        else botGuild = null;

        // check for DM only, when true, all other checks should not happen!
        if (this.permissionInfo.dmOnly) {
            if (message.channel.type != 'dm') {
                discordServices.sendEmbedToMember(message.member, {
                    title: 'Error',
                    description: 'The command you just tried to use is only usable via DM!',
                });
                winston.loggers.get(botGuild?._id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in DMs in the channel ${message.channel.name}.`);
                return;
            }
        } else {
            // Make sure it is only used in the permitted channel
            if (this.permissionInfo?.channel) {
                let channelID = botGuild.channelIDs[this.permissionInfo.channel];

                if (channelID && message.channel.id != channelID) {
                    discordServices.sendMessageToMember(message.member, this.permissionInfo.channelMessage, true);
                    winston.loggers.get(botGuild?._id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in the channel ${this.permissionInfo.channel}, in the channel ${message.channel.name}.`);
                    return;
                }
            }
            // Make sure only the permitted role can call it
            else if (this.permissionInfo?.role) {

                let roleID = botGuild.roleIDs[this.permissionInfo.role];

                // if staff role then check for staff and admin, else check the given role
                if (roleID && (roleID === botGuild.roleIDs.staffRole && 
                    (!discordServices.checkForRole(message.member, roleID) && !discordServices.checkForRole(message.member, botGuild.roleIDs.adminRole))) || 
                    (roleID != botGuild.roleIDs.staffRole && !discordServices.checkForRole(message.member, roleID))) {
                    discordServices.sendMessageToMember(message.member, this.permissionInfo.roleMessage, true);
                    winston.loggers.get(botGuild?._id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available for members with role ${this.permissionInfo.role}, but he has roles: ${message.member.roles.cache.array().map((role) => role.name)}`);
                    return;
                }
            }
        }
        this.runCommand(botGuild, message, args, fromPattern, result);
    }


    /**
     * Required class by children, will throw error if not implemented!
     * @param {BotGuildModel} botGuild
     * @param {CommandoMessage} message
     * @param {Object} args
     * @param {Boolean} fromPattern
     * @param {Promise<*>} result
     * @abstract
     * @protected
     */
    runCommand(botGuild, message, args, fromPattern, result) {
        throw new Error('You need to implement the runCommand method!');
    }
}

/**
 * String permission flags used for command permissions.
 * * ADMIN_ROLE : only admins can use this command
 * * STAFF_ROLE : staff and admin can use this command
 * * ADMIN_CONSOLE : can only be used in the admin console
 * @enum {String}
 */
PermissionCommand.FLAGS = {
    ADMIN_ROLE: 'adminRole',
    STAFF_ROLE: 'staffRole',
    ADMIN_CONSOLE: 'adminConsole',
};

module.exports = PermissionCommand;