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
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);
diff --git a/core/client.js b/core/client.js
index eb003cd5..25fd3874 100644
--- a/core/client.js
+++ b/core/client.js
@@ -371,7 +371,9 @@ function Client(input, output) {
}
if(key || ch) {
- self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line
+ if(Config.logging.traceUserKeyboardInput) {
+ self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line
+ }
self.lastKeyPressMs = Date.now();
@@ -415,7 +417,15 @@ Client.prototype.startIdleMonitor = function() {
};
Client.prototype.end = function () {
- this.menuStack.getCurrentModule().leave();
+ if(this.term) {
+ this.term.disconnect();
+ }
+
+ var currentModule = this.menuStack.getCurrentModule();
+
+ if(currentModule) {
+ currentModule.leave();
+ }
clearInterval(this.idleCheck);
diff --git a/core/client_term.js b/core/client_term.js
index 582f90e5..50feb4d1 100644
--- a/core/client_term.js
+++ b/core/client_term.js
@@ -132,6 +132,10 @@ function ClientTerminal(output) {
});
}
+ClientTerminal.prototype.disconnect = function() {
+ this.output = null;
+};
+
ClientTerminal.prototype.isANSI = function() {
// :TODO: Others??
return [ 'ansi', 'pc-ansi', 'qansi', 'scoansi', 'syncterm' ].indexOf(this.termType) > -1;
@@ -140,11 +144,17 @@ ClientTerminal.prototype.isANSI = function() {
// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it)
ClientTerminal.prototype.write = function(s, convertLineFeeds) {
- this.output.write(this.encode(s, convertLineFeeds));
+ this.rawWrite(this.encode(s, convertLineFeeds));
};
ClientTerminal.prototype.rawWrite = function(s) {
- this.output.write(s);
+ if(this.output) {
+ this.output.write(s, function written(err) {
+ if(err) {
+ Log.warn('Failed writing to socket: ' + err.toString());
+ }
+ });
+ }
};
ClientTerminal.prototype.pipeWrite = function(s, spec) {
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/door.js b/core/door.js
index 4b447629..57561bb4 100644
--- a/core/door.js
+++ b/core/door.js
@@ -95,7 +95,7 @@ Door.prototype.run = function() {
args[i] = self.exeInfo.args[i].format({
dropFile : self.exeInfo.dropFile,
node : self.exeInfo.node.toString(),
- inhSocket : self.exeInfo.inhSocket.toString(),
+ //inhSocket : self.exeInfo.inhSocket.toString(),
srvPort : sockServer ? sockServer.address().port.toString() : '-1',
userId : self.client.user.userId.toString(),
});
diff --git a/core/dropfile.js b/core/dropfile.js
index a84eaec3..b44d7efe 100644
--- a/core/dropfile.js
+++ b/core/dropfile.js
@@ -154,7 +154,8 @@ function DropFile(client, fileType) {
// :TODO: local/serial/telnet need to be configurable -- which also changes socket handle!
return iconv.encode([
'2', // :TODO: This needs to be configurable!
- self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows!
+ // :TODO: Completely broken right now -- This need to be configurable & come from temp socket server most likely
+ '-1', // self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows!
'57600',
Config.general.boardName,
self.client.user.userId.toString(),
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
diff --git a/core/menu_stack.js b/core/menu_stack.js
index f8bc18f6..ed855897 100644
--- a/core/menu_stack.js
+++ b/core/menu_stack.js
@@ -145,5 +145,8 @@ MenuStack.prototype.goto = function(name, options, cb) {
};
MenuStack.prototype.getCurrentModule = function() {
- return this.top().instance;
+ var top = this.top();
+ if(top) {
+ return top.instance;
+ }
};
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);
});
},
diff --git a/core/servers/ssh.js b/core/servers/ssh.js
index 44956845..58fc4e17 100644
--- a/core/servers/ssh.js
+++ b/core/servers/ssh.js
@@ -237,7 +237,7 @@ SSHServerModule.prototype.createServer = function() {
ident : 'enigma-bbs-' + enigVersion + '-srv',
// Note that sending 'banner' breaks at least EtherTerm!
debug : function debugSsh(dbgLine) {
- if(true === Config.servers.ssh.debugConnections) {
+ if(true === Config.servers.ssh.traceConnections) {
Log.trace('SSH: ' + dbgLine);
}
},
diff --git a/core/servers/telnet.js b/core/servers/telnet.js
index 44d157be..ba85599f 100644
--- a/core/servers/telnet.js
+++ b/core/servers/telnet.js
@@ -499,7 +499,7 @@ function TelnetClient(input, output) {
});
this.connectionDebug = function(info, msg) {
- if(Config.servers.telnet.debugConnections) {
+ if(Config.servers.telnet.traceConnections) {
self.log.trace(info, 'Telnet: ' + msg);
}
};
diff --git a/core/system_menu_method.js b/core/system_menu_method.js
index 86723dcc..54528c80 100644
--- a/core/system_menu_method.js
+++ b/core/system_menu_method.js
@@ -22,26 +22,8 @@ function login(callingMenu, formData, extraArgs) {
userLogin(callingMenu.client, formData.value.username, formData.value.password, function authResult(err) {
if(err) {
// login failure
- if(err.existingConn) {
- client.term.rawWrite(ansi.resetScreen());
-
- var artOpts = {
- client : client,
- font : _.has(callingMenu, 'menuConfig.config.tooNode.font') ? callingMenu.menuConfig.config.tooNode.font : null,
- name : _.has(callingMenu, 'menuConfig.config.tooNode.art') ? callingMenu.menuConfig.config.tooNode.art : 'TOONODE',
- };
-
- theme.displayThemeArt(artOpts, function artDisplayed(err) {
- if(err) {
- client.term.write('\nA user by that name is already logged in.\n');
- }
-
- setTimeout(function timeout() {
- callingMenu.prevMenu();
- }, 2000);
- });
-
- return;
+ if(err.existingConn && _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) {
+ callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu);
} else {
// Other error
callingMenu.prevMenu();
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);
+}
diff --git a/core/view.js b/core/view.js
index c8565f4e..e5a9c502 100644
--- a/core/view.js
+++ b/core/view.js
@@ -224,6 +224,12 @@ View.prototype.setPropertyValue = function(propName, value) {
break;
case 'argName' : this.submitArgName = value; break;
+
+ case 'validate' :
+ if(_.isFunction(value)) {
+ this.validate = value;
+ }
+ break;
}
if(/styleSGR[0-9]{1,2}/.test(propName)) {
@@ -269,4 +275,4 @@ View.prototype.onKeyPress = function(ch, key) {
};
View.prototype.getData = function() {
-};
\ No newline at end of file
+};
diff --git a/core/view_controller.js b/core/view_controller.js
index c97178ae..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();
@@ -157,17 +158,32 @@ 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!
+ var methodModule = require(paths.join(__dirname, 'system_view_validate.js'));
+ if(_.isFunction(methodModule[propAsset.asset])) {
+ propValue = methodModule[propAsset.asset];
+ }
} 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 +378,54 @@ 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(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
@@ -370,15 +433,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) {
diff --git a/docs/doors.md b/docs/doors.md
index 4cc4c41a..e71cf9b7 100644
--- a/docs/doors.md
+++ b/docs/doors.md
@@ -173,4 +173,17 @@ doorTradeWars2002BBSLink: {
```
-Fill in your credentials in `sysCode`, `authCode`, and `schemeCode` and that's it!
\ No newline at end of file
+Fill in your credentials in `sysCode`, `authCode`, and `schemeCode` and that's it!
+
+# Resources
+
+### DOSBox
+* Custom DOSBox builds http://home.arcor.de/h-a-l-9000/
+
+## Door Downloads & Support Sites
+### General
+* http://bbsfiles.com/
+* http://bbstorrents.bbses.info/
+
+### L.O.R.D.
+* http://lord.lordlegacy.com/
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 7152cf1a..51e85ffe 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -4,29 +4,31 @@ 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](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
+
+## 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 +38,8 @@ general: {
servers: {
ssh: {
privateKeyPass: YOUR_PK_PASS
+ enabled: true /* set to false to disable the SSH server */
+ }
}
messages: {
areas: [
@@ -44,12 +48,17 @@ messages: {
}
```
-5\. Launch!
+## Launch!
```bash
./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 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
diff --git a/mods/abracadabra.js b/mods/abracadabra.js
index 20fc1b19..5e5ff34d 100644
--- a/mods/abracadabra.js
+++ b/mods/abracadabra.js
@@ -70,6 +70,7 @@ function AbracadabraModule(options) {
this.config = options.menuConfig.config;
+ // :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts!
assert(_.isString(this.config.name, 'Config \'name\' is required'));
assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required'));
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required'));
@@ -140,6 +141,7 @@ function AbracadabraModule(options) {
],
function complete(err) {
if(err) {
+ self.client.log.warn( { error : err.toString() }, 'Could not start door');
self.lastError = err;
self.prevMenu();
} else {
@@ -158,7 +160,7 @@ function AbracadabraModule(options) {
encoding : self.config.encoding || self.client.term.outputEncoding,
dropFile : self.dropFile.fileName,
node : self.client.node,
- inhSocket : self.client.output._handle.fd,
+ //inhSocket : self.client.output._handle.fd,
};
var doorInstance = new door.Door(self.client, exeInfo);
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);
+};
diff --git a/mods/bbs_link.js b/mods/bbs_link.js
index cbbcfbd2..76bbe291 100644
--- a/mods/bbs_link.js
+++ b/mods/bbs_link.js
@@ -3,6 +3,7 @@
var MenuModule = require('../core/menu_module.js').MenuModule;
var Log = require('../core/logger.js').log;
+var resetScreen = require('../core/ansi_term.js').resetScreen;
var async = require('async');
var _ = require('lodash');
@@ -35,6 +36,7 @@ var packageJson = require('../package.json');
*/
// :TODO: BUG: When a client disconnects, it's not handled very well -- the log is spammed with tons of errors
+// :TODO: ENH: Support nodeMax and tooManyArt
exports.getModule = BBSLinkModule;
@@ -132,6 +134,9 @@ function BBSLinkModule(options) {
var clientTerminated;
+ self.client.term.write(ansi.resetScreen());
+ self.client.term.write(' Connecting to BBSLink.net, please wait...\n');
+
var bridgeConnection = net.createConnection(connectOpts, function connected() {
self.client.log.info(connectOpts, 'BBSLink bridge connection established');
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: {
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
diff --git a/mods/whos_online.js b/mods/whos_online.js
index 8891055f..d2a3f977 100644
--- a/mods/whos_online.js
+++ b/mods/whos_online.js
@@ -78,7 +78,14 @@ WhosOnlineModule.prototype.mciReady = function(mciData, cb) {
userName : oe.user.username,
realName : oe.user.properties.real_name,
timeOn : _.capitalize(moment.duration(55, 'minutes').humanize()),
- action : oe.currentMenuModule.menuConfig.desc || 'Unknown',
+ action : function getCurrentAction() {
+ var cmm = oe.currentMenuModule;
+ if(cmm) {
+ return cmm.menuConfig.desc || 'Unknown';
+ }
+ return 'Unknown';
+ //oe.currentMenuModule.menuConfig.desc || 'Unknown',
+ },
location : oe.user.properties.location,
affils : oe.user.properties.affiliation,
});
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"
+ }
}