const { join } = require('path')
const { promises: { readdir, stat } } = require('fs')
const {
Client
} = require('eris')
const Orator = require('../orator')
const RAMManager = require('../ram-manager')
const StatusManager = require('../status-manager')
const { logger, ExtendedMap } = require('../../util')
const defaults = require('../../config/settings.json')
class DataClient extends Client {
/**
* Class representing a DataClient.
* @extends {Client}
* @param {String} token The Discord bot token.
* @param {DataClientOptions} [options] The DataClient options.
*/
constructor (token, {
databaseManager = new RAMManager(),
oratorOptions,
statusManagerOptions,
erisOptions,
disabledEvents = []
} = {}) {
super(token, erisOptions)
/**
* @type {DatabaseManager}
*/
this.dbm = databaseManager
/**
* @type {Orator}
*/
this.ora = new Orator(
defaults.oratorOptions.defaultPrefix,
{ ...defaults.oratorOptions, ...oratorOptions }
)
/**
* @type {StatusManager}
*/
this.sm = new StatusManager(
this,
databaseManager,
{ ...defaults.statusManagerOptions, ...statusManagerOptions }
)
/**
* @type {ExtendedMap<string, Command<this>>}
*/
this.commands = new ExtendedMap()
/**
* @type {ExtendedMap<string, Permission>}
*/
this.permissions = new ExtendedMap()
const fromRoot = (path) => join(__dirname, '../..', path)
this._commandsToLoad = [ fromRoot('commands') ]
this._settingCommandsToLoad = []
this._eventsToLoad = [ fromRoot('events') ]
this._permissionsToLoad = [ fromRoot('permissions') ]
// load built in events and commands
this
.on('ready', this._onReady.bind(this))
.on('guildCreate', this._onGuildCreate.bind(this))
.on('messageCreate', this._onMessageCreate.bind(this))
}
/**
* Connect to discord.
* @returns {Promise<void>}
*/
async connect () {
if (!this.token) {
throw Error('Token is empty!')
}
await Promise.all([
this._loadLoadables('commands', this._commandsToLoad),
this._loadLoadables('events', this._eventsToLoad),
this._loadLoadables('permissions', this._permissionsToLoad)
])
await this._loadLoadables('settings', this._settingCommandsToLoad)
this.ora.permissions = this.permissions
logger.info('Connecting to Discord...')
return super.connect()
}
/**
* Find a command from commands.
* @param {string} name Name of command to search.
* @param {ExtendedMap<string, Command<this>>} commands A collection of commands to search instead of the build in commands.
* @returns {Command<this>|void}
*/
findCommand (name, commands = this.commands) {
if (name) {
return commands.find(
(command) => command.name.toLowerCase() === name.toLowerCase() ||
command.aliases.includes(name.toLowerCase())
)
}
}
/**
* Ready event handler.
* @private
* @returns {void}
*/
_onReady () {
logger.success('Connected')
this._addNewGuilds()
this._setOwner()
this.sm.initialize()
}
/**
* guildCreate event handler.
* @private
* @param {Guild} guild The guild to add.
* @returns {Promise<DatabaseObject|void>} The new guild database object.
*/
_onGuildCreate (guild) {
return this._addGuild(guild)
}
/**
* messageCreate event handler.
* @private
* @param {Message} msg The message that triggered the event {@link https://abal.moe/Eris/docs/Message|(link)}.
* @returns {Promise<void>}
*/
_onMessageCreate (msg) {
return this.ora.processMessage(this, msg)
}
/**
* Check guilds and add new ones.
* @private
* @returns {Array<Promise<DatabaseObject|void>>} The list of newly added guild database objects.
*/
async _addNewGuilds () {
const dbGuilds = await this.dbm.newQuery('guild').find()
const toAdd = this.guilds.filter(
(guild) => !dbGuilds.find((dbGuild) => dbGuild.id === guild.id)
)
return Promise.all(toAdd.map((guild) => this._addGuild(guild)))
}
/**
* Add a guild to the database.
* @private
* @param {Guild} guild The guild to add.
* @returns {Promise<DatabaseObject|void>} The new guild database object.
*/
_addGuild (guild) {
return this.dbm.newObject('guild')
.save({ id: guild.id })
.catch((error) => {
logger.error('Could not add guild:', error)
})
}
/**
* Set bot owner.
* @private
* @returns {Promise<void>}
*/
async _setOwner () {
this.ownerID = (await this.getOAuthApplication()).owner.id
}
/**
* Load data files.
* @private
* @param {string} path The path to the loadable file/directory.
* @returns {Array<LoadableObject>} The loadable objects loaded from file.
*/
async _loadFiles (path) {
const file = await stat(path)
const files = file.isDirectory() ? await readdir(path) : [ '' ]
const res = []
for (const fd of files) {
const filePath = join(path, fd)
if (
(filePath.match(/(?<!\.(?:test|spec|d))\.[jt]sx?$/)) ||
(await stat(filePath)).isDirectory()
) {
try {
let data = require(filePath)
if (data.__esModule) {
data = data.default
}
if (!data.isIndex) {
res.push(data)
}
} catch (e) {
logger.error(
`Unable to read ${path}/${fd}:\n\t\t\u0020${e}`
)
}
}
}
return res
}
/**
* Resolve loadable, be it path or array.
* @private
* @param {Loadable} loadable Parse a loadable to clean up any arrays or paths.
* @returns {Array<LoadableObject>} The cleaned loadable(s).
*/
async _resolveLoadables (loadable) {
if (!Array.isArray(loadable)) {
loadable = [ loadable ]
}
const ax = []
for (const dx of loadable) {
if (typeof dx === 'string') {
ax.push(...(await this._loadFiles(dx)))
} else {
ax.push(dx)
}
}
return ax
}
/**
* Add commands to store.
* @param {...string|Command<this>|Array<string|Command<this>>} commands Commands to add to store.
* @returns {DataClient} Current state of DataClient.
*/
addCommands (...commands) {
return this._addLoadables(commands, this._commandsToLoad)
}
/**
* Add settings commands to store.
* @param {...string|Command<this>|Array<string|Command<this>>} commands Commands to add to store.
* @returns {DataClient} Current state of DataClient.
*/
addSettingCommands (...commands) {
return this._addLoadables(commands, this._settingCommandsToLoad)
}
/**
* Add events to store.
* @param {...string|DiscordEvent|Array<string|DiscordEvent>} events Events to add to store.
* @returns {DataClient} Current state of DataClient.
*/
addEvents (...events) {
return this._addLoadables(events, this._eventsToLoad)
}
/**
* Add permissions to store.
* @param {...string|Permission|Array<string|Permission>} permissions Permissions to add to store.
* @returns {DataClient} Current state of DataClient.
*/
addPermissions (...permissions) {
return this._addLoadables(permissions, this._permissionsToLoad)
}
/**
* Add loadables to store.
* @private
* @param {Array<Loadable>} loadables Array of things to load.
* @param {Array<Loadable>} store Store to save loadables too.
* @returns {DataClient} Current state of DataClient.
*/
_addLoadables (loadables, store) {
let cx = loadables.length
while (cx--) {
if (Array.isArray(loadables[cx])) {
let i = loadables[cx].length
while (i--) {
store.push(loadables[cx][i])
}
} else {
store.push(loadables[cx])
}
}
return this
}
/**
* Loads some loadable files, calls correct loader function based on type.
* @private
* @param {string} type Type of loadable.
* @param {Array<Loadable>} store Array of things to load.
* @returns {Promise<void>}
*/
async _loadLoadables (type, store) {
if (!store.length) {
return
}
logger.info(`Loading ${type}...`)
let loader
switch (type) {
case 'commands':
loader = this._loadCommand
break
case 'settings':
loader = this._loadSettingCommand
break
case 'events':
loader = this._loadEvent
break
case 'permissions':
loader = this._loadPermission
break
default: throw Error(`Unknown type: ${type}`)
}
for (const loadables of store) {
for (const loadable of await this._resolveLoadables(loadables)) {
loader.call(this, loadable)
}
}
store = []
}
/**
* Command loader.
* @private
* @param {Command<this>} command The command to load.
* @returns {void}
*/
_loadCommand (command) {
this.commands.set(command.name, command)
}
/**
* SettingCommand loader.
* @private
* @param {SettingCommand<this>} command The command to load.
* @returns {void}
*/
_loadSettingCommand (command) {
this.commands.get('settings').subCommands.set(command.name, command)
}
/**
* Permission loader.
* @private
* @param {Permission} permission The permission to load.
* @returns {void}
*/
_loadPermission (permission) {
this.permissions.set(permission.level, permission)
}
/**
* Event loader.
* @private
* @param {DiscordEvent} event The event to load.
* @returns {void}
*/
_loadEvent (event) {
this.on(event.name, event.run.bind(null, this))
}
}
module.exports = DataClient
/**
* @typedef DataClientOptions
* @property {DatabaseManager} [databaseManager] The DatabaseManager.
* @property {OratorOptions} [oratorOptions] Params to pass to the Orator class.
* @property {StatusManagerOptions} [statusManagerOptions] StatusManagerOptions object.
* @property {Object} [options.erisOptions] Options to pass to Eris Client.
*/
/**
* @typedef {string|LoadableObject|Array<string|LoadableObject>} Loadable
*/
/**
* @typedef {Command<any>|DiscordEvent<any>|Permission} LoadableObject
*/