Source: modules.js

const reload = require('require-reload')(require)
const path = require('path')
const watch = require('node-watch')
const Discord = require('discord.js')

/**
 * Module Base class
 * @global
 */
class BotModule {
  /**
   * Instantiates a Module
   */
  constructor (name) {
    /** Module name */
    this.name = name
    /** Discord.js client */
    this.bot = global.Core.bot
    /** Registered Commands */
    this.commands = []
    /** Registered event handlers */
    this.events = []
  }

  /**
   * Registers a command.
   * @param {string} name - Command name.
   * @param {object} options - Command options.
   * @param {function} func - Command handler.
   * @returns {BotCommand}
   * @see {BotCommand}
   */
  registerCommand (name, options, func) {
    try {
      const command = Core.commands.register(name, Object.assign(options, {
        module: this
      }), func)
      this.commands.push(command)
      return command
    } catch (e) {
      Core.log(e, 2)
      return false
    }
  }

  /**
   * Unregisters a command (only if it belongs to this module).
   * @param {string|BotCommand} command - Command to unregister
   */
  unregisterCommand (command) {
    try {
      const cmd = command instanceof BotCommand ? command : Core.commands.registered[command]
      if (!cmd.module || cmd.module !== this) return
      Core.commands.unregister(cmd)
    } catch (e) {
      Core.log(e, 2)
    }
  }

  /**
   * Registers a setting parameter.
   * @param {string} key - Parameter key
   * @param {object} props - Parameter properties
   */
  registerParameter (key, props) {
    return Core.settings.register(key, Object.assign(props, {
      module: this
    }))
  }

  /**
   * Unregisters a setting parameter.
   * @param {string} key - Parameter key
   */
  unregisterParameter (key) {
    if (Core.settings.schema[key] && Core.settings.schema[key].module === this) {
      return Core.settings.unregister(key)
    }
  }

  /**
   * Registers an event listener bound to this module
   * @param {string} name - Event Name - Prefix with discord. for discord events
   * @param {function} evHandler - Event Handler
   * @example
   * this.registerEvent('discord.message', messageHandler)
   * this.registerEvent('customEvent', handler)
   */
  registerEvent (name, evHandler) {
    // Try to avoid event handler if the module is disabled
    const self = this
    const handler = async function (param) {
      let guild = param instanceof Discord.Guild ? param : param.guild
      if (param && param.guildId) guild = { id: param.guildId }
      if (guild && guild.id && await Core.modules.isDisabledForGuild(guild, self)) {
        return false
      }
      evHandler.apply(self, arguments)
    }
    if (name.match(/^discord\.(.*)/)) {
      Core.bot.on(name.match(/^discord\.(.*)/)[1], handler)
    } else if (name.match(/^db\.(.*)/)) {
      const channelName = name.match(/^db\.(.*)/)[1]
      this.events.push({ name, handler: Core.data.subscribe(channelName, handler), evHandler })
    } else {
      Core.events.on(name, handler)
    }
    this.events.push({ name, handler, evHandler })
  }

  /**
   * Unregisters an event listener
   * @param {string} name - Event Name
   * @param {string} evHandler - Optional event handler
   */
  unregisterEvent (name, evHandler) {
    this.events.filter(v => {
      return v.name === name && (v.evHandler === evHandler || evHandler == null)
    }).forEach(ev => {
      if (ev.name.match(/^discord\.(.*)/)) {
        Core.bot.removeListener(ev.name.match(/^discord\.(.*)/)[1], ev.handler)
      } else if (ev.name.match(/^db\.(.*)/)) {
        ev.handler.off()
      } else {
        Core.events.removeListener(ev.name, ev.handler)
      }
    })
  }

  shutdown () {
    if (typeof this.unload === 'function') this.unload()
    for (const param in Core.settings.schema) {
      if (Core.settings.schema[param].module === this) this.unregisterParameter(param)
    }
    this.events.forEach(ev => {
      if (ev.name.match(/^discord\.(.*)/)) {
        Core.bot.removeListener(ev.name.match(/^discord\.(.*)/)[1], ev.handler)
      } else {
        Core.events.removeListener(ev.name, ev.handler)
      }
    })
    Core.commands.unregister(this.commands)
  }
}

/**
 * Manages the modules.
 */
class ModuleManager {
  constructor () {
    /** Currently loaded modules */
    this.loaded = {}
    this.modulePath = Core.properties.modulePath || './modules'
    // Hot Module Reloading
    if (Core.properties.watch) {
      this.pendingReloads = {}
      watch(this.modulePath, { recursive: true }, (event, file) => {
        // Get module name
        const modName = file.split(this.modulePath)[1].match(/^\/?([^/\\.]*)(\.[^/\\])?\/?/)[1]
        // Ignore if the module isn't loaded
        if (!this.loaded[modName]) return
        try {
          // Avoid reloading twice
          if (this.pendingReloads[modName]) clearTimeout(this.pendingReloads[modName])
          // Reload the module
          this.pendingReloads[modName] = setTimeout(() => {
            this.reload(modName)
            Core.log(`Reloaded Module "${modName}"!`)
            delete this.pendingReloads[modName]
          }, 100)
        } catch (e) {
          console.error(e)
        }
      })
    }
  }

  /**
   * Loads the specified modules.
   * @param {string[]} modules - Modules to load
   */
  load (modules) {
    const m = (typeof modules === 'string') ? [modules] : modules
    if (!m.forEach) throw new Error('This function only accepts strings or arrays.')
    m.forEach((mod) => {
      try {
        const Module = reload(path.join(this.modulePath, mod))
        this.loaded[mod] = new Module(mod)
        Core.log(`Loaded Module "${mod}"!`, 1)
        if (typeof this.loaded[mod].init === 'function') this.loaded[mod].init()
        if (typeof this.loaded[mod].ready === 'function') {
          if (Core.ready) {
            this.loaded[mod].ready()
          } else {
            Core.bot.once('ready', () => this.loaded[mod].ready())
          }
        }
      } catch (e) {
        Core.log(e, 2)
      }
    })
  }

  /**
   * Unloads the specified modules.
   * @param {string[]} modules - Modules to unload
   */
  unload (modules) {
    const m = (typeof modules === 'string') ? [modules] : modules
    if (!m.forEach) throw new Error('This function only accepts strings or arrays.')
    m.forEach((mod) => {
      try {
        this.loaded[mod].shutdown()
        delete this.loaded[mod]
        Core.log(`Unloaded Module "${mod}".`, 1)
      } catch (e) {
        Core.log(e, 2)
      }
    })
  }

  /**
   * Reloads the specified modules.
   * @param {string[]} modules - Modules to load
   */
  reload (modules) {
    this.unload(modules)
    this.load(modules)
  }

  /**
   * Enables a module only in a specific guild
   * @param {Discord.Guild} guild
   * @param {string|BotModule} mod - Module name or instance
   */
  async enableForGuild (guild, mod) {
    const g = await Core.guilds.getGuild(guild)
    const m = mod instanceof BotModule ? mod : this.loaded[mod]
    if (!m) throw new Error('No such module.')
    if (!g.data.disabledModules) g.data.disabledModules = { }
    g.data.disabledModules[m.name] = false
    await g.saveData()
  }

  /**
   * Disables a module only in a specific guild
   * @param {Discord.Guild} guild
   * @param {string|BotModule} mod - Module name or instance
   */
  async disableForGuild (guild, mod) {
    const g = await Core.guilds.getGuild(guild)
    const m = mod instanceof BotModule ? mod : this.loaded[mod]
    if (!m) throw new Error('No such module.')
    if (!g.data.disabledModules) g.data.disabledModules = { }
    g.data.disabledModules[m.name] = true
    await g.saveData()
  }

  /**
   * Checks if a module is disabled in a guild.
   * @param {Discord.Guild} guild
   * @param {string|BotModule} mod - Module name or instance
   */
  async isDisabledForGuild (guild, mod) {
    const d = (await Core.guilds.getGuild(guild)).data
    const m = mod instanceof BotModule ? mod : this.registered[mod]
    if (!m) return true
    if (d.disabledModules && d.disabledModules[m.name] != null) {
      if (d.disabledModules[mod.name] === true) return true
      if (d.disabledModules[mod.name] === false) return false
      return m.defaultDisabled
    }
    return m.defaultDisabled
  }
}

module.exports = ModuleManager
global.BotModule = BotModule