structures/Command.js

const path = require('path')

const { Collection } = require('../util')
const { Responder, Resolver } = require('../managers')
const Base = require('./Base')

/**
 * Built-in command class
 * @extends {Base}
 * @abstract
 * @prop {Resolver} resolver Command resolver
 * @prop {Responder} responder Command responder
 * @prop {Collection} subcommands Collection of subcommands
 * @prop {Map} timers Map of timer cooldowns
 */
class Command extends Base {
  /**
   * Creates a new Command instance
   * @arg {Client} client Client instance
   * @arg {...Object} args Command options
   */
  constructor (client, ...args) {
    super(client)
    if (this.constructor === Command) {
      throw new Error('Must extend abstract Command')
    }

    const resolver = this.resolver = new Resolver(client)
    if (!client.noDefaults) {
      resolver.loadResolvers(path.join(__dirname, '..', 'resolvers'))
    }
    if (client._resolvers) {
      resolver.loadResolvers(client._resolvers)
    }

    this.responder = new Responder(this)
    this.subcommands = new Collection()
    this.timers = new Map()

    this._options = Object.assign({}, ...args)
  }

  /**
   * Verifies the options passed to the constructor
   * @arg {Object} args Options passed to the Command constructor
   * @private
   */
  set _options (args = {}) {
    const {
      name,
      group = 'none',
      aliases = [],
      cooldown = 5,
      usage = [],
      options = {},
      subcommands = {},
      subcommand
    } = args

    this.triggers = typeof name === 'string'
    ? [name].concat(aliases)
    : (Array.isArray(aliases) && aliases.length > 0 ? aliases : [])

    if (!this.triggers.length) {
      throw new Error(`${this.constructor.name} command is not named`)
    }

    this.cooldown = cooldown
    this.options = options
    if (this.options.modOnly) {
      this.options.permissions = (options.permissions || []).concat('manageGuild')
    }

    this.group = group
    this.usage = usage
    this.localeKey = options.localeKey
    this.resolver.load(usage)

    for (const command in subcommands) {
      const name = subcommands[command].name || command
      for (const alias of [name].concat(subcommands[command].aliases || [])) {
        this.subcommands.set(alias, {
          name,
          usage: subcommands[command].usage || [],
          options: subcommands[command].options || {}
        })
      }
    }
    this.subcommand = subcommand
  }

  /**
   * Checks the validaty of a command and executes it
   * @arg {Container} container Container context
   * @returns {Promise}
   */
  execute (container) {
    const responder = this.responder.create(container)

    let usage = this.usage
    let process = 'handle'

    const subcmd = this.subcommand ? this.subcommand : container.rawArgs[0]
    const cmd = this.subcommands.get(subcmd)
    if (cmd) {
      usage = cmd.usage
      process = cmd.name
      container.rawArgs = container.rawArgs.slice(this.subcommand ? 0 : 1)
      container.trigger += ' ' + subcmd
    }

    if (!this.check(container, responder, cmd)) return

    return this.resolver.resolve(container.msg, container.rawArgs, {
      prefix: (container.settings || {}).prefix || this._client.prefix,
      command: container.trigger
    }, usage).then((args = {}) => {
      container.args = args
      return this[process](container, responder)
    }, err => responder.error(`{{%errors.${err.message}`)).catch(this.logError.bind(this))
  }

  /**
   * Checks if a command is valid, run in `execute()`
   * @arg {Container} container Container context
   * @arg {Responder} responder Responder instance
   * @arg {Command} [subcmd] Subcommand
   */
  check ({ msg, isPrivate, admins, client }, responder, subcmd) {
    const isAdmin = admins.includes(msg.author.id)
    const { guildOnly, permissions = [], botPerms = [] } = subcmd ? subcmd.options : this.options
    const adminOnly = (subcmd && subcmd.options.adminOnly) || this.options.adminOnly

    if (adminOnly === true && !isAdmin) {
      return false
    }

    if (guildOnly === true && isPrivate) {
      responder.format('emoji:error').send('{{%errors.NO_PMS}}')
      return false
    }

    if (permissions.length && !(isAdmin || this.hasPermissions(msg.channel, msg.author, ...permissions))) {
      responder.error('{{%errors.NO_PERMS}}', {
        perms: permissions.map(p => `\`${p}\``).join(', ')
      })
      return false
    }

    if (botPerms.length && !this.hasPermissions(msg.channel, client.user, ...botPerms)) {
      responder.error('{{%errors.NO_PERMS_BOT}}', {
        perms: botPerms.map(p => `\`${p}\``).join(', ')
      })
      return false
    }

    if (isAdmin) return true
    const awaitID = msg.author.id
    if (this.cooldown > 0) {
      const now = Date.now() / 1000 | 0
      if (!this.timers.has(awaitID)) {
        this.timers.set(awaitID, now)
      } else {
        const diff = now - this.timers.get(awaitID)
        if (diff < this.cooldown) {
          responder.error('{{%errors.ON_COOLDOWN}}', {
            delay: 0,
            deleteDelay: 5000,
            time: `**${this.cooldown - diff}**`
          })
          return false
        } else {
          this.timers.delete(awaitID)
          this.timers.set(awaitID, now)
        }
      }
    }
    return true
  }

  /**
   * Command handler
   * @arg {Container} container Container object
   * @arg {Responder} responder Responder instance
   */
  async handle (container, responder) { return true }

  logError (err) {
    if (err && this.logger) {
      this.logger.error(`Error running ${this.triggers[0]} command`, err)
    }
  }

  get permissionNode () {
    return `${this.group}.${this.triggers[0]}`
  }
}

module.exports = Command