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" + } }