aboutsummaryrefslogtreecommitdiffstats
path: root/server/src/serverLib
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/serverLib')
-rw-r--r--server/src/serverLib/CommandManager.js201
-rw-r--r--server/src/serverLib/ConfigManager.js74
-rw-r--r--server/src/serverLib/CoreApp.js84
-rw-r--r--server/src/serverLib/ImportsManager.js87
-rw-r--r--server/src/serverLib/MainServer.js376
-rw-r--r--server/src/serverLib/RateLimiter.js105
-rw-r--r--server/src/serverLib/StatsManager.js63
-rw-r--r--server/src/serverLib/index.js14
8 files changed, 593 insertions, 411 deletions
diff --git a/server/src/serverLib/CommandManager.js b/server/src/serverLib/CommandManager.js
index 0c4f0aa..e715c6b 100644
--- a/server/src/serverLib/CommandManager.js
+++ b/server/src/serverLib/CommandManager.js
@@ -1,47 +1,71 @@
-/**
- * Commands / protocol manager- loads, validates and handles command execution
- *
- * Version: v2.0.0
- * Developer: Marzavec ( https://github.com/marzavec )
- * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
- *
- */
-
-const path = require('path');
-const didYouMean = require('didyoumean2').default;
+import {
+ basename,
+ join,
+ sep,
+ dirname,
+ relative,
+} from 'path';
+import didYouMean from 'didyoumean2';
// default command modules path
const CmdDir = 'src/commands';
+/**
+ * Commands / protocol manager- loads, validates and handles command execution
+ * @property {Array} commands - Array of currently loaded command modules
+ * @property {Array} categories - Array of command modules categories
+ * @author Marzavec ( https://github.com/marzavec )
+ * @version v2.0.0
+ * @license WTFPL ( http://www.wtfpl.net/txt/copying/ )
+ */
class CommandManager {
/**
* Create a `CommandManager` instance for handling commands/protocol
*
* @param {Object} core Reference to the global core object
*/
- constructor (core) {
+ constructor(core) {
+ /**
+ * Stored reference to the core
+ * @type {CoreApp}
+ */
this.core = core;
+
+ /**
+ * Command module storage
+ * @type {Array}
+ */
this.commands = [];
+
+ /**
+ * Command module category names (based off directory or module meta)
+ * @type {Array}
+ */
this.categories = [];
- if (!this.core.config.hasOwnProperty('logErrDetailed')) {
+
+ /**
+ * Full path to config.json file
+ * @type {String}
+ */
+ if (typeof this.core.config.logErrDetailed === 'undefined') {
this.core.config.logErrDetailed = false;
}
}
/**
* (Re)initializes name spaces for commands and starts load routine
- *
+ * @public
* @return {String} Module errors or empty if none
*/
- loadCommands () {
+ loadCommands() {
this.commands = [];
this.categories = [];
const commandImports = this.core.dynamicImports.getImport(CmdDir);
let cmdErrors = '';
- Object.keys(commandImports).forEach(file => {
- let command = commandImports[file];
- let name = path.basename(file);
+ Object.keys(commandImports).forEach((file) => {
+ const command = commandImports[file];
+ const name = basename(file);
cmdErrors += this.validateAndLoad(command, file, name);
});
@@ -50,29 +74,28 @@ class CommandManager {
/**
* Checks the module after having been `require()`ed in and reports errors
- *
* @param {Object} command reference to the newly loaded object
* @param {String} file file path to the module
* @param {String} name command (`cmd`) name
- *
+ * @private
* @return {String} Module errors or empty if none
*/
- validateAndLoad (command, file, name) {
- let error = this.validateCommand(command);
+ validateAndLoad(command, file, name) {
+ const error = this.validateCommand(command);
if (error) {
- let errText = `Failed to load '${name}': ${error}`;
+ const errText = `Failed to load command module '${name}': ${error}`;
console.log(errText);
return errText;
}
if (!command.category) {
- let base = path.join(this.core.dynamicImports.base, 'commands');
+ const base = join(this.core.dynamicImports.base, 'commands');
let category = 'Uncategorized';
- if (file.indexOf(path.sep) > -1) {
- category = path.dirname(path.relative(base, file))
- .replace(new RegExp(path.sep.replace('\\', '\\\\'), 'g'), '/');
+ if (file.indexOf(sep) > -1) {
+ category = dirname(relative(base, file))
+ .replace(new RegExp(sep.replace('\\', '\\\\'), 'g'), '/');
}
command.info.category = category;
@@ -86,7 +109,7 @@ class CommandManager {
try {
command.init(this.core);
} catch (err) {
- let errText = `Failed to initialize '${name}': ${err}`;
+ const errText = `Failed to initialize '${name}': ${err}`;
console.log(errText);
return errText;
}
@@ -99,121 +122,109 @@ class CommandManager {
/**
* Checks the module after having been `require()`ed in and reports errors
- *
* @param {Object} object reference to the newly loaded object
- *
+ * @private
* @return {String} Module errors or null if none
*/
- validateCommand (object) {
- if (typeof object !== 'object')
- return 'command setup is invalid';
-
- if (typeof object.run !== 'function')
- return 'run function is missing';
-
- if (typeof object.info !== 'object')
- return 'info object is missing';
-
- if (typeof object.info.name !== 'string')
- return 'info object is missing a valid name field';
+ validateCommand(object) {
+ if (typeof object !== 'object') { return 'command setup is invalid'; }
+ if (typeof object.run !== 'function') { return 'run function is missing'; }
+ if (typeof object.info !== 'object') { return 'info object is missing'; }
+ if (typeof object.info.name !== 'string') { return 'info object is missing a valid name field'; }
return null;
}
/**
* Pulls all command names from a passed `category`
- *
* @param {String} category [Optional] filter return results by this category
- *
+ * @public
* @return {Array} Array of command modules matching the category
*/
- all (category) {
+ all(category) {
return !category ? this.commands : this.commands.filter(
- c => c.info.category.toLowerCase() === category.toLowerCase()
- );
+ (c) => c.info.category.toLowerCase() === category.toLowerCase(),
+ );
}
/**
- * Pulls all category names
- *
- * @return {Array} Array of sub directories under CmdDir
+ * All category names
+ * @public
+ * @readonly
+ * @return {Array} Array of command category names
*/
- get categoriesList () {
+ get categoriesList() {
return this.categories;
}
/**
- * Pulls command by name or alia(s)
- *
+ * Pulls command by name or alias
* @param {String} name name or alias of command
- *
+ * @public
* @return {Object} Target command module object
*/
- get (name) {
+ get(name) {
return this.findBy('name', name)
|| this.commands.find(
- command => command.info.aliases instanceof Array &&
- command.info.aliases.indexOf(name) > -1
+ (command) => command.info.aliases instanceof Array
+ && command.info.aliases.indexOf(name) > -1,
);
}
/**
* Pulls command by arbitrary search of the `module.info` attribute
- *
* @param {String} key name or alias of command
* @param {String} value name or alias of command
- *
+ * @public
* @return {Object} Target command module object
*/
- findBy (key, value) {
- return this.commands.find(c => c.info[key] === value);
+ findBy(key, value) {
+ return this.commands.find((c) => c.info[key] === value);
}
/**
* Runs `initHooks` function on any modules that utilize the event
- *
+ * @private
* @param {Object} server main server object
*/
- initCommandHooks (server) {
- this.commands.filter(c => typeof c.initHooks !== 'undefined').forEach(
- c => c.initHooks(server)
- );
+ initCommandHooks(server) {
+ this.commands.filter((c) => typeof c.initHooks !== 'undefined').forEach(
+ (c) => c.initHooks(server),
+ );
}
/**
* Finds and executes the requested command, or fails with semi-intelligent error
- *
* @param {Object} server main server reference
* @param {Object} socket calling socket reference
* @param {Object} data command structure passed by socket (client)
- *
+ * @public
* @return {*} Arbitrary module return data
*/
- handleCommand (server, socket, data) {
+ handleCommand(server, socket, data) {
// Try to find command first
- let command = this.get(data.cmd);
+ const command = this.get(data.cmd);
if (command) {
return this.execute(command, server, socket, data);
- } else {
- // Then fail with helpful (sorta) message
- return this.handleFail(server, socket, data);
}
+
+ // Then fail with helpful (sorta) message
+ return this.handleFail(server, socket, data);
}
/**
* Requested command failure handler, attempts to find command and reports back
- *
* @param {Object} server main server reference
* @param {Object} socket calling socket reference
* @param {Object} data command structure passed by socket (client)
- *
+ * @private
* @return {*} Arbitrary module return data
*/
- handleFail (server, socket, data) {
- const maybe = didYouMean(data.cmd, this.all().map(c => c.info.name), {
+ handleFail(server, socket, data) {
+ const maybe = didYouMean(data.cmd, this.all().map((c) => c.info.name), {
threshold: 5,
- thresholdType: 'edit-distance'
+ thresholdType: 'edit-distance',
});
if (maybe) {
@@ -221,7 +232,7 @@ class CommandManager {
return this.handleCommand(server, socket, {
cmd: 'socketreply',
cmdKey: server.cmdKey,
- text: `Command not found, did you mean: \`${maybe}\`?`
+ text: `Command not found, did you mean: \`${maybe}\`?`,
});
}
@@ -229,39 +240,37 @@ class CommandManager {
return this.handleCommand(server, socket, {
cmd: 'socketreply',
cmdKey: server.cmdKey,
- text: 'Unknown command'
+ text: 'Unknown command',
});
}
/**
* Attempt to execute the requested command, fail if err or bad params
- *
* @param {Object} command target command module
* @param {Object} server main server reference
* @param {Object} socket calling socket reference
* @param {Object} data command structure passed by socket (client)
- *
+ * @private
* @return {*} Arbitrary module return data
*/
- async execute (command, server, socket, data) {
+ async execute(command, server, socket, data) {
if (typeof command.requiredData !== 'undefined') {
- let missing = [];
- for (let i = 0, len = command.requiredData.length; i < len; i++) {
- if (typeof data[command.requiredData[i]] === 'undefined')
- missing.push(command.requiredData[i]);
+ const missing = [];
+ for (let i = 0, len = command.requiredData.length; i < len; i += 1) {
+ if (typeof data[command.requiredData[i]] === 'undefined') { missing.push(command.requiredData[i]); }
}
if (missing.length > 0) {
console.log(`Failed to execute '${
- command.info.name
- }': missing required ${missing.join(', ')}\n\n`);
+ command.info.name
+ }': missing required ${missing.join(', ')}\n\n`);
this.handleCommand(server, socket, {
cmd: 'socketreply',
cmdKey: server.cmdKey,
text: `Failed to execute '${
- command.info.name
- }': missing required ${missing.join(', ')}\n\n`
+ command.info.name
+ }': missing required ${missing.join(', ')}\n\n`,
});
return null;
@@ -271,20 +280,20 @@ class CommandManager {
try {
return await command.run(this.core, server, socket, data);
} catch (err) {
- let errText = `Failed to execute '${command.info.name}': `;
-
+ const errText = `Failed to execute '${command.info.name}': `;
+
// If we have more detail enabled, then we get the trace
// if it isn't, or the property doesn't exist, then we'll get only the message
if (this.core.config.logErrDetailed === true) {
console.log(errText + err.stack);
} else {
- console.log(errText + err.toString())
+ console.log(errText + err.toString());
}
this.handleCommand(server, socket, {
cmd: 'socketreply',
cmdKey: server.cmdKey,
- text: errText + err.toString()
+ text: errText + err.toString(),
});
return null;
@@ -292,4 +301,4 @@ class CommandManager {
}
}
-module.exports = CommandManager;
+export default CommandManager;
diff --git a/server/src/serverLib/ConfigManager.js b/server/src/serverLib/ConfigManager.js
index e29a0e7..bb414be 100644
--- a/server/src/serverLib/ConfigManager.js
+++ b/server/src/serverLib/ConfigManager.js
@@ -1,38 +1,47 @@
+import dateFormat from 'dateformat';
+import {
+ existsSync,
+ ensureFileSync,
+ readJsonSync,
+ copySync,
+ writeJSONSync,
+ removeSync,
+} from 'fs-extra';
+import { resolve } from 'path';
+
/**
* Server configuration manager, handling loading, creation, parsing and saving
* of the main config.json file
- *
- * Version: v2.0.0
- * Developer: Marzavec ( https://github.com/marzavec )
- * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
- *
+ * @property {String} base - Base path that all imports are required in from
+ * @author Marzavec ( https://github.com/marzavec )
+ * @version v2.0.0
+ * @license WTFPL ( http://www.wtfpl.net/txt/copying/ )
*/
-const dateFormat = require('dateformat');
-const fse = require('fs-extra');
-const path = require('path');
-
class ConfigManager {
/**
* Create a `ConfigManager` instance for managing application settings
- *
* @param {String} basePath executing directory name; __dirname
*/
- constructor (basePath = __dirname) {
- this.configPath = path.resolve(basePath, 'config/config.json');
+ constructor(basePath = __dirname) {
+ /**
+ * Full path to config.json file
+ * @type {String}
+ */
+ this.configPath = resolve(basePath, 'config/config.json');
- if (!fse.existsSync(this.configPath)){
- fse.ensureFileSync(this.configPath);
+ if (!existsSync(this.configPath)) {
+ ensureFileSync(this.configPath);
}
}
/**
- * Loads config.json (main server config) into mem
- *
- * @return {Object || Boolean} False if the config.json could not be loaded
+ * Loads config.json (main server config) into memory
+ * @public
+ * @return {(JSON|Boolean)} False if the config.json could not be loaded
*/
- async load () {
+ async load() {
try {
- this.config = fse.readJsonSync(this.configPath);
+ this.config = readJsonSync(this.configPath);
} catch (e) {
return false;
}
@@ -42,12 +51,12 @@ class ConfigManager {
/**
* Creates backup of current config into configPath
- *
+ * @private
* @return {String} Backed up config.json path
*/
- async backup () {
+ backup() {
const backupPath = `${this.configPath}.${dateFormat('dd-mm-yy-HH-MM-ss')}.bak`;
- fse.copySync(this.configPath, backupPath);
+ copySync(this.configPath, backupPath);
return backupPath;
}
@@ -55,18 +64,18 @@ class ConfigManager {
/**
* First makes a backup of the current `config.json`, then writes current config
* to disk
- *
+ * @public
* @return {Boolean} False on failure
*/
- async save () {
- const backupPath = await this.backup();
+ save() {
+ const backupPath = this.backup();
try {
- fse.writeJSONSync(this.configPath, this.config, {
- // Indent with two spaces
+ writeJSONSync(this.configPath, this.config, {
+ // Indent with two spaces
spaces: 2,
});
- fse.removeSync(backupPath);
+ removeSync(backupPath);
return true;
} catch (err) {
@@ -78,18 +87,17 @@ class ConfigManager {
/**
* Updates current config[`key`] with `value` then writes changes to disk
- *
* @param {*} key arbitrary configuration key
* @param {*} value new value to change `key` to
- *
+ * @public
* @return {Boolean} False on failure
*/
- async set (key, value) {
+ set(key, value) {
const realKey = `${key}`;
this.config[realKey] = value;
- return await this.save();
+ return this.save();
}
}
-module.exports = ConfigManager;
+export default ConfigManager;
diff --git a/server/src/serverLib/CoreApp.js b/server/src/serverLib/CoreApp.js
index 6bab090..8c2225d 100644
--- a/server/src/serverLib/CoreApp.js
+++ b/server/src/serverLib/CoreApp.js
@@ -1,30 +1,32 @@
-/**
- * The core / global reference object
- *
- * Version: v2.0.0
- * Developer: Marzavec ( https://github.com/marzavec )
- * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
- *
- */
-
-const path = require('path');
-const {
+import { join } from 'path';
+import {
CommandManager,
ConfigManager,
ImportsManager,
MainServer,
- StatsManager
-} = require('./');
+ StatsManager,
+} from '.';
+/**
+ * The core app builds all required classes and maintains a central
+ * reference point across the app
+ * @property {ConfigManager} configManager - Provides loading and saving of the server config
+ * @property {Object} config - The current json config object
+ * @property {ImportsManager} dynamicImports - Dynamic require interface allowing hot reloading
+ * @property {CommandManager} commands - Manages and executes command modules
+ * @property {StatsManager} stats - Stores and adjusts arbritary stat data
+ * @property {MainServer} server - Main websocket server reference
+ * @author Marzavec ( https://github.com/marzavec )
+ * @version v2.0.0
+ * @license WTFPL ( http://www.wtfpl.net/txt/copying/ )
+ */
class CoreApp {
/**
- * Create the main core instance.
- */
- constructor () {
-
- }
-
- async init () {
+ * Load config then initialize children
+ * @public
+ * @return {void}
+ */
+ async init() {
await this.buildConfigManager();
this.buildImportManager();
@@ -33,8 +35,14 @@ class CoreApp {
this.buildMainServer();
}
- async buildConfigManager () {
- this.configManager = new ConfigManager(path.join(__dirname, '../..'));
+ /**
+ * Creates a new instance of the ConfigManager, loads and checks
+ * the server config
+ * @private
+ * @return {void}
+ */
+ async buildConfigManager() {
+ this.configManager = new ConfigManager(join(__dirname, '../..'));
this.config = await this.configManager.load();
if (this.config === false) {
@@ -43,23 +51,43 @@ class CoreApp {
}
}
- buildImportManager () {
- this.dynamicImports = new ImportsManager(path.join(__dirname, '../..'));
+ /**
+ * Creates a new instance of the ImportsManager
+ * @private
+ * @return {void}
+ */
+ buildImportManager() {
+ this.dynamicImports = new ImportsManager(join(__dirname, '../..'));
}
- buildCommandsManager () {
+ /**
+ * Creates a new instance of the CommandManager and loads the command modules
+ * @private
+ * @return {void}
+ */
+ buildCommandsManager() {
this.commands = new CommandManager(this);
this.commands.loadCommands();
}
- buildStatsManager () {
+ /**
+ * Creates a new instance of the StatsManager and sets the server start time
+ * @private
+ * @return {void}
+ */
+ buildStatsManager() {
this.stats = new StatsManager(this);
this.stats.set('start-time', process.hrtime());
}
- buildMainServer () {
+ /**
+ * Creates a new instance of the MainServer
+ * @private
+ * @return {void}
+ */
+ buildMainServer() {
this.server = new MainServer(this);
}
}
-module.exports = CoreApp;
+export { CoreApp };
diff --git a/server/src/serverLib/ImportsManager.js b/server/src/serverLib/ImportsManager.js
index 8db7b68..ac1fc4c 100644
--- a/server/src/serverLib/ImportsManager.js
+++ b/server/src/serverLib/ImportsManager.js
@@ -1,69 +1,77 @@
+import {
+ resolve,
+ basename as _basename,
+ relative,
+} from 'path';
+import RecursiveRead from 'readdir-recursive';
+
/**
* Import managment base, used to load commands/protocol and configuration objects
- *
- * Version: v2.0.0
- * Developer: Marzavec ( https://github.com/marzavec )
- * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
- *
+ * @property {String} base - Base path that all imports are required in from
+ * @author Marzavec ( https://github.com/marzavec )
+ * @version v2.0.0
+ * @license WTFPL ( http://www.wtfpl.net/txt/copying/ )
*/
-
-const read = require('readdir-recursive');
-const path = require('path');
-
class ImportsManager {
/**
- * Create a `ImportsManager` instance for (re)loading classes and config
- *
+ * Create an `ImportsManager` instance for (re)loading classes and config
* @param {String} basePath executing directory name; default __dirname
*/
- constructor (basePath) {
+ constructor(basePath) {
+ /**
+ * Stored reference to the base directory path
+ * @type {String}
+ */
this.basePath = basePath;
+
+ /**
+ * Data holder for imported modules
+ * @type {Object}
+ */
this.imports = {};
}
/**
* Pull base path that all imports are required in from
- *
+ * @public
* @type {String} readonly
*/
- get base () {
+ get base() {
return this.basePath;
}
/**
* Gather all js files from target directory, then verify and load
- *
- * @param {String} dirName The name of the dir to load, relative to the basePath.
- *
+ * @param {String} dirName The name of the dir to load, relative to the basePath
+ * @private
* @return {String} Load errors or empty if none
*/
- loadDir (dirName) {
- const dir = path.resolve(this.basePath, dirName);
+ loadDir(dirName) {
+ const dir = resolve(this.basePath, dirName);
let errorText = '';
try {
- read.fileSync(dir).forEach(file => {
- const basename = path.basename(file);
+ RecursiveRead.fileSync(dir).forEach((file) => {
+ const basename = _basename(file);
if (basename.startsWith('_') || !basename.endsWith('.js')) return;
let imported;
try {
imported = require(file);
+
+ if (!this.imports[dirName]) {
+ this.imports[dirName] = {};
+ }
+
+ this.imports[dirName][file] = imported;
} catch (e) {
- let err = `Unable to load modules from ${dirName} (${path.relative(dir, file)})\n${e}`;
+ const err = `Unable to load modules from ${dirName} (${relative(dir, file)})\n${e}`;
errorText += err;
console.error(err);
- return errorText;
- }
-
- if (!this.imports[dirName]) {
- this.imports[dirName] = {};
}
-
- this.imports[dirName][file] = imported;
});
} catch (e) {
- let err = `Unable to load modules from ${dirName}\n${e}`;
+ const err = `Unable to load modules from ${dirName}\n${e}`;
errorText += err;
console.error(err);
return errorText;
@@ -75,15 +83,13 @@ class ImportsManager {
/**
* Unlink references to each loaded module, pray to google that gc knows it's job,
* then reinitialize this class to start the reload
- *
- * @param {Array} dirName The name of the dir to load, relative to the _base path.
- *
+ * @public
* @return {String} Load errors or empty if none
*/
- reloadDirCache () {
+ reloadDirCache() {
let errorText = '';
- Object.keys(this.imports).forEach(dir => {
+ Object.keys(this.imports).forEach((dir) => {
Object.keys(this.imports[dir]).forEach((mod) => {
delete require.cache[require.resolve(mod)];
});
@@ -97,20 +103,19 @@ class ImportsManager {
/**
* Pull reference to imported modules that were imported from dirName, or
* load required directory if not found
- *
* @param {String} dirName The name of the dir to load, relative to the _base path.
- *
+ * @public
* @return {Object} Object containing command module paths and structs
*/
- getImport (dirName) {
- let imported = this.imports[dirName];
+ getImport(dirName) {
+ const imported = this.imports[dirName];
if (!imported) {
this.loadDir(dirName);
}
- return Object.assign({}, this.imports[dirName]);
+ return { ...this.imports[dirName] };
}
}
-module.exports = ImportsManager;
+export default ImportsManager;
diff --git a/server/src/serverLib/MainServer.js b/server/src/serverLib/MainServer.js
index 8ef5129..35a0d3e 100644
--- a/server/src/serverLib/MainServer.js
+++ b/server/src/serverLib/MainServer.js
@@ -1,58 +1,96 @@
+import {
+ Server as WsServer,
+ OPEN as SocketReady,
+} from 'ws';
+import { createHash } from 'crypto';
+import RateLimiter from './RateLimiter';
+
+import { ServerConst } from '../utility/Constants';
+
/**
* Main websocket server handling communications and connection events
- *
- * Version: v2.0.0
- * Developer: Marzavec ( https://github.com/marzavec )
- * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
- *
+ * @property {RateLimiter} police - Main rate limit handler
+ * @property {String} cmdKey - Internal use command key
+ * @author Marzavec ( https://github.com/marzavec )
+ * @version v2.0.0
+ * @license WTFPL ( http://www.wtfpl.net/txt/copying/ )
*/
-
-const WsServer = require('ws').Server;
-const SocketReady = require('ws').OPEN;
-const Crypto = require('crypto');
-const RateLimiter = require('./RateLimiter');
-const PulseSpeed = 16000; // ping all clients every X ms
-const IpSalt = [...Array(Math.floor(Math.random()*128)+128)].map(i=>(~~(Math.random()*36)).toString(36)).join('');
-const InternalCmdKey = [...Array(Math.floor(Math.random()*128)+128)].map(i=>(~~(Math.random()*36)).toString(36)).join('');
-
class MainServer extends WsServer {
/**
- * Create a HackChat server instance.
- *
- * @param {Object} core Reference to the global core object
- */
- constructor (core) {
+ * Create a HackChat server instance
+ * @param {CoreApp} core Reference to the global core object
+ */
+ constructor(core) {
super({ port: core.config.websocketPort });
+ /**
+ * Stored reference to the core
+ * @type {CoreApp}
+ */
this.core = core;
+
+ /**
+ * Command key used to verify internal commands
+ * @type {String}
+ */
+ this.internalCmdKey = [...Array(Math.floor(Math.random() * 128) + 128)].map(() => (~~(Math.random() * 36)).toString(36)).join('');
+
+ /**
+ * Salt used to hash a clients ip
+ * @type {String}
+ */
+ this.ipSalt = [...Array(Math.floor(Math.random() * 128) + 128)].map(() => (~~(Math.random() * 36)).toString(36)).join('');
+
+ /**
+ * Data store for command hooks
+ * @type {Object}
+ */
this.hooks = {};
+
+ /**
+ * Main rate limit tracker
+ * @type {RateLimiter}
+ */
this.police = new RateLimiter();
+
+ /**
+ * Black listed command names
+ * @type {Object}
+ */
this.cmdBlacklist = {};
+ /**
+ * Stored info about the last server error
+ * @type {ErrorEvent}
+ */
+ this.lastErr = null;
+
this.setupServer();
this.loadHooks();
}
/**
* Internal command key getter. Used to verify that internal only commands
- * originate internally and not from a connected client.
- * TODO: update to a structure that cannot be passed through json
- *
- * @type {String} readonly
+ * originate internally and not from a connected client
+ * @todo Update to a structure that cannot be passed through json
+ * @type {String}
+ * @public
+ * @readonly
*/
- get cmdKey () {
- return InternalCmdKey;
+ get cmdKey() {
+ return this.internalCmdKey;
}
/**
* Create ping interval and setup server event listeners
- *
+ * @private
+ * @return {void}
*/
- setupServer () {
- this.heartBeat = setInterval(() => this.beatHeart(), PulseSpeed);
+ setupServer() {
+ this.heartBeat = setInterval(() => this.beatHeart(), ServerConst.PulseSpeed);
this.on('error', (err) => {
- this.handleError('server', err);
+ this.handleError(err);
});
this.on('connection', (socket, request) => {
@@ -62,66 +100,71 @@ class MainServer extends WsServer {
/**
* Send empty `ping` frame to each client
- *
+ * @private
+ * @return {void}
*/
- beatHeart () {
- let targetSockets = this.findSockets({});
+ beatHeart() {
+ const targetSockets = this.findSockets({});
if (targetSockets.length === 0) {
return;
}
- for (let i = 0, l = targetSockets.length; i < l; i++) {
+ for (let i = 0, l = targetSockets.length; i < l; i += 1) {
try {
if (targetSockets[i].readyState === SocketReady) {
targetSockets[i].ping();
}
- } catch (e) { }
+ } catch (e) { /* yolo */ }
}
}
/**
* Bind listeners for the new socket created on connection to this class
- *
- * @param {Object} socket New socket object
+ * @param {ws#WebSocket} socket New socket object
* @param {Object} request Initial headers of the new connection
+ * @private
+ * @return {void}
*/
- newConnection (socket, request) {
- socket.remoteAddress = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
+ newConnection(socket, request) {
+ const newSocket = socket;
- socket.on('message', (data) => {
+ newSocket.address = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
+
+ newSocket.on('message', (data) => {
this.handleData(socket, data);
});
- socket.on('close', () => {
+ newSocket.on('close', () => {
this.handleClose(socket);
});
- socket.on('error', (err) => {
- this.handleError(socket, err);
+ newSocket.on('error', (err) => {
+ this.handleError(err);
});
}
/**
* Handle incoming messages from clients, parse and check command, then hand-off
- *
- * @param {Object} socket Calling socket object
+ * @param {ws#WebSocket} socket Calling socket object
* @param {String} data Message sent from client
+ * @private
+ * @return {void}
*/
- handleData (socket, data) {
+ handleData(socket, data) {
// Don't penalize yet, but check whether IP is rate-limited
- if (this.police.frisk(socket.remoteAddress, 0)) {
+ if (this.police.frisk(socket.address, 0)) {
this.core.commands.handleCommand(this, socket, {
cmd: 'socketreply',
cmdKey: this.cmdKey,
- text: 'You are being rate-limited or blocked.'
+ text: 'You are being rate-limited or blocked.',
});
return;
}
// Penalize here, but don't do anything about it
- this.police.frisk(socket.remoteAddress, 1);
+ this.police.frisk(socket.address, 1);
// Ignore ridiculously large packets
if (data.length > 65536) {
@@ -141,11 +184,11 @@ class MainServer extends WsServer {
return;
}
- // TODO: make this more flexible
- /*
- * Issue #1: hard coded `cmd` check
- * Issue #2: hard coded `cmd` value checks
- */
+ /**
+ * @todo make the following more flexible
+ * Issue #1: hard coded `cmd` check
+ * Issue #2: hard coded `cmd` value checks
+ */
if (typeof payload.cmd === 'undefined') {
return;
}
@@ -161,7 +204,7 @@ class MainServer extends WsServer {
if (typeof this.cmdBlacklist[payload.cmd] === 'function') {
return;
}
- // End TODO //
+ // End @todo //
// Execute `in` (incoming data) hooks and process results
payload = this.executeHooks('in', socket, payload);
@@ -171,11 +214,11 @@ class MainServer extends WsServer {
this.core.commands.handleCommand(this, socket, {
cmd: 'socketreply',
cmdKey: this.cmdKey,
- text: payload
+ text: payload,
});
return;
- } else if (payload === false) {
+ } if (payload === false) {
// A hook requested this data be dropped
return;
}
@@ -185,87 +228,107 @@ class MainServer extends WsServer {
}
/**
- * Handle socket close from clients
- *
- * @param {Object} socket Closing socket object
+ * Pass socket close event to disconnection command module
+ * @param {ws#WebSocket} socket Closing socket object
+ * @private
+ * @return {void}
*/
- handleClose (socket) {
+ handleClose(socket) {
this.core.commands.handleCommand(this, socket, {
cmd: 'disconnect',
- cmdKey: this.cmdKey
+ cmdKey: this.cmdKey,
});
}
/**
* "Handle" server or socket errors
- *
- * @param {Object||String} socket Calling socket object, or 'server'
- * @param {String} err The sad stuff
+ * @param {ErrorEvent} err The sad stuff
+ * @private
+ * @return {void}
*/
- handleError (socket, err) {
+ handleError(err) {
+ this.lastErr = err;
console.log(`Server error: ${err}`);
}
/**
* Send data payload to specific socket/client
- *
* @param {Object} payload Object to convert to json for transmission
- * @param {Object} socket The target client
+ * @param {ws#WebSocket} socket The target client
+ * @example
+ * server.send({
+ * cmd: 'info',
+ * text: 'Only targetSocket will see this'
+ * }, targetSocket);
+ * @public
+ * @return {void}
*/
- send (payload, socket) {
+ send(payload, socket) {
+ let outgoingPayload = payload;
+
// Add timestamp to command
- payload.time = Date.now();
+ outgoingPayload.time = Date.now();
// Execute `in` (incoming data) hooks and process results
- payload = this.executeHooks('out', socket, payload);
+ outgoingPayload = this.executeHooks('out', socket, outgoingPayload);
- if (typeof payload === 'string') {
+ if (typeof outgoingPayload === 'string') {
// A hook malfunctioned, reply with error
this.core.commands.handleCommand(this, socket, {
cmd: 'socketreply',
cmdKey: this.cmdKey,
- text: payload
+ text: outgoingPayload,
});
return;
- } else if (payload === false) {
+ } if (outgoingPayload === false) {
// A hook requested this data be dropped
return;
}
try {
if (socket.readyState === SocketReady) {
- socket.send(JSON.stringify(payload));
+ socket.send(JSON.stringify(outgoingPayload));
}
- } catch (e) { }
+ } catch (e) { /* yolo */ }
}
/**
* Overload function for `this.send()`
- *
* @param {Object} payload Object to convert to json for transmission
- * @param {Object} socket The target client
+ * @param {ws#WebSocket} socket The target client
+ * @example
+ * server.reply({
+ * cmd: 'info',
+ * text: 'Only targetSocket will see this'
+ * }, targetSocket);
+ * @public
+ * @return {void}
*/
- reply (payload, socket) {
+ reply(payload, socket) {
this.send(payload, socket);
}
/**
* Finds sockets/clients that meet the filter requirements, then passes the data to them
- *
* @param {Object} payload Object to convert to json for transmission
* @param {Object} filter see `this.findSockets()`
- *
+ * @example
+ * server.broadcast({
+ * cmd: 'info',
+ * text: 'Everyone in "programming" will see this'
+ * }, { channel: 'programming' });
+ * @public
* @return {Boolean} False if no clients matched the filter, true if data sent
*/
- broadcast (payload, filter) {
- let targetSockets = this.findSockets(filter);
+ broadcast(payload, filter) {
+ const targetSockets = this.findSockets(filter);
if (targetSockets.length === 0) {
return false;
}
- for (let i = 0, l = targetSockets.length; i < l; i++) {
+ for (let i = 0, l = targetSockets.length; i < l; i += 1) {
this.send(payload, targetSockets[i]);
}
@@ -274,51 +337,54 @@ class MainServer extends WsServer {
/**
* Finds sockets/clients that meet the filter requirements, returns result as array
- *
* @param {Object} data Object to convert to json for transmission
* @param {Object} filter The socket must of equal or greater attribs matching `filter`
- * = {} // matches all
- * = { channel: 'programming' } // matches any socket where (`socket.channel` === 'programming')
- * = { channel: 'programming', nick: 'Marzavec' } // matches any socket where (`socket.channel` === 'programming' && `socket.nick` === 'Marzavec')
- *
+ * @example
+ * // match all sockets:
+ * `filter` = {}
+ * // match any socket where socket.channel === 'programming'
+ * `filter` = { channel: 'programming' }
+ * // match any socket where
+ * // socket.channel === 'programming' && socket.nick === 'Marzavec'
+ * `filter` = { channel: 'programming', nick: 'Marzavec' }
+ * @public
* @return {Array} Clients who matched the filter requirements
*/
- findSockets (filter) {
- let filterAttribs = Object.keys(filter);
- let reqCount = filterAttribs.length;
+ findSockets(filter) {
+ const filterAttribs = Object.keys(filter);
+ const reqCount = filterAttribs.length;
let curMatch;
- let matches = [];
- for ( let socket of this.clients ) {
+ const matches = [];
+ this.clients.forEach((socket) => {
+ // for (const socket of this.clients) {
curMatch = 0;
- for (let i = 0; i < reqCount; i++) {
+ for (let i = 0; i < reqCount; i += 1) {
if (typeof socket[filterAttribs[i]] !== 'undefined') {
- switch(typeof filter[filterAttribs[i]]) {
+ switch (typeof filter[filterAttribs[i]]) {
case 'object': {
if (Array.isArray(filter[filterAttribs[i]])) {
if (filter[filterAttribs[i]].indexOf(socket[filterAttribs[i]]) !== -1) {
- curMatch++;
- }
- } else {
- if (socket[filterAttribs[i]] === filter[filterAttribs[i]]) {
- curMatch++;
+ curMatch += 1;
}
+ } else if (socket[filterAttribs[i]] === filter[filterAttribs[i]]) {
+ curMatch += 1;
}
- break;
+ break;
}
case 'function': {
if (filter[filterAttribs[i]](socket[filterAttribs[i]])) {
- curMatch++;
+ curMatch += 1;
}
- break;
+ break;
}
default: {
if (socket[filterAttribs[i]] === filter[filterAttribs[i]]) {
- curMatch++;
+ curMatch += 1;
}
- break;
+ break;
}
}
}
@@ -327,7 +393,7 @@ class MainServer extends WsServer {
if (curMatch === reqCount) {
matches.push(socket);
}
- }
+ });
return matches;
}
@@ -335,18 +401,20 @@ class MainServer extends WsServer {
/**
* Hashes target socket's remote address using non-static variable length salt
* encodes and shortens the output, returns that value
- *
- * @param {Object||String} target Either the target socket or ip as string
- *
+ * @param {(ws#WebSocket|String)} target Either the target socket or ip as string
+ * @example
+ * let userHash = server.getSocketHash('1.2.3.4');
+ * let userHash = server.getSocketHash(client);
+ * @public
* @return {String} Hashed client connection string
*/
- getSocketHash (target) {
- let sha = Crypto.createHash('sha256');
+ getSocketHash(target) {
+ const sha = createHash('sha256');
if (typeof target === 'string') {
- sha.update(target + IpSalt);
+ sha.update(target + this.ipSalt);
} else {
- sha.update(target.remoteAddress + IpSalt);
+ sha.update(target.address + this.ipSalt);
}
return sha.digest('base64').substr(0, 15);
@@ -355,9 +423,10 @@ class MainServer extends WsServer {
/**
* (Re)loads all command module hooks, then sorts their order of operation by
* priority, ascending (0 being highest priority)
- *
+ * @public
+ * @return {void}
*/
- loadHooks () {
+ loadHooks() {
// clear current hooks (if any)
this.clearHooks();
// notify each module to register their hooks (if any)
@@ -366,23 +435,23 @@ class MainServer extends WsServer {
let curHooks = [];
let hookObj = [];
- if (typeof this.hooks['in'] !== 'undefined') {
+ if (typeof this.hooks.in !== 'undefined') {
// start sorting, with incoming first
- curHooks = [ ...this.hooks['in'].keys() ];
- for (let i = 0, j = curHooks.length; i < j; i++) {
- hookObj = this.hooks['in'].get(curHooks[i]);
- hookObj.sort( (h1, h2) => h1.priority - h2.priority );
- this.hooks['in'].set(hookObj);
+ curHooks = [...this.hooks.in.keys()];
+ for (let i = 0, j = curHooks.length; i < j; i += 1) {
+ hookObj = this.hooks.in.get(curHooks[i]);
+ hookObj.sort((h1, h2) => h1.priority - h2.priority);
+ this.hooks.in.set(hookObj);
}
}
- if (typeof this.hooks['out'] !== 'undefined') {
+ if (typeof this.hooks.out !== 'undefined') {
// then outgoing
- curHooks = [ ...this.hooks['out'].keys() ];
- for (let i = 0, j = curHooks.length; i < j; i++) {
- hookObj = this.hooks['out'].get(curHooks[i]);
- hookObj.sort( (h1, h2) => h1.priority - h2.priority );
- this.hooks['out'].set(hookObj);
+ curHooks = [...this.hooks.out.keys()];
+ for (let i = 0, j = curHooks.length; i < j; i += 1) {
+ hookObj = this.hooks.out.get(curHooks[i]);
+ hookObj.sort((h1, h2) => h1.priority - h2.priority);
+ this.hooks.out.set(hookObj);
}
}
}
@@ -391,17 +460,19 @@ class MainServer extends WsServer {
* Adds a target function to an array of hooks. Hooks are executed either before
* processing user input (`in`) or before sending data back to the client (`out`)
* and allows a module to modify each payload before moving forward
- *
* @param {String} type The type of event, typically `in` (incoming) or `out` (outgoing)
* @param {String} command Should match the desired `cmd` attrib of the payload
- * @param {Function} hookFunction Target function to execute, should accept `server`, `socket` and `payload` as parameters
- * @param {Number} priority Execution priority, hooks with priority 1 will be executed before hooks with priority 200 for example
+ * @param {Function} hookFunction Target function to execute, should accept
+ * `server`, `socket` and `payload` as parameters
+ * @param {Number} priority Execution priority, hooks with priority 1 will be executed before
+ * hooks with priority 200 for example
+ * @example
+ * // Create hook to add "and stuff" to every chat line
+ * server.registerHook('in', 'chat', (server, socket, payload) => payload.text += ' and stuff');
+ * @public
+ * @return {void}
*/
- registerHook (type, command, hookFunction, priority) {
- if (typeof priority === 'undefined') {
- priority = 25;
- }
-
+ registerHook(type, command, hookFunction, priority = 25) {
if (typeof this.hooks[type] === 'undefined') {
this.hooks[type] = new Map();
}
@@ -412,7 +483,7 @@ class MainServer extends WsServer {
this.hooks[type].get(command).push({
run: hookFunction,
- priority: priority
+ priority,
});
}
@@ -422,25 +493,25 @@ class MainServer extends WsServer {
* A payload (modified or not) that will continue through the data flow
* A boolean false to indicate halting the data through flow
* A string which indicates an error occured in executing the hook
- *
* @param {String} type The type of event, typically `in` (incoming) or `out` (outgoing)
- * @param {Object} socket Either the target client or the client triggering the hook (depending on `type`)
- * @param {Object} payload Either incoming data from client or outgoing data (depending on `type`)
- *
- * @return {Object || Boolean}
+ * @param {ws#WebSocket} socket Either target client or client (depends on `type`)
+ * @param {Object} payload Either incoming data from client or outgoing data (depends on `type`)
+ * @private
+ * @return {Object|Boolean}
*/
- executeHooks (type, socket, payload) {
- let command = payload.cmd;
+ executeHooks(type, socket, payload) {
+ const command = payload.cmd;
+ let newPayload = payload;
if (typeof this.hooks[type] !== 'undefined') {
if (this.hooks[type].has(command)) {
- let hooks = this.hooks[type].get(command);
+ const hooks = this.hooks[type].get(command);
- for (let i = 0, j = hooks.length; i < j; i++) {
+ for (let i = 0, j = hooks.length; i < j; i += 1) {
try {
- payload = hooks[i].run(this.core, this, socket, payload);
+ newPayload = hooks[i].run(this.core, this, socket, newPayload);
} catch (err) {
- let errText = `Hook failure, '${type}', '${command}': `;
+ const errText = `Hook failure, '${type}', '${command}': `;
if (this.core.config.logErrDetailed === true) {
console.log(errText + err.stack);
} else {
@@ -450,23 +521,24 @@ class MainServer extends WsServer {
}
// A hook function may choose to return false to prevent all further processing
- if (payload === false) {
+ if (newPayload === false) {
return false;
}
}
}
}
- return payload;
+ return newPayload;
}
/**
* Wipe server hooks to make ready for module reload calls
- *
+ * @public
+ * @return {void}
*/
- clearHooks () {
+ clearHooks() {
this.hooks = {};
}
}
-module.exports = MainServer;
+export default MainServer;
diff --git a/server/src/serverLib/RateLimiter.js b/server/src/serverLib/RateLimiter.js
index 43cf077..3f9bc27 100644
--- a/server/src/serverLib/RateLimiter.js
+++ b/server/src/serverLib/RateLimiter.js
@@ -1,39 +1,60 @@
+import { RateLimits } from '../utility/Constants';
+
/**
* Tracks frequency of occurances based on `id` (remote address), then allows or
* denies command execution based on comparison with `threshold`
- *
- * Version: v2.0.0
- * Developer: Marzavec ( https://github.com/marzavec )
- * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
- *
+ * @property {Object} data - The current stats data
+ * @author Marzavec ( https://github.com/marzavec )
+ * @author Andrew Belt ( https://github.com/AndrewBelt )
+ * @version v2.0.0
+ * @license WTFPL ( http://www.wtfpl.net/txt/copying/ )
*/
-
class RateLimiter {
/**
- * Create a ratelimiter instance.
- */
- constructor () {
+ * Create a ratelimiter instance
+ */
+ constructor() {
+ /**
+ * Data holder rate limit records
+ * @type {Object}
+ */
this.records = {};
- this.halflife = 30 * 1000; // milliseconds
- this.threshold = 25;
+
+ /**
+ * Time in milliseconds to decrement ratelimit weight
+ * @type {Number}
+ */
+ this.halflife = RateLimits.halflife;
+
+ /**
+ * Weight until ratelimited
+ * @type {Number}
+ */
+ this.threshold = RateLimits.threshold;
+
+ /**
+ * Stores the associated connection fingerprint with record id
+ * @type {Array}
+ */
this.hashes = [];
}
/**
* Finds current score by `id`
- *
* @param {String} id target id / address
- *
+ * @private
* @return {Object} Object containing the record meta
*/
- search (id) {
+ search(id) {
let record = this.records[id];
if (!record) {
- record = this.records[id] = {
+ this.records[id] = {
time: Date.now(),
- score: 0
- }
+ score: 0,
+ };
+
+ record = this.records[id];
}
return record;
@@ -41,20 +62,22 @@ class RateLimiter {
/**
* Adjusts the current ratelimit score by `deltaScore`
- *
* @param {String} id target id / address
* @param {Number} deltaScore amount to adjust current score by
- *
+ * @example
+ * // Penalize by 1 and store if connection is ratelimited or not
+ * let isLimited = police.frisk(socket.address, 1);
+ * @public
* @return {Boolean} True if record threshold has been exceeded
*/
- frisk (id, deltaScore) {
- let record = this.search(id);
+ frisk(id, deltaScore) {
+ const record = this.search(id);
if (record.arrested) {
return true;
}
- record.score *= Math.pow(2, -(Date.now() - record.time ) / this.halflife);
+ record.score *= 2 ** -(Date.now() - record.time) / this.halflife;
record.score += deltaScore;
record.time = Date.now();
@@ -67,11 +90,16 @@ class RateLimiter {
/**
* Statically set server to no longer accept traffic from `id`
- *
* @param {String} id target id / address
+ * @example
+ * // Usage within a command module:
+ * let badClient = server.findSockets({ channel: socket.channel, nick: targetNick });
+ * server.police.arrest(badClient[0].address, badClient[0].hash);
+ * @public
+ * @return {void}
*/
- arrest (id, hash) {
- let record = this.search(id);
+ arrest(id, hash) {
+ const record = this.search(id);
record.arrested = true;
this.hashes[hash] = id;
@@ -79,17 +107,32 @@ class RateLimiter {
/**
* Remove statically assigned limit from `id`
- *
* @param {String} id target id / address
+ * @example
+ * // Usage within a command module:
+ * server.police.pardon('targetHashOrIP');
+ * @public
+ * @return {void}
*/
- pardon (id) {
- if (typeof this.hashes[id] !== 'undefined') {
- id = this.hashes[id];
+ pardon(id) {
+ let targetId = id;
+ if (typeof this.hashes[targetId] !== 'undefined') {
+ targetId = this.hashes[targetId];
}
- let record = this.search(id);
+ const record = this.search(targetId);
record.arrested = false;
}
+
+ /**
+ * Clear all records
+ * @public
+ * @return {void}
+ */
+ clear() {
+ this.records = {};
+ this.hashes = [];
+ }
}
-module.exports = RateLimiter;
+export default RateLimiter;
diff --git a/server/src/serverLib/StatsManager.js b/server/src/serverLib/StatsManager.js
index e472504..b6e4f97 100644
--- a/server/src/serverLib/StatsManager.js
+++ b/server/src/serverLib/StatsManager.js
@@ -1,60 +1,79 @@
/**
* Simple generic stats collection script for events occurances (etc)
- *
- * Version: v2.0.0
- * Developer: Marzavec ( https://github.com/marzavec )
- * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
- *
+ * @property {Object} data - The current stats data
+ * @author Marzavec ( https://github.com/marzavec )
+ * @version v2.0.0
+ * @license WTFPL ( http://www.wtfpl.net/txt/copying/ )
*/
-
class StatsManager {
/**
- * Create a stats instance.
- *
+ * Create a stats instance
*/
- constructor () {
+ constructor() {
+ /**
+ * Data holder for the stats class
+ * @type {Object}
+ */
this.data = {};
}
/**
* Retrieve value of arbitrary `key` reference
- *
* @param {String} key Reference to the arbitrary store name
- *
+ * @example
+ * // Find previously set `start-time`
+ * stats.get('start-time');
+ * @public
* @return {*} Data referenced by `key`
*/
- get (key) {
+ get(key) {
return this.data[key];
}
/**
* Set value of arbitrary `key` reference
- *
* @param {String} key Reference to the arbitrary store name
* @param {Number} value New value for `key`
+ * @example
+ * // Set `start-time`
+ * stats.set('start-time', process.hrtime());
+ * @public
+ * @return {void}
*/
- set (key, value) {
+ set(key, value) {
this.data[key] = value;
}
/**
* Increase value of arbitrary `key` reference, by 1 or `amount`
- *
* @param {String} key Reference to the arbitrary store name
- * @param {Number} amount Value to increase `key` by, or 1 if omitted
+ * @param {?Number} [amount=1] Value to increase `key` by, or 1 if omitted
+ * @example
+ * // Increment by `amount`
+ * stats.increment('users', 6);
+ * // Increment by 1
+ * stats.increment('users');
+ * @public
+ * @return {void}
*/
- increment (key, amount) {
- this.set(key, (this.get(key) || 0) + (amount || 1));
+ increment(key, amount = 1) {
+ this.set(key, (this.get(key) || 0) + amount);
}
/**
* Reduce value of arbitrary `key` reference, by 1 or `amount`
- *
* @param {String} key Reference to the arbitrary store name
- * @param {Number} amount Value to decrease `key` by, or 1 if omitted
+ * @param {?Number} [amount=1] Value to decrease `key` by, or 1 if omitted
+ * @example
+ * // Decrement by `amount`
+ * stats.decrement('users', 6);
+ * // Decrement by 1
+ * stats.decrement('users');
+ * @public
+ * @return {void}
*/
- decrement (key, amount) {
- this.set(key, (this.get(key) || 0) - (amount || 1));
+ decrement(key, amount = 1) {
+ this.set(key, (this.get(key) || 0) - amount);
}
}
diff --git a/server/src/serverLib/index.js b/server/src/serverLib/index.js
index 4583de6..a820588 100644
--- a/server/src/serverLib/index.js
+++ b/server/src/serverLib/index.js
@@ -1,8 +1,6 @@
-module.exports = {
- CommandManager: require('./CommandManager'),
- ConfigManager: require('./ConfigManager'),
- ImportsManager: require('./ImportsManager'),
- MainServer: require('./MainServer'),
- RateLimiter: require('./RateLimiter'),
- StatsManager: require('./StatsManager')
-};
+export const CommandManager = require('./CommandManager').default;
+export const ConfigManager = require('./ConfigManager').default;
+export const ImportsManager = require('./ImportsManager').default;
+export const MainServer = require('./MainServer').default;
+export const RateLimiter = require('./RateLimiter').default;
+export const StatsManager = require('./StatsManager');