From e6aaac1f512d4e0e9cec9c9ed5f7e9f8a89f11b3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 8 Dec 2015 11:07:34 -0700 Subject: [PATCH 01/19] Change 'engine' block to 'engines' --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f02cc5c1..dc5ae3ab 100644 --- a/package.json +++ b/package.json @@ -29,5 +29,7 @@ "ssh2": "^0.4.12", "string-format": "davidchambers/string-format#mini-language" }, - "engine": "node >= 0.12.2" + "engines" : { + "node" : ">=0.12.2" + } } From cdfd0740244b72fd6b18a96d17a914ccffbf411d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 8 Dec 2015 11:21:21 -0700 Subject: [PATCH 02/19] Updated quickstart to note Node version requirements/etc. and better overall docs --- docs/index.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/index.md b/docs/index.md index 7152cf1a..39767ba6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,29 +4,29 @@ ENiGMA½ is a modern from scratch BBS package written in Node.js. # Quickstart TL;DR? This should get you started... -1\. Clone +## Prerequisites +* Node.js version **v0.12.2 or higher** (v4.2+ is recommended). io.js should also work, though I have not yet tested this. +* Windows users will need additional dependencies installed for the `npm install` step in order to compile native binaries: + * A recent copy of Visual Studio (Express editions OK) + * Python 2.7.x + +## Clone ```bash git clone https://github.com/NuSkooler/enigma-bbs.git ``` -2\. Install dependencies +## Install Node Modules ```bash npm install ``` -**Note for Windows users**:
-Some dependencies require compilation. You will need at least the following installed for `npm install` to succeed: -* A recent copy of Visual Studio (Express editions OK) -* Python 2.7.x - -3\. Generate a SSH Private Key
-Note that you can skip this step and disable the SSH server in your `config.hjson` if desired. - +## Generate a SSH Private Key +To utilize the SSH server, a SSH Private Key will need generated. This step can be skipped if desired by disabling the SSH server in `config.hjson`. ```bash openssl genrsa -des3 -out ./misc/ssh_private_key.pem 2048 ``` -4\. Create a minimal config
+## Create a Minimal Config The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`. This is a [HJSON](http://hjson.org/) file (compiliant JSON is also OK). See [Configuration](config.md) for more information. ```hjson @@ -36,6 +36,7 @@ general: { servers: { ssh: { privateKeyPass: YOUR_PK_PASS + } } messages: { areas: [ @@ -44,12 +45,12 @@ messages: { } ``` -5\. Launch! +## Launch! ```bash ./main.js ``` Some points of interest: * Default ports are 8888 (Telnet) and 8889 (SSH) -* The first user you create via applying is the root SysOp. +* The first user you create via applying is the SysOp (aka root) * You may want to tail the logfile with Bunyan: `tail -F ./logs/enigma-bbs.log | ./node_modules/bunyan/bin/bunyan` \ No newline at end of file From b3c457729aba2784316e5cb2850d3134e190047c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 8 Dec 2015 11:23:42 -0700 Subject: [PATCH 03/19] * Add note about disabling SSH server in exmaple config --- docs/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.md b/docs/index.md index 39767ba6..bf3693b2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,6 +36,7 @@ general: { servers: { ssh: { privateKeyPass: YOUR_PK_PASS + enabled: true /* set to false to disable the SSH server */ } } messages: { From ce7a24fb9e61827697175bfea514652f9e9250f4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 8 Dec 2015 12:33:54 -0700 Subject: [PATCH 04/19] Yet more documentation updates --- docs/index.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 39767ba6..61bcef91 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,9 @@ ENiGMA½ is a modern from scratch BBS package written in Node.js. TL;DR? This should get you started... ## Prerequisites -* Node.js version **v0.12.2 or higher** (v4.2+ is recommended). io.js should also work, though I have not yet tested this. +* [Node.js](https://nodejs.org/) version **v0.12.2 or higher** (v4.2+ is recommended) + * [io.js](https://iojs.org/) should also work, though I have not yet tested this. + * :information_source: It is suggested to use [nvm](https://github.com/creationix/nvm) to manage your Node/io.js installs * Windows users will need additional dependencies installed for the `npm install` step in order to compile native binaries: * A recent copy of Visual Studio (Express editions OK) * Python 2.7.x @@ -50,6 +52,11 @@ messages: { ./main.js ``` +# Advanced Installation +If you've become convinced you would like a "production" BBS running ENiGMA½ a more advanced installation may be in order. + +[PM2](https://github.com/Unitech/pm2) is an excellent choice for managing your running ENiGMA½ instances. Additionally, it is suggested that you run as a specific more locked down user (e.g. 'enigma'). + Some points of interest: * Default ports are 8888 (Telnet) and 8889 (SSH) * The first user you create via applying is the SysOp (aka root) From 964b899fe5c1b2ef47bb4fe0241d413423549b13 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 10 Dec 2015 00:03:58 -0700 Subject: [PATCH 05/19] * 'validator' property --- core/view.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/view.js b/core/view.js index c8565f4e..1b164eae 100644 --- a/core/view.js +++ b/core/view.js @@ -224,6 +224,13 @@ View.prototype.setPropertyValue = function(propName, value) { break; case 'argName' : this.submitArgName = value; break; + + case 'validate' : + console.log(value) + if(_.isFunction(value)) { + this.validate = value; + } + break; } if(/styleSGR[0-9]{1,2}/.test(propName)) { @@ -269,4 +276,4 @@ View.prototype.onKeyPress = function(ch, key) { }; View.prototype.getData = function() { -}; \ No newline at end of file +}; From 743035683cfb7552fe8dc7aceecb48df80fe774f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 10 Dec 2015 00:04:38 -0700 Subject: [PATCH 06/19] * WIP on new view validation framework --- core/view_controller.js | 76 +++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/core/view_controller.js b/core/view_controller.js index c97178ae..caef9e0f 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -157,17 +157,28 @@ function ViewController(options) { case 'method' : case 'systemMethod' : - if(_.isString(propAsset.location)) { - - } else { + if('validate' === propName) { + // :TODO: handle propAsset.location for @method script specification if('systemMethod' === propAsset.type) { - // :TODO: + // :TODO: implementation validation @systemMethod handling! } else { - // local to current module - var currentModule = self.client.currentMenuModule; - if(_.isFunction(currentModule.menuMethods[propAsset.asset])) { - // :TODO: Fix formData & extraArgs... this all needs general processing - propValue = currentModule.menuMethods[propAsset.asset]({}, {});//formData, conf.extraArgs); + if(_.isFunction(self.client.currentMenuModule.menuMethods[propAsset.asset])) { + propValue = self.client.currentMenuModule.menuMethods[propAsset.asset]; + } + } + } else { + if(_.isString(propAsset.location)) { + + } else { + if('systemMethod' === propAsset.type) { + // :TODO: + } else { + // local to current module + var currentModule = self.client.currentMenuModule; + if(_.isFunction(currentModule.menuMethods[propAsset.asset])) { + // :TODO: Fix formData & extraArgs... this all needs general processing + propValue = currentModule.menuMethods[propAsset.asset]({}, {});//formData, conf.extraArgs); + } } } } @@ -362,7 +373,40 @@ ViewController.prototype.setFocus = function(focused) { }; ViewController.prototype.switchFocus = function(id) { - //this.setFocus(true); // ensure events are attached + // + // Perform focus switching validation now + // + var self = this; + var focusedView = self.focusedView; + + function performSwitch() { + self.attachClientEvents(); + + // remove from old + self.setViewFocusWithEvents(focusedView, false); + + // set to new + self.setViewFocusWithEvents(self.getView(id), true); + }; + + + if(focusedView && focusedView.validate) { + focusedView.validate(focusedView.getData(), function validated(isValid, msgProp) { + if(isValid) { + performSwitch(); + } else { + if(_.isFunction(self.client.currentMenuModule.menuMethods.validationFailure)) { + self.client.currentMenuModule.menuMethods.validationFailure(focusedView, msgProp, function validateFailNext() { + + }); + } + } + }); + } else { + performSwitch(); + } + +/* this.attachClientEvents(); // remove from old @@ -370,15 +414,19 @@ ViewController.prototype.switchFocus = function(id) { // set to new this.setViewFocusWithEvents(this.getView(id), true); + */ }; -ViewController.prototype.nextFocus = function() { +ViewController.prototype.nextFocus = function() { + var nextId; + if(!this.focusedView) { - this.switchFocus(this.views[this.firstId].id); + nextId = this.views[this.firstId].id; } else { - var nextId = this.views[this.focusedView.id].nextId; - this.switchFocus(nextId); + nextId = this.views[this.focusedView.id].nextId; } + + this.switchFocus(nextId); }; ViewController.prototype.setViewOrder = function(order) { From 295c6fad9fea0ae4e45ac72f236edaa2a297ad4f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 10 Dec 2015 16:47:37 -0700 Subject: [PATCH 07/19] * Skeleton for ArtPoolModule --- mods/art_pool.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 mods/art_pool.js diff --git a/mods/art_pool.js b/mods/art_pool.js new file mode 100644 index 00000000..8b0020fd --- /dev/null +++ b/mods/art_pool.js @@ -0,0 +1,33 @@ +/* jslint node: true */ +'use strict'; + +var MenuModule = require('../core/menu_module.js').MenuModule; + + +exports.getModule = ArtPoolModule; + +exports.moduleInfo = { + name : 'Art Pool', + desc : 'Display art from a pool of options', + author : 'NuSkooler', +}; + +function ArtPoolModule(options) { + MenuModule.call(this, options); + + var config = this.menuConfig.config; + + // + // :TODO: General idea + // * Break up some of MenuModule initSequence's calls into methods + // * initSequence here basically has general "clear", "next", etc. as per normal + // * Display art -> ooptinal pause -> display more if requested, etc. + // * Finally exit & move on as per normal + +} + +require('util').inherits(ArtPoolModule, MenuModule); + +MessageAreaModule.prototype.mciReady = function(mciData, cb) { + this.standardMCIReadyHandler(mciData, cb); +}; From b8c42810ea734e11f088d346ba7fb008f7914486 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 10 Dec 2015 21:43:36 -0700 Subject: [PATCH 08/19] Many NUA updates --- mods/nua.js | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 mods/nua.js diff --git a/mods/nua.js b/mods/nua.js new file mode 100644 index 00000000..87a60d7d --- /dev/null +++ b/mods/nua.js @@ -0,0 +1,135 @@ +/* jslint node: true */ +'use strict'; +var MenuModule = require('../core/menu_module.js').MenuModule; +var user = require('../core/user.js'); +var theme = require('../core/theme.js'); +var login = require('../core/system_menu_method.js').login; +var Config = require('../core/config.js').config; + +var async = require('async'); + +exports.getModule = NewUserAppModule; + +exports.moduleInfo = { + name : 'NUA', + desc : 'New User Application', +} + +var MciViewIds = { + userName : 1, + password : 9, + confirm : 10, + errMsg : 11, +}; + +function NewUserAppModule(options) { + MenuModule.call(this, options); + + var self = this; + + this.menuMethods = { + // + // Validation stuff + // + validatePassConfirmMatch : function(data, cb) { + var passwordView = self.viewControllers.menu.getView(MciViewIds.password); + cb(passwordView.getData() === data ? null : new Error('Passwords do not match')); + }, + + viewValidationListener : function(err, cb) { + var errMsgView = self.viewControllers.menu.getView(MciViewIds.errMsg); + var newFocusId; + if(err) { + errMsgView.setText(err.message); + err.view.clearText(); + + if(err.view.getId() === MciViewIds.confirm) { + newFocusId = MciViewIds.password; + var passwordView = self.viewControllers.menu.getView(MciViewIds.password); + passwordView.clearText(); + } + } else { + errMsgView.clearText(); + } + + cb(newFocusId); + }, + + + // + // Submit handlers + // + submitApplication : function(formData, extraArgs) { + var newUser = new user.User(); + + newUser.username = formData.value.username; + + newUser.properties = { + real_name : formData.value.realName, + birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), + sex : formData.value.sex, + location : formData.value.location, + affiliation : formData.value.affils, + email_address : formData.value.email, + web_address : formData.value.web, + account_created : new Date().toISOString(), + + message_area_name : getDefaultMessageArea().name, + + term_height : client.term.termHeight, + term_width : client.term.termWidth, + + // :TODO: This is set in User.create() -- proabbly don't need it here: + //account_status : Config.users.requireActivation ? user.User.AccountStatus.inactive : user.User.AccountStatus.active, + + // :TODO: Other defaults + // :TODO: should probably have a place to create defaults/etc. + }; + + if('*' === Config.defaults.theme) { + newUser.properties.theme_id = theme.getRandomTheme(); + } else { + newUser.properties.theme_id = Config.defaults.theme; + } + + // :TODO: .create() should also validate email uniqueness! + newUser.create( { password : formData.value.password }, function created(err) { + if(err) { + self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed'); + + self.gotoMenu(extraArgs.error, function result(err) { + if(err) { + self.prevMenu(); + } + }); + } else { + self.client.log.info( { username : formData.value.username, userId : newUser.userId }, 'New user created'); + + // Cache SysOp information now + // :TODO: Similar to bbs.js. DRY + if(newUser.isSysOp()) { + Config.general.sysOp = { + username : formData.value.username, + properties : newUser.properties, + }; + } + + if(user.User.AccountStatus.inactive === client.user.properties.account_status) { + self.gotoMenu(extraArgs.inactive); + } else { + // + // If active now, we need to call login() to authenticate + // + login(self, formData, extraArgs); + } + } + }); + }, + }; +} + +require('util').inherits(NewUserAppModule, MenuModule); + +NewUserAppModule.prototype.mciReady = function(mciData, cb) { + this.standardMCIReadyHandler(mciData, cb); +}; \ No newline at end of file From 34a8bdae21333809492974d5fd0a85768d106aa4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 10 Dec 2015 21:43:57 -0700 Subject: [PATCH 09/19] * Notes on validation framework usage --- core/fse.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/fse.js b/core/fse.js index 5902bb8d..c49f81f4 100644 --- a/core/fse.js +++ b/core/fse.js @@ -538,7 +538,8 @@ function FullScreenEditorModule(options) { this.mciReadyHandler = function(mciData, cb) { self.createInitialViews(mciData, function viewsCreated(err) { - + // :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in + // place - if this is for existing usernames else validate spec self.viewControllers.header.on('leave', function headerViewLeave(view) { if(2 === view.id) { // "to" field From 007103cbc115fde154e63a1bae28331e3e5a9f6c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 10 Dec 2015 21:44:21 -0700 Subject: [PATCH 10/19] * New validation methods --- core/system_view_validate.js | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 core/system_view_validate.js diff --git a/core/system_view_validate.js b/core/system_view_validate.js new file mode 100644 index 00000000..169f481d --- /dev/null +++ b/core/system_view_validate.js @@ -0,0 +1,56 @@ +var user = require('./user.js'); +var Config = require('./config.js').config; + + +exports.validateUserNameAvail = validateUserNameAvail; +exports.validateEmailAvail = validateEmailAvail; +exports.validateBirthdate = validateBirthdate; +exports.validatePasswordSpec = validatePasswordSpec; + +function validateUserNameAvail(data, cb) { + if(data.length < Config.users.usernameMin) { + cb(new Error('Username too short')); + } else if(data.length > Config.users.usernameMax) { + // generally should be unreached due to view restraints + cb(new Error('Username too long')); + } else { + var usernameRegExp = new RegExp(Config.users.usernamePattern); + var invalidNames = Config.users.newUserNames + Config.users.badUserNames; + + if(!usernameRegExp.test(data)) { + cb(new Error('Username contains invalid characters')); + } else if(invalidNames.indexOf(data.toLowerCase()) > -1) { + cb(new Error('Username is blacklisted')); + } else { + user.getUserIdAndName(data, function userIdAndName(err) { + if(!err) { // err is null if we succeeded -- meaning this user exists already + cb(new Error('Userame unavailable')); + } else { + cb(null); + } + }); + } + } +} + +function validateEmailAvail(data, cb) { + user.getUserIdsWithProperty('email_address', data, function userIdsWithEmail(err, uids) { + if(err) { + cb(new Error('Internal system error')); + } else if(uids.length > 0) { + cb(new Error('Email address not unique')); + } else { + cb(null); + } + }); +} + + +function validateBirthdate(data, cb) { + // :TODO: check for dates in the future, or > reasonable values + cb(isNaN(Date.parse(data)) ? new Error('Invalid birthdate') : null); +} + +function validatePasswordSpec(data, cb) { + cb((!data || data.length < Config.users.passwordMin) ? new Error('Password too short') : null); +} From 490d71c180e8e32d2b187f3b06607e45ac3b509f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 10 Dec 2015 21:44:36 -0700 Subject: [PATCH 11/19] Remove console.log() --- core/view.js | 1 - 1 file changed, 1 deletion(-) diff --git a/core/view.js b/core/view.js index 1b164eae..e5a9c502 100644 --- a/core/view.js +++ b/core/view.js @@ -226,7 +226,6 @@ View.prototype.setPropertyValue = function(propName, value) { case 'argName' : this.submitArgName = value; break; case 'validate' : - console.log(value) if(_.isFunction(value)) { this.validate = value; } From cb3ae84bc572c33c6a1eeea77906555e2f33fe1e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 10 Dec 2015 21:45:02 -0700 Subject: [PATCH 12/19] Many updates to view validation framework --- core/view_controller.js | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/core/view_controller.js b/core/view_controller.js index caef9e0f..02b13b83 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -70,8 +70,9 @@ function ViewController(options) { self.nextFocus(); break; - case 'accept' : + case 'accept' : if(self.focusedView && self.focusedView.submit) { + // :TODO: need to do validation here!!! self.submitForm(key); } else { self.nextFocus(); @@ -161,6 +162,10 @@ function ViewController(options) { // :TODO: handle propAsset.location for @method script specification if('systemMethod' === propAsset.type) { // :TODO: implementation validation @systemMethod handling! + var methodModule = require(paths.join(__dirname, 'system_view_validate.js')); + if(_.isFunction(methodModule[propAsset.asset])) { + propValue = methodModule[propAsset.asset]; + } } else { if(_.isFunction(self.client.currentMenuModule.menuMethods[propAsset.asset])) { propValue = self.client.currentMenuModule.menuMethods[propAsset.asset]; @@ -391,14 +396,28 @@ ViewController.prototype.switchFocus = function(id) { if(focusedView && focusedView.validate) { - focusedView.validate(focusedView.getData(), function validated(isValid, msgProp) { - if(isValid) { - performSwitch(); + focusedView.validate(focusedView.getData(), function validated(err) { + if(_.isFunction(self.client.currentMenuModule.menuMethods.viewValidationListener)) { + if(err) { + err.view = focusedView; + } + + self.client.currentMenuModule.menuMethods.viewValidationListener(err, function validateComplete(newFocusId) { + if(err) { + // :TODO: switchFocus() really needs a cb -- + var newFocusView; + if(newFocusId) { + newFocusView = self.getView(newFocusId) || focusedView; + } + + self.setViewFocusWithEvents(newFocusView, true); + } else { + performSwitch(); + } + }); } else { - if(_.isFunction(self.client.currentMenuModule.menuMethods.validationFailure)) { - self.client.currentMenuModule.menuMethods.validationFailure(focusedView, msgProp, function validateFailNext() { - - }); + if(!err) { + performSwitch(); } } }); From c9f287e5fd9e1a9a72be73fd32f01f723bff36ec Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 10 Dec 2015 21:46:08 -0700 Subject: [PATCH 13/19] Lots of updates including usage of validation framework and nua module --- mods/menu.hjson | 66 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/mods/menu.hjson b/mods/menu.hjson index cf58e34c..b392771f 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -77,9 +77,7 @@ art: USERLOG next: fullLoginSequenceLoginArt config: { - tooNode: { - art: TOONODE - } + tooNodeMenu: loginAttemptTooNode } form: { 0: { @@ -114,6 +112,14 @@ } } + loginAttemptTooNode: { + art: TOONODE + options: { + cls: true + nextTimeout: 2000 + } + } + logoff: { art: LOGOFF next: @systemMethod:logoff @@ -122,6 +128,7 @@ TODO: display PRINT before this (Obv/2) or NEWUSER1 (Mystic) */ newUserApplication: { + module: nua art: NUA next: [ { @@ -141,6 +148,7 @@ focus: true argName: username maxLength: @config:users.usernameMax + validate: @systemMethod:validateUserNameAvail } ET2: { argName: realName @@ -149,6 +157,7 @@ MET3: { argName: birthdate maskPattern: "####/##/##" + validate: @systemMethod:validateBirthdate } ME4: { argName: sex @@ -166,6 +175,7 @@ ET7: { argName: email maxLength: 255 + validate: @systemMethod:validateEmailAvail } ET8: { argName: web @@ -175,11 +185,13 @@ argName: password password: true maxLength: @config:users.passwordMax + validate: @systemMethod:validatePasswordSpec } ET10: { argName: passwordConfirm password: true maxLength: @config:users.passwordMax + validate: @method:validatePassConfirmMatch } TM12: { argName: submission @@ -445,7 +457,14 @@ module: last_callers art: LASTCALL options: { pause: true } - next: fullLoginSequenceSysStats + next: fullLoginSequenceWhosOnline + } + fullLoginSequenceWhosOnline: { + desc: Who's Online + module: whos_online + art: WHOSON + options: { pause: true } + next: fullLoginSequenceSysStats } fullLoginSequenceSysStats: { desc: System Stats @@ -613,27 +632,31 @@ value: { command: "2" } action: @menu:doorLORD } + { + value: { command: "4" } + action: @menu:doorTradeWars2002BBSLink + } ] } - /* - The 'abracadabra' module's config.args accepts the following format objects: - {dropFile} - Path to generated dropfile - {node} - Node number - */ + doorPimpWars: { desc: Playing PimpWars module: abracadabra config: { name: PimpWars dropFileType: DORINFO - cmd: /usr/bin/dosemu + cmd: /home/nuskooler/DOS/scripts/pimpwars.sh args: [ - "-quiet", "-f", "/home/nuskooler/DOS/X/LORD/dosemu.conf", "X:\\PW\\START.BAT {dropFile} {node}" + "{node}", + "{dropFile}", + "{srvPort}", ], nodeMax: 1 tooManyArt: DOORMANY + io: socket } - }, + } + doorLORD: { desc: Playing L.O.R.D. module: abracadabra @@ -646,6 +669,22 @@ ] } } + + // + // TradeWars 2000 example via BBSLink + // + // You will need to register with BBSLink to obtain sysCode, authCode and schemeCode + // + doorTradeWars2002BBSLink: { + desc: Playing TW 2002 (BBSLink) + module: bbs_link + config: { + sysCode: XXXXXXXX + authCode: XXXXXXXX + schemeCode: XXXXXXXX + door: tw + } + } /////////////////////////////////////////////////////////////////////// // Message Area Menu /////////////////////////////////////////////////////////////////////// @@ -818,7 +857,7 @@ } { value: { 1: 3 } - action: @menu:messageArea + action: @systemMethod:prevMenu } { value: { 1: 4 } @@ -1034,6 +1073,7 @@ argName: subject maxLength: 72 submit: true + // :TODO: Validate -> close/cancel if empty } } submit: { From 36cd4a0d30cb560e8779ebac1112c2aacb72c45b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 11 Dec 2015 10:55:23 -0700 Subject: [PATCH 14/19] Updated README --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4beca5a8..9ed90948 100644 --- a/README.md +++ b/README.md @@ -8,24 +8,24 @@ ENiGMA½ is a modern BBS software with a nostalgic flair! ## Feature Available Now * Multi platform: Anywhere Node.js runs likely works (tested under Linux and OS X) * Multi node support - * **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JS based mods + * **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based mods * MCI support for lightbars, toggles, input areas, and so on plus many other other bells and whistles - * Telnet & SSH access built in. Additional servers are easy to implement & plug in + * Telnet & **SSH** access built in. Additional servers are easy to implement & plug in * [CP437](http://www.ascii-codes.com/) and UTF-8 output - * [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior. + * [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior * [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support - * Renegade style pipe codes + * Pipe codes (ala Renegade) * [SQLite](http://sqlite.org/) storage of users and message areas - * Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password storage - * Door support including common dropfile formats and [DOSEMU](http://www.dosemu.org/) + * Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption + * Door support including common dropfile formats and legacy DOS doors (See [Doors](docs/doors.md)) * [Bunyan](https://github.com/trentm/node-bunyan) logging ## In the Works * Lots of code cleanup, ES6+ usage, and **documentation**! * FTN import & export * File areas -* Full access checking framework -* SysOp console +* Full access checking framework (ACS) +* SysOp dashboard (ye ol' WFC) * Missing functionality such as searching, pipe code support in message areas, etc. * String localization * A lot more! Feel free to request features via [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) @@ -37,9 +37,9 @@ See [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) for more ## Support * Use [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) +* **Discussion on a ENiGMA BBS!** * IRC: **#enigma-bbs** on **chat.freenode.net** * Email: bryan -at- l33t.codes -* **Discussion on a ENiGMA BBS!** * Facebook ENiGMA½ group ## Terminal Clients @@ -50,7 +50,7 @@ ENiGMA has been tested with many terminals. However, the following are suggested ## Boards * WQH: :skull: Xibalba :skull: (**telnet://xibalba.l33t.codes:44510**) -* Support board: BLACK ƒlag (**telnet://blackflag.acid.org:2425**) +* Support board: ☠ BLACK ƒlag ☠ (**telnet://blackflag.acid.org:2425**) ## Installation From bd4c50fa8da8a04ce66a46adf3e1ddb59f6962f1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 11 Dec 2015 13:57:56 -0700 Subject: [PATCH 15/19] Handle SIGINT a bit more gracefully - disconnect clients --- core/bbs.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index f02e481c..2b9d0020 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -106,13 +106,18 @@ function initialize(cb) { logger.init(); process.on('SIGINT', function onSigInt() { - // :TODO: for any client in |clientConnections|, if 'ready', send a "Server Disconnecting" + semi-gracefull hangup - // e.g. client.disconnectNow() + logger.log.info('Process interrupted, shutting down...'); - logger.log.info('Process interrupted, shutting down'); + var activeConnections = clientConns.getActiveConnections(); + var i = activeConnections.length; + while(i--) { + activeConnections[i].term.write('\n\nServer is shutting down NOW! Disconnecting...\n\n'); + clientConns.removeClient(activeConnections[i]); + } + process.exit(); }); - + // Init some extensions require('string-format').extend(String.prototype, require('./string_util.js').stringFormatExtensions); From 0d477210f9fca7f4e969b9f16b08a9abbb431858 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 11 Dec 2015 13:58:58 -0700 Subject: [PATCH 16/19] Add Config.general.menuFile: Specify name or path of menu HJSON file. Defaults to menu.hjson in mods --- core/config.js | 3 +++ core/menu_util.js | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index 2fe4cc30..d3e02122 100644 --- a/core/config.js +++ b/core/config.js @@ -85,8 +85,11 @@ function getDefaultConfig() { closedSystem : false, // is the system closed to new users? loginAttempts : 3, + + menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./mods) }, + // :TODO: see notes below about 'theme' section - move this! preLoginTheme : '*', users : { diff --git a/core/menu_util.js b/core/menu_util.js index 58bbb24f..abca4e94 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -31,7 +31,14 @@ function getMenuConfig(name, cb) { async.waterfall( [ function loadMenuJSON(callback) { - configCache.getModConfig('menu.hjson', function loaded(err, menuJson) { + var menuFilePath = Config.general.menuFile; + + // menuFile is assumed to be in 'mods' if a path is not supplied + if('.' === paths.dirname(menuFilePath)) { + menuFilePath = paths.join(__dirname, '../mods', menuFilePath); + } + + configCache.getConfig(menuFilePath, function loaded(err, menuJson) { callback(err, menuJson); }); }, From e1a1064374e806aad9c8f380953265bb104895dc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 12 Dec 2015 15:22:08 -0700 Subject: [PATCH 17/19] Add error message view --- mods/themes/luciano_blocktronics/MSGEHDR.ANS | Bin 1574 -> 1578 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/mods/themes/luciano_blocktronics/MSGEHDR.ANS b/mods/themes/luciano_blocktronics/MSGEHDR.ANS index e12ad8e1d82b117a2f514befd91b8a9d63a9dee9..2687f5ee2e515b74f61561b40513e2f658926a56 100644 GIT binary patch delta 137 zcmZ3+vx;XzrYP6#+tSenxzf?*rq0sQhStUgx%Y2RY*;f{m+`@5KBh;LKQi5$e1Z9v zps_hnk&y{Z(VhE~53r=kD}ePF=BkGH7@9#0GBk&)xjosGHII`^D%dg9+10>c@2D$fd^G>W=!^k^Xi}3-F Date: Sat, 12 Dec 2015 15:52:56 -0700 Subject: [PATCH 18/19] * Validation framework functional * Use validation for FSE, NUA, etc. * Switch to nua.js from apply.js (MenuModule + validation) --- core/fse.js | 34 +++++-- core/mask_edit_text_view.js | 5 + core/system_view_validate.js | 29 +++++- core/view_controller.js | 93 +++++++++---------- mods/menu.hjson | 16 ++++ mods/msg_area_post_fse.js | 19 ---- mods/themes/luciano_blocktronics/MSGEHDR.ANS | Bin 1578 -> 1587 bytes 7 files changed, 121 insertions(+), 75 deletions(-) diff --git a/core/fse.js b/core/fse.js index c49f81f4..ad7d6ab8 100644 --- a/core/fse.js +++ b/core/fse.js @@ -48,6 +48,8 @@ exports.moduleInfo = { TL12 - User1 TL13 - User2 + + TL16 - Error / Information area Footer - Viewing HM1 - Menu (prev/next/etc.) @@ -73,7 +75,9 @@ var MCICodeIds = { ViewCount : 8, HashTags : 9, MessageID : 10, - ReplyToMsgID : 11 + ReplyToMsgID : 11, + + ErrorMsg : 16, }, ViewModeFooter : { MsgNum : 6, @@ -540,6 +544,8 @@ function FullScreenEditorModule(options) { self.createInitialViews(mciData, function viewsCreated(err) { // :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in // place - if this is for existing usernames else validate spec + + /* self.viewControllers.header.on('leave', function headerViewLeave(view) { if(2 === view.id) { // "to" field @@ -551,7 +557,7 @@ function FullScreenEditorModule(options) { } }); } - }); + });*/ cb(err); }); @@ -762,6 +768,26 @@ function FullScreenEditorModule(options) { }; this.menuMethods = { + // + // Validation stuff + // + viewValidationListener : function(err, cb) { + var errMsgView = self.viewControllers.header.getView(MCICodeIds.ViewModeHeader.ErrorMsg); + var newFocusViewId; + if(errMsgView) { + if(err) { + errMsgView.setText(err.message); + + if(MCICodeIds.ViewModeHeader.Subject === err.view.getId()) { + // :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel) + } + } else { + errMsgView.clearText(); + } + } + cb(newFocusViewId); + }, + headerSubmit : function(formData, extraArgs) { self.switchToBody(); }, @@ -880,7 +906,3 @@ FullScreenEditorModule.prototype.mciReady = function(mciData, cb) { this.mciReadyHandler(mciData, cb); //this['mciReadyHandler' + _.capitalize(this.editorType)](mciData); }; - -FullScreenEditorModule.prototype.validateToUserName = function(un, cb) { - cb(null); // note: to be implemented by sub classes -}; diff --git a/core/mask_edit_text_view.js b/core/mask_edit_text_view.js index 36a90eb5..a25a2553 100644 --- a/core/mask_edit_text_view.js +++ b/core/mask_edit_text_view.js @@ -178,6 +178,11 @@ MaskEditTextView.prototype.setPropertyValue = function(propName, value) { MaskEditTextView.prototype.getData = function() { var rawData = MaskEditTextView.super_.prototype.getData.call(this); + + if(!rawData || 0 === rawData.length) { + return rawData; + } + var data = ''; assert(rawData.length <= this.patternArray.length); diff --git a/core/system_view_validate.js b/core/system_view_validate.js index 169f481d..5600e2b6 100644 --- a/core/system_view_validate.js +++ b/core/system_view_validate.js @@ -1,12 +1,21 @@ var user = require('./user.js'); var Config = require('./config.js').config; - +exports.validateNonEmpty = validateNonEmpty; +exports.validateMessageSubject = validateMessageSubject; exports.validateUserNameAvail = validateUserNameAvail; exports.validateEmailAvail = validateEmailAvail; exports.validateBirthdate = validateBirthdate; exports.validatePasswordSpec = validatePasswordSpec; +function validateNonEmpty(data, cb) { + cb(data && data.length > 0 ? null : new Error('Field cannot be empty')); +} + +function validateMessageSubject(data, cb) { + cb(data && data.length > 1 ? null : new Error('Subject too short')); +}; + function validateUserNameAvail(data, cb) { if(data.length < Config.users.usernameMin) { cb(new Error('Username too short')); @@ -34,6 +43,24 @@ function validateUserNameAvail(data, cb) { } function validateEmailAvail(data, cb) { + // + // This particular method allows empty data - e.g. no email entered + // + if(!data || 0 === data.length) { + return cb(null); + } + + // + // Otherwise, it must be a valid email. We'll be pretty lose here, like + // the HTML5 spec. + // + // See http://stackoverflow.com/questions/7786058/find-the-regex-used-by-html5-forms-for-validation + // + var emailRegExp = /[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; + if(!emailRegExp.test(data)) { + return cb(new Error('Invalid email address')); + } + user.getUserIdsWithProperty('email_address', data, function userIdsWithEmail(err, uids) { if(err) { cb(new Error('Internal system error')); diff --git a/core/view_controller.js b/core/view_controller.js index 02b13b83..d4dce737 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -73,7 +73,17 @@ function ViewController(options) { case 'accept' : if(self.focusedView && self.focusedView.submit) { // :TODO: need to do validation here!!! - self.submitForm(key); + var focusedView = self.focusedView; + self.validateView(focusedView, function validated(err, newFocusedViewId) { + console.log(err) + if(err) { + var newFocusedView = self.getView(newFocusedViewId) || focusedView; + self.setViewFocusWithEvents(newFocusedView, true); + } else { + self.submitForm(key); + } + }); + //self.submitForm(key); } else { self.nextFocus(); } @@ -313,6 +323,27 @@ function ViewController(options) { view.setFocus(focused); }; + + this.validateView = function(view, cb) { + if(view && _.isFunction(view.validate)) { + view.validate(view.getData(), function validateResult(err) { + var viewValidationListener = self.client.currentMenuModule.menuMethods.viewValidationListener; + if(_.isFunction(viewValidationListener)) { + if(err) { + err.view = view; // pass along the view that failed + } + + viewValidationListener(err, function validationComplete(newViewFocusId) { + cb(err, newViewFocusId); + }); + } else { + cb(err); + } + }); + } else { + cb(null); + } + }; } util.inherits(ViewController, events.EventEmitter); @@ -384,56 +415,20 @@ ViewController.prototype.switchFocus = function(id) { var self = this; var focusedView = self.focusedView; - function performSwitch() { - self.attachClientEvents(); + self.validateView(focusedView, function validated(err, newFocusedViewId) { + if(err) { + var newFocusedView = self.getView(newFocusedViewId) || focusedView; + self.setViewFocusWithEvents(newFocusedView, true); + } else { + self.attachClientEvents(); - // remove from old - self.setViewFocusWithEvents(focusedView, false); + // remove from old + self.setViewFocusWithEvents(focusedView, false); - // set to new - self.setViewFocusWithEvents(self.getView(id), true); - }; - - - if(focusedView && focusedView.validate) { - focusedView.validate(focusedView.getData(), function validated(err) { - if(_.isFunction(self.client.currentMenuModule.menuMethods.viewValidationListener)) { - if(err) { - err.view = focusedView; - } - - self.client.currentMenuModule.menuMethods.viewValidationListener(err, function validateComplete(newFocusId) { - if(err) { - // :TODO: switchFocus() really needs a cb -- - var newFocusView; - if(newFocusId) { - newFocusView = self.getView(newFocusId) || focusedView; - } - - self.setViewFocusWithEvents(newFocusView, true); - } else { - performSwitch(); - } - }); - } else { - if(!err) { - performSwitch(); - } - } - }); - } else { - performSwitch(); - } - -/* - this.attachClientEvents(); - - // remove from old - this.setViewFocusWithEvents(this.focusedView, false); - - // set to new - this.setViewFocusWithEvents(this.getView(id), true); - */ + // set to new + self.setViewFocusWithEvents(self.getView(id), true); + } + }); }; ViewController.prototype.nextFocus = function() { diff --git a/mods/menu.hjson b/mods/menu.hjson index b392771f..8d310d8d 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -153,6 +153,7 @@ ET2: { argName: realName maxLength: 32 + validate: @systemMethod:validateNonEmpty } MET3: { argName: birthdate @@ -163,10 +164,12 @@ argName: sex maskPattern: A textStyle: upper + validate: @systemMethod:validateNonEmpty } ET5: { argName: location maxLength: 32 + validate: @systemMethod:validateNonEmpty } ET6: { argName: affils @@ -239,23 +242,28 @@ focus: true argName: username maxLength: @config:users.usernameMax + validate: @systemMethod:validateUserNameAvail } ET2: { argName: realName maxLength: 32 + validate: @systemMethod:validateNonEmpty } MET3: { argName: birthdate maskPattern: "####/##/##" + validate: @systemMethod:validateBirthdate } ME4: { argName: sex maskPattern: A textStyle: upper + validate: @systemMethod:validateNonEmpty } ET5: { argName: location maxLength: 32 + validate: @systemMethod:validateNonEmpty } ET6: { argName: affils @@ -264,6 +272,7 @@ ET7: { argName: email maxLength: 255 + validate: @systemMethod:validateEmailAvail } ET8: { argName: web @@ -273,11 +282,13 @@ argName: password password: true maxLength: @config:users.passwordMax + validate: @systemMethod:validatePasswordSpec } ET10: { argName: passwordConfirm password: true maxLength: @config:users.passwordMax + validate: @method:validatePassConfirmMatch } TM12: { argName: submission @@ -362,6 +373,7 @@ maxLength: 72 submit: true text: New user feedback + validate: @systemMethod:validateMessageSubject } } submit: { @@ -921,11 +933,13 @@ ET2: { argName: to focus: true + validate: @systemMethod:validateNonEmpty } ET3: { argName: subject maxLength: 72 submit: true + validate: @systemMethod:validateNonEmpty } TL4: { // :TODO: this is for RE: line (NYI) @@ -1068,11 +1082,13 @@ argName: to focus: true text: All + validate: @systemMethod:validateNonEmpty } ET3: { argName: subject maxLength: 72 submit: true + validate: @systemMethod:validateNonEmpty // :TODO: Validate -> close/cancel if empty } } diff --git a/mods/msg_area_post_fse.js b/mods/msg_area_post_fse.js index d6034e51..f5439004 100644 --- a/mods/msg_area_post_fse.js +++ b/mods/msg_area_post_fse.js @@ -64,22 +64,3 @@ AreaPostFSEModule.prototype.enter = function(client) { AreaPostFSEModule.super_.prototype.enter.call(this, client); }; - -AreaPostFSEModule.prototype.validateToUserName = function(un, cb) { - var self = this; - - if(!un) { - cb(new Error('Username must be supplied!')); - return; - } - - if(!self.isLocalEmail()) { - cb(null); - return; - } - - user.getUserIdAndName(un, function uidAndName(err, userId, userName) { - self.toUserId = userId; - cb(err); - }); -}; \ No newline at end of file diff --git a/mods/themes/luciano_blocktronics/MSGEHDR.ANS b/mods/themes/luciano_blocktronics/MSGEHDR.ANS index 2687f5ee2e515b74f61561b40513e2f658926a56..70c36bbec8fa26db7e78beeab495d37646ce6bd7 100644 GIT binary patch delta 75 zcmZ3*vzcc?rZ_Lx?c37P2D#GF=BCcl(T3K>2D$fd^G>W=!^k^Xi}3-F Date: Sat, 12 Dec 2015 15:57:56 -0700 Subject: [PATCH 19/19] * Actually use nua.js submitApplication :) --- mods/menu.hjson | 4 ++-- mods/nua.js | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mods/menu.hjson b/mods/menu.hjson index 8d310d8d..c11e6a39 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -207,7 +207,7 @@ *: [ { value: { "submission" : 0 } - action: @method:apply/submitApplication + action: @method:submitApplication extraArgs: { inactive: userNeedsActivated error: newUserCreateError @@ -301,7 +301,7 @@ *: [ { value: { "submission" : 0 } - action: @method:apply/submitApplication + action: @method:submitApplication extraArgs: { inactive: userNeedsActivated error: newUserCreateError diff --git a/mods/nua.js b/mods/nua.js index 87a60d7d..ca33c970 100644 --- a/mods/nua.js +++ b/mods/nua.js @@ -5,6 +5,7 @@ var user = require('../core/user.js'); var theme = require('../core/theme.js'); var login = require('../core/system_menu_method.js').login; var Config = require('../core/config.js').config; +var getDefaultMessageArea = require('../core/message_area.js').getDefaultMessageArea; var async = require('async'); @@ -76,8 +77,8 @@ function NewUserAppModule(options) { message_area_name : getDefaultMessageArea().name, - term_height : client.term.termHeight, - term_width : client.term.termWidth, + term_height : self.client.term.termHeight, + term_width : self.client.term.termWidth, // :TODO: This is set in User.create() -- proabbly don't need it here: //account_status : Config.users.requireActivation ? user.User.AccountStatus.inactive : user.User.AccountStatus.active, @@ -114,7 +115,7 @@ function NewUserAppModule(options) { }; } - if(user.User.AccountStatus.inactive === client.user.properties.account_status) { + if(user.User.AccountStatus.inactive === self.client.user.properties.account_status) { self.gotoMenu(extraArgs.inactive); } else { //