index.js

require('dotenv').config()
const path = require('path')
const fs = require('fs')
const discordJs = require('discord.js')
const ZigUtils = require('./lib/utils')

/**
 * ES6 Discord bot framework
 *
 * @class
 * @extends EventEmitter
 */
class Esdi extends require('events') {
  /**
   * Initializes a new Esdi server
   *
   * @param {Object} [config] configuration object
   * @param {Number} [config.loopInterval = 15] loop interval (in seconds)
   * @param {Boolean} [config.loadDefaultFiles = true] whether to load default files
   * @param {String} [config.dbAddress = 'local'] address of remote database, or `local` for local database
   * @param {String} [config.dbPort = 5984] port of remote database
   * @param {String} [config.dbNamespace = 'esdi_'] namespace prefix for database names
   * @param {String} [config.discordToken = process.env.DISCORD_TOKEN] token for Discord bot
   * @param {String} [config.botPrefix = 'esdi!'] prefix for {@link Command|Commands}
   * @param {Number} [config.hookServerPort = 8587] port for {@link Hook} server
   * @param {String} [config.hookServerHost = 'localhost'] hostname for {@link Hook} server
   * @param {Object|Boolean} [config.hookServerTls = false] `false` for HTTP, or object with paths to `key` and `cert` files for TLS
   * @param {String} [config.botOwner = '139293101767262208'] Discord ID of bot instance owner
   * @tutorial setting-up-an-esdi-instance
   * @constructor
   */
  constructor ({ loopInterval = 15, loadDefaultFiles = true, dbAddress = 'local', dbPort = 5984, dbNamespace = 'esdi_', discordToken = process.env.DISCORD_TOKEN, botPrefix = 'esdi!', hookServerPort = 8587, hookServerHost = 'localhost', hookServerTls = false, botOwner = '139293101767262208' } = {}) {
    super()

    /**
     * Starts the Esdi server
     *
     * @event Esdi#start
     * @memberof Esdi
     */
    this.on('start', () => {
      this.startTime = new Date()
      console.log(`[%] Starting Esdi @ ${this.startTime.toLocaleString()} PT`)
      this.serverLoop = setInterval(this.loop.bind(this), loopInterval * 1000)

      this.controllers.forEach(controller => {
        if (!controller.init) return
        controller.init({ server: this, loopInterval, loadDefaultFiles, dbAddress, dbPort, dbNamespace, discordToken, botPrefix, hookServerPort, hookServerHost, hookServerTls, botOwner })
      })
    })

    /**
     * Stops the Esdi server
     *
     * @event Esdi#stop
     * @memberof Esdi
     */
    this.on('stop', () => {
      this.stopTime = new Date()
      console.log(`[%] Stopping Esdi @ ${this.stopTime.toLocaleString()} PT`)
      clearTimeout(this.serverLoop)
      this.serverLoop = false
    })

    // initialize framework
    this.utils = ZigUtils
    this.controllers = new Map()
    this.commands = new discordJs.Collection()
    this.models = new Map()
    this.events = new Set()
    this.hooks = new Map()

    // if enabled, load default files
    if (!loadDefaultFiles) return
    this.load()
  }

  /**
   * Loads framework files into the Esdi server
   *
   * @param {String} [dir = __dirname] directory from which to load files
   * @param {String} [type = null] framework file type to load, or `null` for all types
   * @memberof Esdi
   */
  load (dir = __dirname, type = null) {
    // if no type provided, load all types
    if (type === null) {
      this._loadType('models', dir)
      this._loadType('controllers', dir)
      this._loadType('commands', dir)
      this._loadType('events', dir)
      this._loadType('hooks', dir)
    // otherwise, load specified type
    } else {
      this._loadType(type, dir)
    }
  }

  /**
   * Called every Esdi server loop
   *
   * @event Esdi#loop
   * @memberof Esdi
   */
  loop () {
    this.emit('loop')
  }

  /**
   * Wrapper method that starts the Esdi server
   *
   * @fires Esdi#start
   * @memberof Esdi
   */
  start () {
    this.emit('start')
  }

  /**
   * Wrapper method that stops the Esdi server
   *
   * @fires Esdi#stop
   * @memberof Esdi
   */
  stop () {
    this.emit('stop')
  }

  /**
   * Returns formatted uptime of process
   *
   * @returns {String} formatted timestamp
   * @memberof Esdi
   */
  determineUptime () {
    const formatUptime = (sec) => {
      const pad = (s) => {
        return (s < 10 ? '0' : '') + s
      }

      const days = Math.floor(sec / (60 * 60 * 24))
      const hours = Math.floor(sec / (60 * 60))
      const minutes = Math.floor(sec % (60 * 60) / 60)
      const seconds = Math.floor(sec % 60)

      return `${days ? days + 'd ' : ''}${pad(hours % ((days || 1) * 24))}:${pad(minutes)}:${pad(seconds)}s`
    }

    return formatUptime(process.uptime())
  }

  /**
   * Returns formatted memory usage of process
   *
   * @returns {String} formatted memory usage (in megabytes)
   * @memberof Esdi
   */
  determineMemory () {
    const formatMemory = (data) => `${(data / 1024 / 1024).toFixed(2)} MB`

    const memoryData = process.memoryUsage()
    return formatMemory(memoryData.rss)
  }

  /**
   * Returns formatted processor usage of process
   *
   * @returns {String} formatted processor use percentage
   * @memberof Esdi
   */
  determineProcessor () {
    const determineProcessor = () => {
      const usage = Object.assign({}, process.cpuUsage())
      usage.time = process.uptime() * 1000 // seconds to milliseconds
      usage.percent = (usage.system + usage.user) / (usage.time * 10) // microseconds to milliseconds
      return usage
    }

    return determineProcessor().percent.toFixed(2) + '%'
  }

  /**
   * Helper method that loads framework files into the Esdi server
   *
   * @param {String} type framework file type to load
   * @param {String} dir directory to load files from
   * @memberof Esdi
   * @private
   */
  _loadType (type, dir) {
    if (fs.existsSync(path.resolve(dir, type))) {
      const files = fs.readdirSync(
        path.resolve(dir, type), (e) => {
          if (e) throw e
        }
      )

      if (files.length > 0) {
        for (const file of files) {
          if (file.endsWith('.js')) {
            const filename = file.split('.')[0]

            if (type === 'commands') {
              const Command = this.models.get('Command')
              console.log(`[+] Loaded ${filename} <command> from ${path.resolve(dir, 'commands')}`)
              const sourcePath = path.resolve(dir, 'commands', file)

              this.commands.set(filename, new Command(require(sourcePath), sourcePath))
              continue
            }

            if (type === 'events') {
              const Event = this.models.get('Event')
              console.log(`[+] Loaded ${filename} <event> from ${path.resolve(dir, 'events')}`)
              const sourcePath = path.resolve(dir, 'events', file)

              this.events.add(new Event(require(sourcePath), sourcePath))
              continue
            }

            if (type === 'hooks') {
              const Hook = this.models.get('Hook')
              console.log(`[+] Loaded ${filename} <hook> from ${path.resolve(dir, 'hooks')}`)
              const sourcePath = path.resolve(dir, 'hooks', file)

              this.hooks.set(filename, new Hook(require(sourcePath), sourcePath))
              continue
            }

            this[type].set(filename, require(
              path.resolve(dir, type, file)
            ))

            if (type === 'controllers') {
              if (this.controllers.get(filename).init) {
                this.on('start', this.controllers.get(filename).start.bind(this.controllers.get(filename)))
                this.on('stop', this.controllers.get(filename).stop.bind(this.controllers.get(filename)))
              }

              if (this.controllers.get(filename).loop) {
                this.on('loop', this.controllers.get(filename).loop.bind(this.controllers.get(filename)))
              }
            }

            console.log(`[+] Loaded ${filename} <${type.substr(0, type.length - 1)}> from ${path.resolve(dir, type)}`)
          }
        }
      }
    } else {
      console.log(`[?] No ${type} found in ${dir}/${type}`)
    }
  }
}

module.exports = Esdi