adaptorex
Version:
Connect all your live interactive storytelling devices and software
1,013 lines (926 loc) • 27.3 kB
JavaScript
/**
* creates framework for starting, creating, loading, editing sessions.
*
* Uses minimist to parse optional arguments
*
* Express API server with json parse.
*
* @requires mongo
* @requires ne
* @requires game
* @requires file
*
* @requires express
* @requires http
* @requires socket.io
* @requires path
* @requires readline
* @requires os
*
* @requires minimist
*
* @module index
* @copyright Lasse Marburg 2021
* @license MIT
*/
require("./adaptor.js")
const mongo = require("./mongo.js")
const ne = require("./ne.js")
const game = require("./game.js")
const file = require("./file.js")
const auth = require("./authentication.js")
const readline = require('readline')
const os = require("os")
var argv = require('minimist')(process.argv.slice(2));
const errors = require('./errors')
const cors = require('cors')
const path = require('path')
const express = require("express")
var app = express()
var server = require('http').createServer(app);
io = require('socket.io')(server, {
cors: {
origin: isWhitelisted,
methods: ["GET", "POST"],
credentials: true
}
}
)
// socket.io will not handle BigInt (used by telegram plugin for IDs) well, since serialization fails without the lines below (see also: https://github.com/GoogleChromeLabs/jsbi/issues/30)
BigInt.prototype["toJSON"] = function () {
return this.toString()
}
/** @type {express.Router} */
var api = express.Router()
Object.assign(adaptor, errors)
/**
* global logging Object with coloring and logfile. Use log instead of console.log whenever possible.
*/
global.log = new adaptor.Log()
/** list of game Objects */
var games = {}
/** all plugin modules that were required */
var plugin_modules = {}
/** Database module depends on selection in config */
var database
/** openAPI document object */
var openapi
/**
* initiate adaptor based on config
*
* - create login security middleware.
* - create var data folder for game data
* - provide static route for example website
* - connect to Database
* - instantiate games that are listed in config
*
* @param {Object} config - Contains parameters to load existing games
* @param {Object} config.database - Database connection parameters
* @param {Array} [config.games] - List of games that will be loaded
*/
function init(config) {
let log_socket = io.of(/\/log\/\w+$/)
log.createSocket(log_socket)
Object.assign(adaptor, config)
if (config.authentication == 'basic') {
adaptor.auth = new auth.BasicAuth(config.users)
} else if (config.authentication == 'disabled') {
adaptor.auth = new auth.NoAuth({ login: "Anonymosaurus", password: "" })
} else {
log.warn(0, "No authentication method specified")
adaptor.auth = new auth.NoAuth({ login: "Anonymosaurus", password: "" })
}
log_socket.use(adaptor.auth.authenticateSocket.bind(adaptor.auth))
adaptor.getPlugin = getPlugin
adaptor.getAvailablePlugins = getAvailablePlugins
adaptor.now = function () {
return new Date(Date.now())
}
adaptor.userInfo = () => {
return os.userInfo()
}
if (!adaptor.python) {
if (process.platform == "win32") {
adaptor.python = "python"
} else {
adaptor.python = "python3"
}
}
adaptor.io = io
file.mkdir(path.join(adaptor.data, 'games'))
file.mkdir(path.join(adaptor.data, 'plugins'))
app.use(cors({ credentials: true, origin: isWhitelisted }))
app.use(express.json())
server.listen(config.port)
server.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
log.error("http server", "Port " + config.port + " is already in use. Close any other adaptor instances or other server that uses the same port and restart.")
process.exit(0)
}
})
if (!file.exists(path.join(__dirname, '/public/index.html'))) {
config.headless = true
}
adaptor.webhooks.push({name: "local", title: "Local", url: config.host + ":" + config.port})
let ips = adaptor.getIPs()
for (let ip in ips) {
let ip_name = ip.replace(/[^a-zA-Z0-9 ]/g, '').replace(' ', '_').toLowerCase()
adaptor.webhooks.push({name: ip_name, title: ip, url: `http://${ips[ip]}:${config.port}`})
}
if (config.url) {
adaptor.webhooks.push({name: "remote", title: "Remote", url: config.url})
}
server.on('listening', (res) => {
log.info(0, "Server is listening")
})
let homesocket = io.of('/')
homesocket.on('connection', client)
// Init Database
if (config.hasOwnProperty('database')) {
if (config.database["type"] == "mongodb") {
database = mongo
} else if (config.database["type"] == "nedb") {
database = ne
} else {
log.error(0, "Not a valid database type: " + config.database["type"] + "\n. Options: mongodb, nedb ")
process.exit(0)
}
} else {
database = ne
}
database.start(config.database)
.then(result => {
return adaptor.dereference(path.join(__dirname , '/doc/api/public/openapi.yaml'))
})
.then(result => {
openapi = result
adaptor.openapi = openapi
let load_game_promises = []
if (config.hasOwnProperty('games')) {
for (let game in config.games) {
load_game_promises.push(loadGame(game, config.games[game]))
}
return Promise.all(load_game_promises)
}
return
})
.then(result => {
log.info(0, `adaptor:ex server version ${adaptor.info.version} up and running. Startup time was ${process.uptime()} sec.`)
let issues = []
let errors = []
if(Array.isArray(result)) {
result.forEach(res => {
if(res.issues.length) {
issues = issues.concat(res.issues)
}
if(res.errors.length) {
errors = errors.concat(res.errors)
}
})
}
if(issues.length) {
log.warn(0, `Experienced ${issues.length} issue(s) on startup. See previous error messages.`)
}
if(errors.length) {
log.error(0, `Experienced ${errors.length} unexpected error(s) on startup. See previous error messages.`)
}
if (config.headless) {
log.info(0, "Access adaptor:ex API at one of the following urls")
logWebhooks("/api")
} else {
log.info(0, "Open adaptor:ex editor with one of the following links")
logWebhooks()
}
rl.prompt()
})
.catch(error => {
log.error(0, error)
process.exit(0)
})
// add API routing
api.use(adaptor.auth.authenticate.bind(adaptor.auth))
api.use((req, res, next) => {
log.trace("API", `${req.method} request to ${req.originalUrl} from ${req.user.login}`)
next()
})
api.get('/config', (req, res) => {
res.send(config)
})
api.get('/plugins', (req, res) => {
res.send(getAvailablePlugins())
})
api.get('/games', (req, res) => {
getGames()
.then(result => {
res.send(result)
})
.catch(err => {
log.error("API", err)
res.status(500).send({ name: err.name, message: err.message, stack: err.stack })
})
})
api.post('/game', (req, res) => {
if (typeof req.body.name === 'string') {
if (req.body.template) {
var data = { template: req.body.template }
} else {
var data = undefined
}
loadGame(req.body.name, data)
.then(function (result) {
res.status(201).send(result)
})
.catch(function (err) {
log.error("API", err)
res.status(500).send({ name: err.name, message: err.message, stack: err.stack })
})
} else {
log.error("API", "couldn't load game. Name property missing.")
res.status(400).send({ name: 'InvalidError', message: 'Name property missing.' })
}
})
api.post('/game/:game/setup', (req, res) => {
if (games[req.params.game]) {
games[req.params.game].editSetup(req.body)
.then(function (result) {
res.status(200).send(result)
})
.catch(function (err) {
log.error("API", err)
res.status(500).send({ name: err.name, message: err.message, stack: err.stack })
})
} else {
res.status(404).send({ name: 'NotFoundError', message: `game ${req.params.game} was not found` })
}
})
api.post('/game/:game/_event', (req, res) => {
if (games[req.params.game]) {
if(!req.body.name) {
log.error("API", "couldn't dispatch game event. Name property missing.")
res.status(400).send({ name: 'InvalidError', message: 'name property missing.' })
return
}
try {
games[req.params.game].event.emit(req.body.name, req.body.payload)
log.debug(req.params.game, `dispatch game event '${req.body.name}' via API`)
res.status(200).send({})
} catch (error) {
log.error("API", err)
res.status(500).send({ name: err.name, message: err.message, stack: err.stack })
}
} else {
res.status(404).send({ name: 'NotFoundError', message: `game ${req.params.game} was not found` })
}
})
api.get('/game/:game', (req, res) => {
if (games[req.params.game]) {
games[req.params.game].getElements()
.then(result => {
res.send(result)
})
.catch(err => {
log.error("API", err)
res.status(500).send({ name: err.name, message: err.message, stack: err.stack })
})
} else {
res.status(404).send({ name: 'NotFoundError', message: `game ${req.params.game} was not found` })
}
})
api.get('/game/:game/collections', (req, res) => {
if (games[req.params.game]) {
games[req.params.game].getDataCollectionSchemas()
.then(result => {
res.send(result)
})
.catch(err => {
log.error("API", err)
res.status(500).send({ name: err.name, message: err.message, stack: err.stack })
})
} else {
res.status(404).send({ name: 'NotFoundError', message: `game ${req.params.game} was not found` })
}
})
api.get('/game/:game/media', (req, res) => {
if (games[req.params.game]) {
Promise.resolve(games[req.params.game].getFiles())
.then(result => {
res.send(result)
})
.catch(err => {
log.error("API", err)
res.status(500).send({ name: err.name, message: err.message, stack: err.stack })
})
} else {
res.status(404).send({ name: 'NotFoundError', message: `game ${req.params.game} was not found` })
}
})
api.delete('/game/:game', (req, res) => {
if (typeof req.params.game === 'string') {
deleteGame(req.params.game)
.then(result => {
if (result) {
res.status(204).send()
} else {
res.status(404).send({ name: 'NotFoundError', message: `game ${req.params.game} was not found` })
}
})
.catch(err => {
log.error("API", err)
res.status(500).send({ name: err.name, message: err.message, stack: err.stack })
})
} else {
log.error("API", "couldn't delete game. Name property missing.")
res.status(400).send({ name: 'ValidationError', message: 'Name property missing.' })
}
})
api.get('/game/:game/actions', (req, res) => {
if (games[req.params.game]) {
games[req.params.game].getActions(req.query)
.then(result => {
res.send(result)
})
.catch(err => {
log.error("API", err)
res.status(500).send({ name: err.name, message: err.message, stack: err.stack })
})
} else {
res.status(404).send({ name: 'NotFoundError', message: `game ${req.params.game} was not found` })
}
})
api.get('/game/:game/level_schema', (req, res) => {
if (games[req.params.game]) {
games[req.params.game].getLevelSchema()
.then(result => {
res.send(result)
})
.catch(err => {
log.error("API", err)
res.status(500).send({ name: err.name, message: err.message, stack: err.stack })
})
} else {
res.status(404).send({ name: 'NotFoundError', message: `game ${req.params.game} was not found` })
}
})
app.use('/api', api)
api.use((err, req, res, next) => {
if (err instanceof SyntaxError) {
log.error("API", `${req.method} request to ${req.originalUrl} failed`)
log.error("API", err.message)
return res.status(400).send({ name: err.name, message: err.message })
}
log.error("API", err.message)
next(err)
})
if (!config.headless) {
var client_app = express.Router()
client_app.use('/', express.static(path.join(__dirname, '/public')))
// Host static webpage for local client build
client_app.get('/', function (req, res) {
res.sendFile(path.join(__dirname, 'public', 'index.html'))
})
client_app.get('/game/*', function (req, res) {
res.sendFile(path.join(__dirname, 'public', 'index.html'))
})
app.use('/', client_app)
}
}
function logWebhooks(append_path) {
for(let webhook of adaptor.webhooks) {
log.info(0, `${webhook.title}:\t${webhook.url}${append_path || ''}`)
}
}
/**
* check whitelist to see if cross origin access is allowed for origin url
*
* @param {*} origin
* @param {*} callback
*/
function isWhitelisted(origin, callback) {
if (typeof origin === 'undefined') {
callback(null, true)
} else if (origin.indexOf('//localhost') !== -1) {
callback(null, true)
} else if (config['whitelist']) {
var originIsWhitelisted = config.whitelist.indexOf(origin) !== -1;
if (!originIsWhitelisted) {
log.warn(0, `Rejected request from remote ${origin} because it is not in whitelist.`)
}
callback(null, originIsWhitelisted)
} else {
callback(null, true)
}
}
function client(socket) {
log.debug(0, "client connected on " + socket.handshake.headers.host)
}
/**
* get preview data for all loaded games
*
* @returns {Promise} list of game preview Objects `{name:"gamename",setup:{}}`
*/
async function getGames() {
let returnmsg = []
for (let game in games) {
let game_setup = await games[game].getSetup()
returnmsg.push({ name: game, setup: game_setup })
}
return returnmsg
}
/**
* - create Game Object with provided Database Object
* - resume if there is an existing database
* else build default game
* - open route to Game editing Website
* - note game in config file
*
* @param {string} name - name of game to be created or continued
* @param {Object} [game_data] - game specific config parameters or template name if its a new game
*/
function loadGame(name, game_data) {
if (!game_data) {
game_data = {}
}
return new Promise(function (resolve, reject) {
let name_convert_spaces = name.replace(/ /g, '%20')
var game_db = new database.Database({ "name": name_convert_spaces })
game_db.connect()
.then(result => {
let client = adaptor.io.of(`/game/${name_convert_spaces}`)
client.use(adaptor.auth.authenticateSocket.bind(adaptor.auth))
if (config.hasOwnProperty("url")) {
game_data['files_url'] = config.url + '/' + name_convert_spaces + '/files'
game_data['url'] = config.url + '/' + name_convert_spaces
} else {
game_data['files_url'] = config.host + ':' + config.port + '/' + name_convert_spaces + '/files'
game_data['url'] = config.host + ':' + config.port + '/' + name_convert_spaces
}
game_data['data'] = path.join(adaptor.data, 'games', name_convert_spaces)
game_data['files'] = path.join(adaptor.data, 'games', name_convert_spaces, 'files')
file.mkdir(game_data.data)
file.mkdir(path.join(game_data.data, 'functions'))
file.mkdir(game_data.files)
game_data['name'] = name
let game_app = express.Router()
app.use(`/${name_convert_spaces}`, game_app)
// Host Public game data
game_app.use('/', express.static(path.join(__dirname, '/' , game_data.data), { maxAge: 7200000 }))
let game_api = express.Router()
api.use(`/game/${name_convert_spaces}`, game_api)
let newgame = new game.Game(game_data, game_app, game_api, client)
games[name] = newgame
if (result == "created") {
if (game_data.hasOwnProperty("template")) {
return newgame.build(game_db, game_data.template)
} else {
return newgame.build(game_db, "basic")
}
} else if (result == "connected") {
return newgame.resume(game_db)
} else {
throw new Error(name + " could neither be resumed or created.")
}
})
.then(result => {
if (config_file) {
if (!config.hasOwnProperty('games')) {
config['games'] = {}
}
config.games[name] = game_data
file.saveJSON(config, config_file)
}
log.info(0, result.status + " game " + name)
return resolve(result)
})
.catch(error => {
log.error(0, "Unexpected Error while loading game " + name + ":")
log.error(0, error)
})
});
}
/**
* remove game but keep its data in database
*/
async function unloadGame(name) {
if (games.hasOwnProperty(name)) {
if (config_file) {
log.info("unload", config.games[name])
if (config.games.hasOwnProperty(name)) {
delete config.games[name]
file.saveJSON(config, config_file)
}
}
await games[name].unload()
delete games[name]
log.info('game', name + ' unloaded')
return true
} else {
log.error('unload', "No such Game '" + name + "'")
return false
}
}
/**
* unload and remove game and its Database
* does not create backup! Handle with care
*/
async function deleteGame(name) {
if (games.hasOwnProperty(name)) {
await games[name].db.clone("archive_" + name)
await games[name].delete()
await unloadGame(name)
log.info('delete', name + " was successfully deleted")
return true
} else {
log.error('delete', "No such Game '" + name + "'")
return false
}
}
/**
* Games that load a plugin, use this function, so that all plugin modules are only required once.
* If another game added the same plugin already already required module is used.
*
* plugin has to be a folder in the source plugin directory or the custom plugin directory and contain a package.json.
*
* @callback GetPlugin
* @param {string} - plugin name
* @returns {Object} new instance of the plugins main class
*/
function getPlugin(plugin) {
if (!plugin_modules.hasOwnProperty(plugin)) {
let plugins = this.getAvailablePlugins()
let package = plugins.find(p => p.name == plugin)
if(!package) {
throw new adaptor.NotFoundError("couldn't find " + plugin + " in any of the plugin directories")
}
if (file.exists(path.join(package.path , package.main))) {
plugin_modules[plugin] = require(package.path + '/' + package.main)
} else {
throw new adaptor.NotFoundError("couldn't find main file for " + plugin + " plugin at " + path.join(package.path, package.main))
}
plugin_modules[plugin].package = package
log.debug(0, "load plugin module " + plugin + " from " + package.path)
}
let plugin_object
if (plugin_modules[plugin].hasOwnProperty("Plugin")) {
plugin_object = new plugin_modules[plugin].Plugin()
plugin_object.name = plugin
plugin_object.package = plugin_modules[plugin].package
} else {
plugin_object = plugin_modules[plugin]
}
return plugin_object
}
/**
* list all plugins found in plugins directory
*
* @returns {Array<Object>} information dictionary about all plugins available
*/
function getAvailablePlugins() {
let plugin_directories = getPluginDirectories("plugins", true)
plugin_directories = plugin_directories.concat(getPluginDirectories(path.join(config.data, "plugins")))
if(config.hasOwnProperty("plugins")) {
for(let folder of config.plugins) {
plugin_directories = plugin_directories.concat(getPluginDirectories(folder))
}
}
let plugins = plugin_directories.map(plugin_dir => {
if (file.exists(path.join(plugin_dir , 'package.json'))) {
let package = require(plugin_dir + '/package.json')
package.path = plugin_dir
if(!package.package_name) {
package.package_name = package.name
}
Object.assign(package, package.adaptorex)
delete package.adaptorex
return package
}
})
return plugins
}
function getPluginDirectories(folder, is_app_dir) {
let dirs = []
if(path.isAbsolute(folder)) {
dirs = file.ls(folder, "directories").map(dir => path.join(folder, dir))
} else if (is_app_dir) {
dirs = file.ls(path.join(__dirname , folder), "directories").map(dir => path.join(__dirname, folder, dir))
} else {
dirs = file.ls(path.join(folder), "directories").map(dir => path.join(process.cwd(), folder, dir))
}
return dirs
}
/**
* give all games and plugins the chance to tidy up if necessary then exit node process
*
*/
function quit() {
let promises = []
for (let g in games) {
promises.push(games[g].quit())
}
Promise.all(promises)
.then((result) => {
log.info('', "Auf bald!")
process.exit(0)
}).catch((err) => {
log.error('', err)
process.exit(0)
})
}
/********************************************************************************************
* load config and check options
*
*********************************************************************************************/
/** path to and name of config file. If file is not found config will be created based on options
* @default
*/
var config_file = './adaptorex/config.json'
/** content of config file.
* @default
*/
var config = {
host: "http://localhost",
port: 8081,
data: "./adaptorex",
database: { type: "nedb" },
level: "info",
authentication: "disabled"
}
if (argv.hasOwnProperty('c')) {
setConfig(argv.c)
} else if (argv.hasOwnProperty('config')) {
setConfig(argv.config)
} else {
setConfig(config_file)
}
function setConfig(filename) {
config_file = filename
try {
config = require(path.join(process.cwd(), filename))
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') {
log.error(0, e)
}
log.warn(0, 'config file missing. Will create ' + filename)
file.mkdir(config.data)
}
}
config['headless'] = false
for (let arg in argv) {
if (arg == 'headless') {
config['headless'] = true
} else if (argv[arg] == true) {
log.warn(0, "parameters missing for option '" + arg + "'")
} else {
switch (arg) {
case 'level':
config['level'] = argv.level
break
case 'l':
config['level'] = argv.l
break
case 'p':
config.port = argv.p
break
case 'port':
config.port = argv.port
break
case 'host':
config['host'] = argv.host
break
case 'h':
config['host'] = argv.h
break
case 'u':
config['url'] = argv.u
break
case 'url':
config['url'] = argv.url
break
case 'a':
config['authentication'] = argv.a
break
case 'auth':
config['authentication'] = argv.auth
break
case 'f':
config['data'] = argv.f
break
case 'data':
config['data'] = argv.data
break
case 'files':
config['data'] = argv.files
break
case 'plugins':
config['plugins'] = [argv.plugins]
break
case 'd':
databaseConfig(argv.d)
break
case 'db':
databaseConfig(argv.db)
break
case 'database':
databaseConfig(argv.database)
break
case 'python':
config['python'] = argv.python
case 'login':
loginConfig()
break
case 'password':
loginConfig()
break
case 'pw':
loginConfig()
break
case '_':
break
}
}
}
function loginConfig() {
if (!config.hasOwnProperty("login")) {
config["login"] = {}
log.info(0, "Create Webview login credentials")
} else {
log.info(0, "Change Webview login credentials")
}
if (argv.hasOwnProperty("login")) {
if (argv.login == "off") {
delete config.login["user"]
delete config.login["password"]
log.info(0, "Webview login deactivated. Reactivate by setting login and password.")
return
}
config.login["user"] = argv.login
}
if (argv.hasOwnProperty("password")) {
config.login["password"] = argv.password
}
if (argv.hasOwnProperty("pw")) {
config.login["password"] = argv.pw
}
}
/**
* set url for database access (only mongodb)
* -d mongodb -m mongodb://<username>:<password>@localhost:<port>?replicaSet=adaptor-repl
* with authentification enabled use:
* -d mongodb -m mongodb://<username>:<password>@localhost:<port>/admin?replicaSet=adaptor-repl
*/
function databaseConfig(db_type) {
if (!config.hasOwnProperty("database")) {
config['database'] = { type: db_type }
} else {
config.database["type"] = db_type
}
if (argv.hasOwnProperty('m')) {
config.database['url'] = argv.m
} else if (argv.hasOwnProperty('mongodburl')) {
config.database['url'] = argv.mongodburl
} else if (argv.hasOwnProperty('mongourl')) {
config.database['url'] = argv.mongourl
}
}
file.saveJSON(config, config_file)
.then(result => {
file.mkdir(config.data)
return log.init(config.level, path.join(config.data, '/log'))
})
.then(result => {
init(config)
})
.catch(error => {
log.error("config", error)
})
/** global object that provides some useful input functions */
global.Input = {}
Input.confirmation = function (action, element, callback) {
rl.question("are you sure you want to " + action + " " + element + "? (y/n) ", answ => {
if (answ == "y" || answ == "yes") {
Promise.resolve(callback(element))
.catch(error => {
log.error(0, error)
})
}
rl.prompt()
})
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '> '
})
rl.on('line', (input) => {
if (!input) {
return
}
/*
* split input by space character but ignore spaces in quotes (" or ')
*
* regex for only double quotes: /(?:[^\s"]+|"[^"]*")+/g
* source:
* https://stackoverflow.com/questions/16261635/javascript-split-string-by-space-but-ignore-space-in-quotes-notice-not-to-spli/16261693#16261693
*
* remove quotes consequentially
*/
input = input.match(/(?:[^\s"']+|['"][^'"]*["'])+/g)
for (var i = 0; i < input.length; i++) {
input[i] = input[i].replace(/["']+/g, '')
}
switch (input[0]) {
case "quit":
quit()
break
case "games":
for (let game in games) {
log.info(0, game)
}
case "del":
case "delete":
if (input.length > 1) {
Input.confirmation("delete", input[1], deleteGame)
}
break
case "load":
if (input.length == 2) {
loadGame(input[1])
}
else if (input.length > 2) {
loadGame(input[1], { template: input[2] })
}
break
case "unload":
if (input.length > 1) { unloadGame(input[1]) }
break
case "plugins":
log.info(0, "List of available Plugins:\n")
for (let t of getAvailablePlugins()) {
log.info(0, t.name)
}
break
case "level":
if (input.length == 2) {
log.setLevel(input[1])
config['level'] = input[1]
file.saveJSON(config, config_file)
break
}
log.info(0, "wrong number of arguments for level command")
break
case "urls":
logWebhooks()
break
case "api":
logWebhooks("/api")
break
case "whitelist":
if (input.length == 3) {
if (input[1] == "add") {
if (!config['whitelist']) {
config['whitelist'] = []
}
config['whitelist'].push(input[2])
} else if (input[1] == "remove") {
if (config['whitelist'] && config['whitelist'].includes(input[2])) {
config['whitelist'] = config['whitelist'].filter(w => w !== input[2])
} else {
log.error(0, `Can not remove ${input[2]}. Not found in config.whitelist`)
break
}
}
file.saveJSON(config, config_file)
break
} else if (input.length == 2) {
if (input[1] == "any") {
delete config['whitelist']
} else if (input[1] == "none") {
config['whitelist'] = []
}
file.saveJSON(config, config_file)
break
} else if (input.length == 1) {
log.info(0, config['whitelist'])
break
}
log.info(0, "wrong number of arguments for whitelist command")
break
default:
if (games.hasOwnProperty(input[0])) {
let game = input.shift()
games[game].command(input)
} else {
if (Object.keys(games).length == 1) {
for (let game in games) {
games[game].command(input)
}
} else {
log.info("", "No such game or command '" + input[0] + "'")
}
}
break
}
rl.prompt();
}).on('close', () => {
quit()
})