types/command/CommandHandler.js

import options from "../../config/options.js"
import { Collection } from '../../discord_mod.js'
import BotCommand from './BotCommand.js'
import GameCommand from './GameCommand.js'

export default class CommandHandler {
    constructor(client) {
        this.client = client
        
        // Collection<String, Command>
        this.commands = new Collection()
        this.games = new Collection()

        // cache for prefixes
        this.prefixes = new Collection()
    }

    get tagPrefix() { return new RegExp(`^<@(!?)(${this.client.user.id})>( *)`) }

    async init () {
        // construct fast command index
        this.commands = new Collection()
        this.games = new Collection()
        this.client.commands.forEach(command => this.commands.set(command.name, command))
        this.client.games.forEach(game => this.games.set((game.metadata || {id: '_Game'}).id, game.commands))
        
        // cache all guild prefixes available for this shard 
        await this.updatePrefixes()
    }

    // Force prefix cache update
    async updatePrefixes() {
        /*
        let collection = this.client.database.collection('guilds')
        if(collection) {
            let guilds = await collection.find('*')
            guilds.forEach(guild => {
                // NOTE: GUILD.ID MAY NOT BE ACCURATE
                this.prefixes.set(guild.id, guild.prefix)
            })
        }*/
    }

    async hasPermissions(message, permissions, game) {

        // Fetch member
        let member
        let userPermissions = []
        
        if(message.guild) {
            member = await message.guild.members.fetch(message.author.id)
            let channel = await this.client.channels.fetch(message.channel.id, false)
            userPermissions = (channel.permissionsFor(member) || new Collection()).toArray()
        }

        // Substitute custom permissions
        if(message.author.id === process.env.OWNER_ID) {
            userPermissions.push('GOD')
        }

        if(this.client.moderators.includes(message.author.id)) {
            userPermissions.push('MOD')
        }

        if(game && game.leader.id === message.author.id) {
            userPermissions.push('GAME_LEADER') 
        }

        return permissions.every(permission => userPermissions.includes(permission))
    }
    
    /**
     * Converts a string to 'Title Case'
     * @param {string} string String to convert
     * @returns string
     */
    titleCase(string) {
        return string.toLowerCase().replace(/^_*(.)|_+(.)/g, (s, c, d) => c ? c.toUpperCase() : ' ' + d.toUpperCase())
    }

    /**
     * Returns a prefix
     * @param {Channel} channel The channel that the message was sent in
     * @returns {string}
     */
    getPrefix(channel) {
        let prefix = options.prefix
        if(channel.guild) {
            prefix = this.prefixes.get(channel.guild.id) || options.prefix
        }
        return prefix
    }

    getMessageData(message) {
        let content = message.content
        let prefix,
            parts,
            name,
            args

        // Check if message starts with tag
        if(content.match(this.tagPrefix)) {
            prefix = this.tagPrefix
            content = content.replace(this.tagPrefix, '')
        } else if(content.startsWith(this.getPrefix(message.channel))) {
            prefix = this.getPrefix(message.channel)
            content = content.replace(prefix, '')
        } else {
            return false
        }
        // Split after removing prefix in case prefix has spaces
        parts = content.split(' ')
        name = parts[0].toLowerCase()
        args = parts.slice(1)
        return { prefix, content, name, args }
        
    }

    getCommand(messageData, game) {
        let command

        // Get default commands and aliases
        command = this.commands.get(messageData.name)
            || this.commands.find(command => command.aliases.includes(messageData.name))

        // Get in-game commands
        if(game && this.client.games.has(game.metadata)) {
            command = command || this.client.games.get(game.metadata).commands.get(messageData.name)
        }

        return command
    }

    async handle(message) {
        // Ignore bots
        if(message.author.bot && !message.client.isTestingMode) {
            return false
        }

        // Find command 
        let messageData = this.getMessageData(message)

        // Check command exists
        if(!messageData) return false

        let prefix = this.getPrefix(message.channel)

        // Check for empty tag
        if(messageData.prefix === this.client.user.tag && !messageData.name) {
            message.channel.sendMsgEmbed(`The prefix for this bot is \`${prefix}\`. You can also use ${this.client.user.tag} as a prefix.`)
            return false
        }

        // Validate command type
        let game = this.client.gameManager.games.get(message.channel.id)

        // Get command
        let command = this.getCommand(messageData, game)
        
        if(!command) return false

        if (command instanceof BotCommand) {} 
        else if (command instanceof GameCommand) {
            if(!game) {
                message.channel.sendMsgEmbed(`Please start a game before using this command.`, 'Error!', options.colors.error)
                return false
            }
            if(!game.players.has(message.author.id)) {
                message.channel.sendMsgEmbed(`Only players may use in-game commands.`, 'Error!', options.colors.error)
                return false
            }
        } else {
            throw new TypeError('Improperly constructed command, all commands must be of type BotCommand or GameCommand')
        }


        // Check for dmChannel
        if (message.channel.type === 'dm' && !command.dmCommand) {
            message.channel.send('This command is not available in a DM channel. Please try this again in a server.')
            return false
        } 
        
        if(!(await this.hasPermissions(message, command.permissions, game))) {
            let permissions = command.permissions.map(permission => this.titleCase(permission)).join(', ')
            message.channel.sendMsgEmbed(`Sorry, you don't have the necessary permissions for this command.\n\nRequired permissions: \`${permissions}\``, 'Error!', options.colors.error)
            return false
        }

        if (command.args && messageData.args.join() === '') {
            message.channel.sendMsgEmbed(`Incorrect usage of this command. Usage: \`${prefix}${command.usage}\`.`)
            return
        }

        try {
            if (['dev', 'economy'].includes(command.category)) {
                await this.client.dbClient.createDBInfo(message.author.id)
            }
            command.run(message, messageData.args, game)
        } catch (err) {
            this.client.emit('error', err, this.client, message)
        }
    
    }
}