From 9c2b3be0b1b7fad96b02ff31294c3ac75ba31af9 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sat, 18 May 2019 00:25:35 +0100 Subject: [PATCH 01/36] MRC WIP --- core/config.js | 69 +++++--- core/listening_server.js | 2 +- core/module_util.js | 1 + core/mrc.js | 233 ++++++++++++++++++++++++ core/servers/chat/mrc_multiplexer.js | 255 +++++++++++++++++++++++++++ 5 files changed, 530 insertions(+), 30 deletions(-) create mode 100644 core/mrc.js create mode 100644 core/servers/chat/mrc_multiplexer.js diff --git a/core/config.js b/core/config.js index 5c1f9c42..854e65c8 100644 --- a/core/config.js +++ b/core/config.js @@ -212,7 +212,8 @@ function getDefaultConfig() { badUserNames : [ 'sysop', 'admin', 'administrator', 'root', 'all', - 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix' + 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix', + 'server', 'client', 'notme' ], preAuthIdleLogoutSeconds : 60 * 3, // 3m @@ -253,6 +254,7 @@ function getDefaultConfig() { mods : paths.join(__dirname, './../mods/'), loginServers : paths.join(__dirname, './servers/login/'), contentServers : paths.join(__dirname, './servers/content/'), + chatServers : paths.join(__dirname, './servers/chat/'), scannerTossers : paths.join(__dirname, './scanner_tossers/'), mailers : paths.join(__dirname, './mailers/') , @@ -447,6 +449,15 @@ function getDefaultConfig() { } }, + chatServers : { + mrc: { + enabled : true, + multiplexerPort : 5000, + serverHostname : "mrc.bottomlessabyss.com", + serverPort : 5000 + } + }, + infoExtractUtils : { Exiftool2Desc : { cmd : `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x @@ -965,38 +976,38 @@ function getDefaultConfig() { eventScheduler : { events : { - dailyMaintenance : { - schedule : 'at 11:59pm', - action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', - }, - trimMessageAreas : { - // may optionally use [or ]@watch:/path/to/file - schedule : 'every 24 hours', + // dailyMaintenance : { + // schedule : 'at 11:59pm', + // action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', + // }, + // trimMessageAreas : { + // // may optionally use [or ]@watch:/path/to/file + // schedule : 'every 24 hours', - // action: - // - @method:path/to/module.js:theMethodName - // (path is relative to ENiGMA base dir) - // - // - @execute:/path/to/something/executable.sh - // - action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', - }, + // // action: + // // - @method:path/to/module.js:theMethodName + // // (path is relative to ENiGMA base dir) + // // + // // - @execute:/path/to/something/executable.sh + // // + // action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', + // }, - nntpMaintenance : { - schedule : 'every 12 hours', // should generally be < trimMessageAreas interval - action : '@method:core/servers/content/nntp.js:performMaintenanceTask', - }, + // nntpMaintenance : { + // schedule : 'every 12 hours', // should generally be < trimMessageAreas interval + // action : '@method:core/servers/content/nntp.js:performMaintenanceTask', + // }, - updateFileAreaStats : { - schedule : 'every 1 hours', - action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', - }, + // updateFileAreaStats : { + // schedule : 'every 1 hours', + // action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', + // }, - forgotPasswordMaintenance : { - schedule : 'every 24 hours', - action : '@method:core/web_password_reset.js:performMaintenanceTask', - args : [ '24 hours' ] // items older than this will be removed - }, + // forgotPasswordMaintenance : { + // schedule : 'every 24 hours', + // action : '@method:core/web_password_reset.js:performMaintenanceTask', + // args : [ '24 hours' ] // items older than this will be removed + // }, // // Enable the following entry in your config.hjson to periodically create/update diff --git a/core/listening_server.js b/core/listening_server.js index aa573fa1..7cb7405e 100644 --- a/core/listening_server.js +++ b/core/listening_server.js @@ -28,7 +28,7 @@ function getServer(packageName) { function startListening(cb) { const moduleUtil = require('./module_util.js'); // late load so we get Config - async.each( [ 'login', 'content' ], (category, next) => { + async.each( [ 'login', 'content', 'chat' ], (category, next) => { moduleUtil.loadModulesForCategory(`${category}Servers`, (module, nextModule) => { const moduleInst = new module.getModule(); try { diff --git a/core/module_util.js b/core/module_util.js index f61929d2..033d6094 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -117,6 +117,7 @@ function getModulePaths() { config.paths.mods, config.paths.loginServers, config.paths.contentServers, + config.paths.chatServers, config.paths.scannerTossers, ]; } diff --git a/core/mrc.js b/core/mrc.js new file mode 100644 index 00000000..b6e68463 --- /dev/null +++ b/core/mrc.js @@ -0,0 +1,233 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Log = require('./logger.js').log; +const { MenuModule } = require('./menu_module.js'); +const { Errors } = require('./enig_error.js'); +const { + pipeToAnsi, + stripMciColorCodes +} = require('./color_codes.js'); +const stringFormat = require('./string_format.js'); +const { getThemeArt } = require('./theme.js'); + + +// deps +const _ = require('lodash'); +const async = require('async'); +const net = require('net'); +const moment = require('moment'); + +exports.moduleInfo = { + name : 'MRC Client', + desc : 'Connects to an MRC chat server', + author : 'RiPuk', + packageName : 'codes.l33t.enigma.mrc.client', +}; + +const FormIds = { + mrcChat : 0, +}; + +var MciViewIds = { + mrcChat : { + chatLog : 1, + inputArea : 2, + roomName : 3, + roomTopic : 4, + + customRangeStart : 10, // 10+ = customs + } +}; + +const state = { + socket: '', + alias: '', + room: '', + room_topic: '', + nicks: [], + last_ping: 0 +}; + + +exports.getModule = class mrcModule extends MenuModule { + constructor(options) { + super(options); + + this.log = Log.child( { module : 'MRC' } ); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + state.alias = this.client.user.username; + + + this.menuMethods = { + sendChatMessage : (formData, extraArgs, cb) => { + // const message = _.get(formData.value, 'inputArea', '').trim(); + + const inputAreaView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.inputArea); + const inputData = inputAreaView.getData(); + const textFormatObj = { + fromUserName : state.alias, + message : inputData + }; + + const messageFormat = + this.config.messageFormat || + '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; + + try { + sendChat(stringFormat(messageFormat, textFormatObj)); + } catch(e) { + self.client.log.warn( { error : e.message }, 'MRC error'); + } + inputAreaView.clearText(); + + return cb(null); + + } + } + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + async.series( + [ + // (callback) => { + // console.log("stop idle monitor") + // this.client.stopIdleMonitor(); + // return(callback); + // }, + (callback) => { + return this.prepViewController('mrcChat', FormIds.mrcChat, mciData.menu, callback); + }, + (callback) => { + return this.validateMCIByViewIds('mrcChat', [ MciViewIds.mrcChat.chatLog, MciViewIds.mrcChat.inputArea ], callback); + }, + (callback) => { + const connectOpts = { + port : 5000, + host : "localhost", + }; + + // connect to multiplexer + state.socket = net.createConnection(connectOpts, () => { + // handshake with multiplexer + state.socket.write(`--DUDE-ITS--|${state.alias}\n`); + + + sendClientConnect() + + // send register to central MRC every 60s + setInterval(function () { + sendHeartbeat(state.socket) + }, 60000); + }); + + // when we get data, process it + state.socket.on('data', data => { + data = data.toString(); + this.processReceivedMessage(data); + this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); + }); + + return(callback); + } + ], + err => { + return cb(err); + } + ); + }); + } + + processReceivedMessage(blob) { + blob.split('\n').forEach( message => { + + try { + message = JSON.parse(message) + } catch (e) { + return + } + + if (message.from_user == 'SERVER') { + const params = message.body.split(':'); + + switch (params[0]) { + case 'BANNER': + const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + chatMessageView.addText(pipeToAnsi(params[1].replace(/^\s+/, ''))); + chatMessageView.redraw(); + + case 'ROOMTOPIC': + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomName).setText(params[1]); + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(params[2]); + + case 'USERLIST': + state.nicks = params[1].split(','); + + break; + } + + } else { + // if we're here then we want to show it to the user + const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); + chatMessageView.addText(pipeToAnsi("|08" + currentTime + "|00 " + message.body)); + chatMessageView.redraw(); + } + + return; + + }); + } + +}; + + +function sendMessage(to_user, to_site, to_room, body) { + // drop message if user just mashes enter + if (body == '' || body == state.alias) return; + + // otherwise construct message + const message = { + from_room: state.room, + to_user: to_user, + to_site: to_site, + to_room: to_room, + body: body + } + Log.debug({module: 'mrcclient', message: message}, 'Sending message to MRC multiplexer'); + // TODO: check socket still exists here + state.socket.write(JSON.stringify(message) + '\n'); +} + +function sendChat(message,to_user) { + sendMessage(to_user || '', '', state.room, message) +} + +function sendServerCommand(command, to_site) { + Log.debug({ module: 'mrc', command: command }, 'Sending server command'); + sendMessage('SERVER', to_site || '', state.room, command); + return; +} + +function sendHeartbeat() { + sendServerCommand('IAMHERE'); + return; +} + +function sendClientConnect() { + sendHeartbeat(); + joinRoom('lobby'); + sendServerCommand('BANNERS'); + sendServerCommand('MOTD'); + return; +} + +function joinRoom(room) { + sendServerCommand(`NEWROOM:${state.room}:${room}`); +} diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js new file mode 100644 index 00000000..5419088a --- /dev/null +++ b/core/servers/chat/mrc_multiplexer.js @@ -0,0 +1,255 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Log = require('../../logger.js').log; +const { ServerModule } = require('../../server_module.js'); +const Config = require('../../config.js').get; +const { Errors } = require('../../enig_error.js'); +const { wordWrapText } = require('../../word_wrap.js'); +const { stripMciColorCodes } = require('../../color_codes.js'); + +// deps +const net = require('net'); +const _ = require('lodash'); +const os = require('os'); + +// MRC +const PROTOCOL_VERSION = '1.2.9'; + +const ModuleInfo = exports.moduleInfo = { + name : 'MRC', + desc : 'An MRC Chat Multiplexer', + author : 'RiPuk', + packageName : 'codes.l33t.enigma.mrc.server', + notes : 'https://bbswiki.bottomlessabyss.net/index.php?title=MRC_Chat_platform', +}; + +const connectedSockets = new Set(); +let mrcCentralConnection = ''; + +exports.getModule = class MrcModule extends ServerModule { + constructor() { + super(); + + this.log = Log.child( { server : 'MRC' } ); + + } + + createServer(cb) { + if (!this.enabled) { + return cb(null); + } + + const config = Config(); + const boardName = config.general.boardName + const enigmaVersion = "ENiGMA-BBS_" + require('../../../package.json').version + + const mrcConnectOpts = { + port : 5000, + host : "mrc.bottomlessabyss.net" + }; + + const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${PROTOCOL_VERSION}` + this.log.debug({ handshake : handshake }, "Handshaking with MRC server") + + // create connection to MRC server + this.mrcClient = net.createConnection(mrcConnectOpts, () => { + this.mrcClient.write(handshake); + this.log.info(mrcConnectOpts, 'Connected to MRC server'); + mrcCentralConnection = this.mrcClient + }); + + // do things when we get data from MRC central + this.mrcClient.on('data', (data) => { + // split on \n to deal with getting messages in batches + data.toString().split('\n').forEach( item => { + if (item == '') return; + + this.log.debug( { data : item } , `Received data`); + let message = this.parseMessage(item); + this.log.debug(message, `Parsed data`); + + this.receiveFromMRC(this.mrcClient, message); + }); + }); + + this.mrcClient.on('end', () => { + this.log.info(mrcConnectOpts, 'Disconnected from MRC server'); + }); + + this.mrcClient.on('error', err => { + Log.info( { error : err.message }, 'MRC server error'); + }); + + // start a local server for clients to connect to + this.server = net.createServer( function(socket) { + socket.setEncoding('ascii'); + connectedSockets.add(socket); + + socket.on('data', data => { + // split on \n to deal with getting messages in batches + data.toString().split('\n').forEach( item => { + if (item == '') return; + + // save username with socket + if(item.startsWith('--DUDE-ITS--')) { + socket.username = item.split('|')[1]; + Log.debug( { server : 'MRC', user: socket.username } , `User connected`); + } + else { + receiveFromClient(socket.username, item); + } + }); + + }); + + socket.on('end', function() { + connectedSockets.delete(socket); + }); + + socket.on('error', err => { + if('ECONNRESET' !== err.code) { // normal + console.log(err.message); + } + }); + }); + + + return cb(null); + } + + listen(cb) { + if (!this.enabled) { + return cb(null); + } + + const config = Config(); + + const port = parseInt(config.chatServers.mrc.multiplexerPort); + if(isNaN(port)) { + this.log.warn( { port : config.chatServers.mrc.multiplexerPort, server : ModuleInfo.name }, 'Invalid port' ); + return cb(Errors.Invalid(`Invalid port: ${config.chatServers.mrc.multiplexerPort}`)); + } + Log.info( { server : ModuleInfo.name, port : config.chatServers.mrc.multiplexerPort }, 'MRC multiplexer local listener starting up'); + return this.server.listen(port, cb); + } + + get enabled() { + return _.get(Config(), 'chatServers.mrc.enabled', false) && this.isConfigured(); + } + + isConfigured() { + const config = Config(); + return _.isNumber(_.get(config, 'chatServers.mrc.multiplexerPort')); + } + + sendToClient(message, username) { + connectedSockets.forEach( (client) => { + this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user') + client.write(JSON.stringify(message) + '\n'); + }); + } + + receiveFromMRC(socket, message) { + + const config = Config(); + const siteName = slugify(config.general.boardName) + + if (message.from_user == 'SERVER' && message.body == 'HELLO') { + // initial server hello, can ignore + return; + + } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { + // reply to heartbeat + // this.log.debug('Respond to heartbeat'); + let message = sendToMrcServer(socket, 'CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); + return message; + + } else { + // if not a heartbeat, and we have clients then we need to send something to them + //console.log(this.connectedSockets); + this.sendToClient(message); + return; + } + } + + // split raw data received into an object we can work with + parseMessage(line) { + const msg = line.split('~'); + if (msg.length < 7) { + return; + } + + return { + from_user: msg[0], + from_site: msg[1], + from_room: msg[2], + to_user: msg[3], + to_site: msg[4], + to_room: msg[5], + body: msg[6] + }; + } + +}; + + +// User / site name must be ASCII 33-125, no MCI, 30 chars max, underscores +function sanitiseName(str) { + return str.replace( + /\s/g, '_' + ).replace( + /[^\x21-\x7D]|(\|\w\w)/g, '' // Non-printable & MCI + ).substr( + 0, 30 + ); +} + +function sanitiseRoomName(message) { + return message.replace(/[^\x21-\x7D]|(\|\w\w)/g, '').substr(0, 30); +} + +function sanitiseMessage(message) { + return message.replace(/[^\x20-\x7D]/g, ''); +} + +function receiveFromClient(username, message) { + try { + message = JSON.parse(message) + message.from_user = username + } catch (e) { + Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); + } + + sendToMrcServer(mrcCentralConnection, message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body) +} + +// send a message back to the mrc central server +function sendToMrcServer(socket, fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { + const config = Config(); + const siteName = slugify(config.general.boardName) + + const line = [ + fromUser, + siteName, + sanitiseRoomName(fromRoom), + sanitiseName(toUser || ''), + sanitiseName(toSite || ''), + sanitiseRoomName(toRoom || ''), + sanitiseMessage(messageBody) + ].join('~') + '~'; + + Log.debug({ server : 'MRC', data : line }, 'Sending data'); + return socket.write(line + '\n'); +} + +function slugify(text) +{ + return text.toString() + .replace(/\s+/g, '_') // Replace spaces with - + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '_') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text +} \ No newline at end of file From 67ecad4e1a746fccf475559af4937207ee4d2bec Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 19 May 2019 00:01:58 +0100 Subject: [PATCH 02/36] Implemented most MRC server calls --- art/themes/luciano_blocktronics/mrc.ans | Bin 0 -> 881 bytes core/mrc.js | 191 +++++++++++++++++++----- core/servers/chat/mrc_multiplexer.js | 5 +- 3 files changed, 159 insertions(+), 37 deletions(-) create mode 100644 art/themes/luciano_blocktronics/mrc.ans diff --git a/art/themes/luciano_blocktronics/mrc.ans b/art/themes/luciano_blocktronics/mrc.ans new file mode 100644 index 0000000000000000000000000000000000000000..a1bbd030f30251860bafab4e8e98625373ec9843 GIT binary patch literal 881 zcmb_a!AiqG5KZwSxq1P%#5zp^ZjMTwdPc zx9b!({Js8ILtFahVtZ{j?95op7$M}Ytv4-f4`(Uu02COIm&>;PNfvwZfeKkG=Qwdg zZ=bu>)+vI3g7Nk*=98DAWDGj-UyHLyFu}#?iRX<*Pu{s_*V*)b7QQ}A=&yf&1A$QO A@Bjb+ literal 0 HcmV?d00001 diff --git a/core/mrc.js b/core/mrc.js index b6e68463..681944ab 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -6,10 +6,10 @@ const Log = require('./logger.js').log; const { MenuModule } = require('./menu_module.js'); const { Errors } = require('./enig_error.js'); const { - pipeToAnsi, - stripMciColorCodes + pipeToAnsi } = require('./color_codes.js'); const stringFormat = require('./string_format.js'); +const StringUtil = require('./string_util.js') const { getThemeArt } = require('./theme.js'); @@ -24,8 +24,13 @@ exports.moduleInfo = { desc : 'Connects to an MRC chat server', author : 'RiPuk', packageName : 'codes.l33t.enigma.mrc.client', + + // Whilst this module was put together by me (RiPuk), it should be noted that a lot of the ideas (and even some code snippets) were + // borrowed from the Synchronet implementation of MRC by echicken. So...thanks, your code was very helpful in putting this together. + // Source at http://cvs.synchro.net/cgi-bin/viewcvs.cgi/xtrn/mrc/. }; + const FormIds = { mrcChat : 0, }; @@ -36,7 +41,8 @@ var MciViewIds = { inputArea : 2, roomName : 3, roomTopic : 4, - + mrcUsers : 5, + mrcBbses : 6, customRangeStart : 10, // 10+ = customs } }; @@ -61,29 +67,31 @@ exports.getModule = class mrcModule extends MenuModule { this.menuMethods = { + sendChatMessage : (formData, extraArgs, cb) => { - // const message = _.get(formData.value, 'inputArea', '').trim(); const inputAreaView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.inputArea); const inputData = inputAreaView.getData(); - const textFormatObj = { - fromUserName : state.alias, - message : inputData - }; - - const messageFormat = - this.config.messageFormat || - '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; - - try { - sendChat(stringFormat(messageFormat, textFormatObj)); - } catch(e) { - self.client.log.warn( { error : e.message }, 'MRC error'); - } + + this.processSentMessage(inputData); inputAreaView.clearText(); return cb(null); - + }, + + movementKeyPressed : (formData, extraArgs, cb) => { + const bodyView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); // :TODO: use const here vs magic # + console.log("got arrow key"); + switch(formData.key.name) { + case 'down arrow' : bodyView.scrollDocumentUp(); break; + case 'up arrow' : bodyView.scrollDocumentDown(); break; + case 'page up' : bodyView.keyPressPageUp(); break; + case 'page down' : bodyView.keyPressPageDown(); break; + } + + this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); + + return cb(null); } } } @@ -117,13 +125,13 @@ exports.getModule = class mrcModule extends MenuModule { state.socket = net.createConnection(connectOpts, () => { // handshake with multiplexer state.socket.write(`--DUDE-ITS--|${state.alias}\n`); - sendClientConnect() - // send register to central MRC every 60s + // send register to central MRC and get stats every 60s setInterval(function () { sendHeartbeat(state.socket) + sendServerCommand('STATS') }, 60000); }); @@ -131,7 +139,6 @@ exports.getModule = class mrcModule extends MenuModule { state.socket.on('data', data => { data = data.toString(); this.processReceivedMessage(data); - this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); }); return(callback); @@ -153,37 +160,146 @@ exports.getModule = class mrcModule extends MenuModule { return } + const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + if (message.from_user == 'SERVER') { const params = message.body.split(':'); switch (params[0]) { case 'BANNER': - const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); chatMessageView.addText(pipeToAnsi(params[1].replace(/^\s+/, ''))); chatMessageView.redraw(); + break; case 'ROOMTOPIC': - this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomName).setText(params[1]); - this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(params[2]); - + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomName).setText(`#${params[1]}`); + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(pipeToAnsi(params[2])); + state.room = params[1] + break; + case 'USERLIST': state.nicks = params[1].split(','); + break; + + case 'STATS': + const stats = params[1].split(' '); + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcUsers).setText(stats[2]); + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcBbses).setText(stats[0]); - break; + break; + + default: + chatMessageView.addText(pipeToAnsi(message.body)); + break; } } else { - // if we're here then we want to show it to the user - const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); - const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); - chatMessageView.addText(pipeToAnsi("|08" + currentTime + "|00 " + message.body)); - chatMessageView.redraw(); + if (message.from_user == state.alias && message.to_user == "NOTME") { + // don't deliver NOTME messages + return; + } else { + // if we're here then we want to show it to the user + const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); + chatMessageView.addText(pipeToAnsi("|08" + currentTime + "|00 " + message.body + "|00")); + chatMessageView.redraw(); + } } + this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); return; - }); } + + processSentMessage(message) { + + if (message.startsWith('/')) { + const cmd = message.split(' '); + cmd[0] = cmd[0].substr(1).toLowerCase(); + + switch (cmd[0]) { + case 'rainbow': + const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { + var cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); + a += `|${cc}${c}|00 ` + return a; + }, '').substr(0, 140).replace(/\\s\|\d*$/, ''); + + this.processSentMessage(line) + break; + + case 'l33t': + this.processSentMessage(StringUtil.stylizeString(message.substr(5), 'l33t')); + break; + + case 'kewl': + const text_modes = Array('f','v','V','i','M'); + const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; + this.processSentMessage(StringUtil.stylizeString(message.substr(5), mode)); + break; + + case 'whoon': + sendServerCommand('WHOON'); + break; + + case 'motd': + sendServerCommand('MOTD'); + break; + + case 'meetups': + sendServerCommand('MEETUPS'); + break; + + case 'bbses': + sendServerCommand('CONNECTED'); + break; + + case 'topic': + sendServerCommand(`NEWTOPIC:${state.room}:${message.substr(7)}`) + break; + + case 'join': + joinRoom(cmd[1]); + break; + + case 'chatters': + sendServerCommand('CHATTERS'); + break; + + case 'rooms': + sendServerCommand('LIST'); + break; + + case 'clear': + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog) + chatLogView.setText(''); + sendServerCommand('STATS'); + // chatLogView.redraw(); + break; + + default: + break; + } + + } else { + // just format and send + const textFormatObj = { + fromUserName : state.alias, + message : message + }; + + const messageFormat = + this.config.messageFormat || + '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; + + try { + sendChat(stringFormat(messageFormat, textFormatObj)); + } catch(e) { + self.client.log.warn( { error : e.message }, 'MRC error'); + } + } + + return; + } }; @@ -205,7 +321,7 @@ function sendMessage(to_user, to_site, to_room, body) { state.socket.write(JSON.stringify(message) + '\n'); } -function sendChat(message,to_user) { +function sendChat(message, to_user) { sendMessage(to_user || '', '', state.room, message) } @@ -222,12 +338,15 @@ function sendHeartbeat() { function sendClientConnect() { sendHeartbeat(); - joinRoom('lobby'); - sendServerCommand('BANNERS'); sendServerCommand('MOTD'); + sendServerCommand('STATS'); + joinRoom('lobby'); return; } function joinRoom(room) { + // room names are displayed with a # but referred to without. confusing. + room = room.replace(/^#/, ''); sendServerCommand(`NEWROOM:${state.room}:${room}`); + sendServerCommand('USERLIST') } diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 5419088a..87f78a33 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -46,7 +46,7 @@ exports.getModule = class MrcModule extends ServerModule { const enigmaVersion = "ENiGMA-BBS_" + require('../../../package.json').version const mrcConnectOpts = { - port : 5000, + port : 50000, host : "mrc.bottomlessabyss.net" }; @@ -65,6 +65,9 @@ exports.getModule = class MrcModule extends ServerModule { // split on \n to deal with getting messages in batches data.toString().split('\n').forEach( item => { if (item == '') return; + console.log('start') + console.log(item) + console.log('end') this.log.debug( { data : item } , `Received data`); let message = this.parseMessage(item); From 92528fc16f73c5b407791fdbf2d74241a44a151b Mon Sep 17 00:00:00 2001 From: David Stephens Date: Mon, 20 May 2019 23:37:32 +0100 Subject: [PATCH 03/36] MRC bug squashing --- core/mrc.js | 346 +++++++++++++++------------ core/servers/chat/mrc_multiplexer.js | 29 +-- 2 files changed, 207 insertions(+), 168 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 681944ab..7cf2661d 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -10,8 +10,6 @@ const { } = require('./color_codes.js'); const stringFormat = require('./string_format.js'); const StringUtil = require('./string_util.js') -const { getThemeArt } = require('./theme.js'); - // deps const _ = require('lodash'); @@ -30,7 +28,6 @@ exports.moduleInfo = { // Source at http://cvs.synchro.net/cgi-bin/viewcvs.cgi/xtrn/mrc/. }; - const FormIds = { mrcChat : 0, }; @@ -47,15 +44,28 @@ var MciViewIds = { } }; -const state = { - socket: '', - alias: '', - room: '', - room_topic: '', - nicks: [], - last_ping: 0 -}; + +// TODO: this is a bit shit, could maybe do it with an ansi instead +const helpText = ` +General Chat: +/rooms - List of current rooms +/join - Join a room +/pm - Send a private message +/clear - Clear the chat log +---- +/whoon - Who's on what BBS +/chatters - Who's in what room +/topic - Set the topic +/meetups - MRC MeetUps +/bbses - BBS's connected +/info - Info about specific BBS +--- +/l33t - l337 5p34k +/kewl - BBS KeWL SPeaK +/rainbow - Crazy rainbow text +`; + exports.getModule = class mrcModule extends MenuModule { constructor(options) { @@ -63,9 +73,16 @@ exports.getModule = class mrcModule extends MenuModule { this.log = Log.child( { module : 'MRC' } ); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); - state.alias = this.client.user.username; - - + + this.state = { + socket: '', + alias: this.client.user.username, + room: '', + room_topic: '', + nicks: [], + last_ping: 0 + }; + this.menuMethods = { sendChatMessage : (formData, extraArgs, cb) => { @@ -81,7 +98,6 @@ exports.getModule = class mrcModule extends MenuModule { movementKeyPressed : (formData, extraArgs, cb) => { const bodyView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); // :TODO: use const here vs magic # - console.log("got arrow key"); switch(formData.key.name) { case 'down arrow' : bodyView.scrollDocumentUp(); break; case 'up arrow' : bodyView.scrollDocumentDown(); break; @@ -103,12 +119,7 @@ exports.getModule = class mrcModule extends MenuModule { } async.series( - [ - // (callback) => { - // console.log("stop idle monitor") - // this.client.stopIdleMonitor(); - // return(callback); - // }, + [ (callback) => { return this.prepViewController('mrcChat', FormIds.mrcChat, mciData.menu, callback); }, @@ -122,21 +133,22 @@ exports.getModule = class mrcModule extends MenuModule { }; // connect to multiplexer - state.socket = net.createConnection(connectOpts, () => { + this.state.socket = net.createConnection(connectOpts, () => { + const self = this; // handshake with multiplexer - state.socket.write(`--DUDE-ITS--|${state.alias}\n`); + self.state.socket.write(`--DUDE-ITS--|${self.state.alias}\n`); - sendClientConnect() + self.clientConnect(); // send register to central MRC and get stats every 60s - setInterval(function () { - sendHeartbeat(state.socket) - sendServerCommand('STATS') - }, 60000); + setInterval(function () { + self.sendHeartbeat(); + self.sendServerCommand('STATS') + }, 60000); }); // when we get data, process it - state.socket.on('data', data => { + this.state.socket.on('data', data => { data = data.toString(); this.processReceivedMessage(data); }); @@ -162,29 +174,31 @@ exports.getModule = class mrcModule extends MenuModule { const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + if (message.from_user == 'SERVER') { const params = message.body.split(':'); switch (params[0]) { case 'BANNER': chatMessageView.addText(pipeToAnsi(params[1].replace(/^\s+/, ''))); - chatMessageView.redraw(); break; case 'ROOMTOPIC': this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomName).setText(`#${params[1]}`); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(pipeToAnsi(params[2])); - state.room = params[1] + this.state.room = params[1]; break; case 'USERLIST': - state.nicks = params[1].split(','); + this.state.nicks = params[1].split(','); break; case 'STATS': + console.log("got stats back") const stats = params[1].split(' '); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcUsers).setText(stats[2]); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcBbses).setText(stats[0]); + this.state.last_ping = stats[1]; break; @@ -194,159 +208,183 @@ exports.getModule = class mrcModule extends MenuModule { } } else { - if (message.from_user == state.alias && message.to_user == "NOTME") { + if (message.from_user == this.state.alias && message.to_user == "NOTME") { // don't deliver NOTME messages return; } else { // if we're here then we want to show it to the user const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); chatMessageView.addText(pipeToAnsi("|08" + currentTime + "|00 " + message.body + "|00")); - chatMessageView.redraw(); } } this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); - return; }); } - processSentMessage(message) { - + processSentMessage(message, to_user) { if (message.startsWith('/')) { - const cmd = message.split(' '); - cmd[0] = cmd[0].substr(1).toLowerCase(); - switch (cmd[0]) { - case 'rainbow': - const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { - var cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); - a += `|${cc}${c}|00 ` - return a; - }, '').substr(0, 140).replace(/\\s\|\d*$/, ''); - - this.processSentMessage(line) - break; - - case 'l33t': - this.processSentMessage(StringUtil.stylizeString(message.substr(5), 'l33t')); - break; - - case 'kewl': - const text_modes = Array('f','v','V','i','M'); - const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; - this.processSentMessage(StringUtil.stylizeString(message.substr(5), mode)); - break; - - case 'whoon': - sendServerCommand('WHOON'); - break; - - case 'motd': - sendServerCommand('MOTD'); - break; - - case 'meetups': - sendServerCommand('MEETUPS'); - break; - - case 'bbses': - sendServerCommand('CONNECTED'); - break; - - case 'topic': - sendServerCommand(`NEWTOPIC:${state.room}:${message.substr(7)}`) - break; - - case 'join': - joinRoom(cmd[1]); - break; - - case 'chatters': - sendServerCommand('CHATTERS'); - break; - - case 'rooms': - sendServerCommand('LIST'); - break; - - case 'clear': - const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog) - chatLogView.setText(''); - sendServerCommand('STATS'); - // chatLogView.redraw(); - break; - - default: - break; - } + this.processSlashCommand(message) } else { + if (message == '') { + this.sendServerCommand('STATS'); + return; + } + // just format and send const textFormatObj = { - fromUserName : state.alias, + fromUserName : this.state.alias, message : message }; - + const messageFormat = this.config.messageFormat || '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; - + try { - sendChat(stringFormat(messageFormat, textFormatObj)); + this.sendChat(stringFormat(messageFormat, textFormatObj), to_user || ''); } catch(e) { - self.client.log.warn( { error : e.message }, 'MRC error'); + this.client.log.warn( { error : e.message }, 'MRC error'); } } - - return; + + } + + processSlashCommand(message) { + // get the chat log view in case we need it + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog) + + const cmd = message.split(' '); + cmd[0] = cmd[0].substr(1).toLowerCase(); + + switch (cmd[0]) { + case 'pm': + this.processSentMessage(cmd[2], cmd[1]) + break; + case 'rainbow': + // this is brutal, but i love it + const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { + var cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); + a += `|${cc}${c}|00 `; + return a; + }, '').substr(0, 140).replace(/\\s\|\d*$/, ''); + + this.processSentMessage(line); + break; + + case 'l33t': + this.processSentMessage(StringUtil.stylizeString(message.substr(5), 'l33t')); + break; + + case 'kewl': + const text_modes = Array('f','v','V','i','M'); + const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; + this.processSentMessage(StringUtil.stylizeString(message.substr(5), mode)); + break; + + case 'whoon': + this.sendServerCommand('WHOON'); + break; + + case 'motd': + this.sendServerCommand('MOTD'); + break; + + case 'meetups': + this.sendServerCommand('MEETUPS'); + break; + + case 'bbses': + this.sendServerCommand('CONNECTED'); + break; + + case 'topic': + this.sendServerCommand(`NEWTOPIC:${this.state.room}:${message.substr(7)}`) + break; + + case 'info': + this.sendServerCommand(`INFO ${cmd[1]}`); + break; + + case 'join': + this.joinRoom(cmd[1]); + break; + + case 'chatters': + this.sendServerCommand('CHATTERS'); + break; + + case 'rooms': + this.sendServerCommand('LIST'); + break; + + case 'clear': + chatLogView.setText(''); + break; + + case '?': + chatLogView.addText(helpText); + break; + + default: + + break; + } + + // just do something to get the cursor back to the right place ¯\_(ツ)_/¯ + this.sendServerCommand('STATS'); + + }; + + sendMessage(to_user, to_site, to_room, body) { + + const message = { + from_user: this.state.alias, + from_room: this.state.room, + to_user: to_user, + to_site: to_site, + to_room: to_room, + body: body + }; + + this.log.debug({ message: message }, 'Sending message to MRC multiplexer'); + // TODO: check socket still exists here + + this.state.socket.write(JSON.stringify(message) + '\n'); + }; + + sendServerCommand(command, to_site) { + Log.debug({ module: 'mrc', command: command }, 'Sending server command'); + this.sendMessage('SERVER', to_site || '', this.state.room, command); + }; + + + sendHeartbeat() { + this.sendServerCommand('IAMHERE'); + } + + joinRoom(room) { + // room names are displayed with a # but referred to without. confusing. + room = room.replace(/^#/, ''); + this.state.room = room; + this.sendServerCommand(`NEWROOM:${this.state.room}:${room}`); + this.sendServerCommand('USERLIST') + } + + clientConnect() { + this.sendServerCommand('MOTD'); + this.joinRoom('lobby'); + this.sendServerCommand('STATS'); + this.sendHeartbeat(); + } + + sendChat(message, to_user) { + this.sendMessage(to_user || '', '', this.state.room, message) } - }; -function sendMessage(to_user, to_site, to_room, body) { - // drop message if user just mashes enter - if (body == '' || body == state.alias) return; - - // otherwise construct message - const message = { - from_room: state.room, - to_user: to_user, - to_site: to_site, - to_room: to_room, - body: body - } - Log.debug({module: 'mrcclient', message: message}, 'Sending message to MRC multiplexer'); - // TODO: check socket still exists here - state.socket.write(JSON.stringify(message) + '\n'); -} -function sendChat(message, to_user) { - sendMessage(to_user || '', '', state.room, message) -} -function sendServerCommand(command, to_site) { - Log.debug({ module: 'mrc', command: command }, 'Sending server command'); - sendMessage('SERVER', to_site || '', state.room, command); - return; -} - -function sendHeartbeat() { - sendServerCommand('IAMHERE'); - return; -} - -function sendClientConnect() { - sendHeartbeat(); - sendServerCommand('MOTD'); - sendServerCommand('STATS'); - joinRoom('lobby'); - return; -} - -function joinRoom(room) { - // room names are displayed with a # but referred to without. confusing. - room = room.replace(/^#/, ''); - sendServerCommand(`NEWROOM:${state.room}:${room}`); - sendServerCommand('USERLIST') -} diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 87f78a33..ef0138f8 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -14,6 +14,7 @@ const net = require('net'); const _ = require('lodash'); const os = require('os'); + // MRC const PROTOCOL_VERSION = '1.2.9'; @@ -65,15 +66,13 @@ exports.getModule = class MrcModule extends ServerModule { // split on \n to deal with getting messages in batches data.toString().split('\n').forEach( item => { if (item == '') return; - console.log('start') - console.log(item) - console.log('end') this.log.debug( { data : item } , `Received data`); let message = this.parseMessage(item); this.log.debug(message, `Parsed data`); - - this.receiveFromMRC(this.mrcClient, message); + if (message) { + this.receiveFromMRC(this.mrcClient, message); + } }); }); @@ -82,13 +81,12 @@ exports.getModule = class MrcModule extends ServerModule { }); this.mrcClient.on('error', err => { - Log.info( { error : err.message }, 'MRC server error'); + this.log.info( { error : err.message }, 'MRC server error'); }); // start a local server for clients to connect to this.server = net.createServer( function(socket) { socket.setEncoding('ascii'); - connectedSockets.add(socket); socket.on('data', data => { // split on \n to deal with getting messages in batches @@ -96,7 +94,8 @@ exports.getModule = class MrcModule extends ServerModule { if (item == '') return; // save username with socket - if(item.startsWith('--DUDE-ITS--')) { + if(item.startsWith('--DUDE-ITS--')) { + connectedSockets.add(socket); socket.username = item.split('|')[1]; Log.debug( { server : 'MRC', user: socket.username } , `User connected`); } @@ -147,10 +146,12 @@ exports.getModule = class MrcModule extends ServerModule { return _.isNumber(_.get(config, 'chatServers.mrc.multiplexerPort')); } - sendToClient(message, username) { + sendToClient(message) { connectedSockets.forEach( (client) => { - this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user') - client.write(JSON.stringify(message) + '\n'); + if (message.to_user == '' || message.to_user == client.username || message.to_user == 'CLIENT') { + // this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user') + client.write(JSON.stringify(message) + '\n'); + } }); } @@ -172,7 +173,8 @@ exports.getModule = class MrcModule extends ServerModule { } else { // if not a heartbeat, and we have clients then we need to send something to them //console.log(this.connectedSockets); - this.sendToClient(message); + this.sendToClient(message); + return; } } @@ -220,7 +222,6 @@ function sanitiseMessage(message) { function receiveFromClient(username, message) { try { message = JSON.parse(message) - message.from_user = username } catch (e) { Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); } @@ -250,7 +251,7 @@ function sendToMrcServer(socket, fromUser, fromRoom, toUser, toSite, toRoom, mes function slugify(text) { return text.toString() - .replace(/\s+/g, '_') // Replace spaces with - + .replace(/\s+/g, '_') // Replace spaces with _ .replace(/[^\w\-]+/g, '') // Remove all non-word chars .replace(/\-\-+/g, '_') // Replace multiple - with single - .replace(/^-+/, '') // Trim - from start of text From 9f4f1fca13203fbd7cf46f36d47ba929b7c21588 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Wed, 22 May 2019 23:43:41 +0100 Subject: [PATCH 04/36] Refactor and rename of MRC client and multiplexer --- core/mrc.js | 162 ++++++++++++++++----------- core/servers/chat/mrc_multiplexer.js | 119 +++++++++----------- 2 files changed, 152 insertions(+), 129 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 7cf2661d..5320c999 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -4,12 +4,11 @@ // ENiGMA½ const Log = require('./logger.js').log; const { MenuModule } = require('./menu_module.js'); -const { Errors } = require('./enig_error.js'); const { pipeToAnsi } = require('./color_codes.js'); const stringFormat = require('./string_format.js'); -const StringUtil = require('./string_util.js') +const StringUtil = require('./string_util.js'); // deps const _ = require('lodash'); @@ -23,7 +22,7 @@ exports.moduleInfo = { author : 'RiPuk', packageName : 'codes.l33t.enigma.mrc.client', - // Whilst this module was put together by me (RiPuk), it should be noted that a lot of the ideas (and even some code snippets) were + // Whilst this module was put together by me (RiPuk), it should be noted that a lot of the ideas (and even some code snippets) were // borrowed from the Synchronet implementation of MRC by echicken. So...thanks, your code was very helpful in putting this together. // Source at http://cvs.synchro.net/cgi-bin/viewcvs.cgi/xtrn/mrc/. }; @@ -45,7 +44,7 @@ var MciViewIds = { }; - + // TODO: this is a bit shit, could maybe do it with an ansi instead const helpText = ` General Chat: @@ -73,7 +72,7 @@ exports.getModule = class mrcModule extends MenuModule { this.log = Log.child( { module : 'MRC' } ); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); - + this.state = { socket: '', alias: this.client.user.username, @@ -82,17 +81,17 @@ exports.getModule = class mrcModule extends MenuModule { nicks: [], last_ping: 0 }; - + this.menuMethods = { sendChatMessage : (formData, extraArgs, cb) => { - + const inputAreaView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.inputArea); const inputData = inputAreaView.getData(); - this.processSentMessage(inputData); + this.processOutgoingMessage(inputData); inputAreaView.clearText(); - + return cb(null); }, @@ -109,7 +108,7 @@ exports.getModule = class mrcModule extends MenuModule { return cb(null); } - } + }; } mciReady(mciData, cb) { @@ -129,7 +128,7 @@ exports.getModule = class mrcModule extends MenuModule { (callback) => { const connectOpts = { port : 5000, - host : "localhost", + host : 'localhost', }; // connect to multiplexer @@ -143,7 +142,7 @@ exports.getModule = class mrcModule extends MenuModule { // send register to central MRC and get stats every 60s setInterval(function () { self.sendHeartbeat(); - self.sendServerCommand('STATS') + self.sendServerMessage('STATS'); }, 60000); }); @@ -159,28 +158,36 @@ exports.getModule = class mrcModule extends MenuModule { err => { return cb(err); } - ); + ); }); } + /** + * Adds a message to the chat log on screen + */ + addMessageToChatLog(message) { + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + chatLogView.addText(pipeToAnsi(message)); + } + + /** + * Processes data received back from the MRC multiplexer + */ processReceivedMessage(blob) { blob.split('\n').forEach( message => { try { - message = JSON.parse(message) + message = JSON.parse(message); } catch (e) { - return + return; } - const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); - - if (message.from_user == 'SERVER') { const params = message.body.split(':'); switch (params[0]) { case 'BANNER': - chatMessageView.addText(pipeToAnsi(params[1].replace(/^\s+/, ''))); + this.addMessageToChatLog(params[1].replace(/^\s+/, '')); break; case 'ROOMTOPIC': @@ -188,13 +195,12 @@ exports.getModule = class mrcModule extends MenuModule { this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(pipeToAnsi(params[2])); this.state.room = params[1]; break; - + case 'USERLIST': this.state.nicks = params[1].split(','); break; - + case 'STATS': - console.log("got stats back") const stats = params[1].split(' '); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcUsers).setText(stats[2]); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcBbses).setText(stats[0]); @@ -203,18 +209,25 @@ exports.getModule = class mrcModule extends MenuModule { break; default: - chatMessageView.addText(pipeToAnsi(message.body)); + this.addMessageToChatLog(message.body); break; } } else { - if (message.from_user == this.state.alias && message.to_user == "NOTME") { + if (message.from_user == this.state.alias && message.to_user == 'NOTME') { // don't deliver NOTME messages return; } else { // if we're here then we want to show it to the user const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); - chatMessageView.addText(pipeToAnsi("|08" + currentTime + "|00 " + message.body + "|00")); + + if (message.to_user == this.state.alias) { + // it's a pm + this.addMessageToChatLog('|08' + currentTime + ' |14PM|00 ' + message.body + '|00'); + } else { + // it's not a pm + this.addMessageToChatLog('|08' + currentTime + '|00 ' + message.body + '|00'); + } } } @@ -222,14 +235,17 @@ exports.getModule = class mrcModule extends MenuModule { }); } - processSentMessage(message, to_user) { + /** + * Receives the message input from the user and does something with it based on what it is + */ + processOutgoingMessage(message, to_user) { if (message.startsWith('/')) { - this.processSlashCommand(message) + this.processSlashCommand(message); } else { if (message == '') { - this.sendServerCommand('STATS'); + this.sendServerMessage('STATS'); return; } @@ -244,7 +260,8 @@ exports.getModule = class mrcModule extends MenuModule { '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; try { - this.sendChat(stringFormat(messageFormat, textFormatObj), to_user || ''); + const formattedMessage = stringFormat(messageFormat, textFormatObj); + this.sendMessageToMultiplexer(to_user || '', '', this.state.room, formattedMessage); } catch(e) { this.client.log.warn( { error : e.message }, 'MRC error'); } @@ -252,60 +269,63 @@ exports.getModule = class mrcModule extends MenuModule { } + /** + * Processes a message that begins with a slash + */ processSlashCommand(message) { // get the chat log view in case we need it - const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog) + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); const cmd = message.split(' '); cmd[0] = cmd[0].substr(1).toLowerCase(); switch (cmd[0]) { case 'pm': - this.processSentMessage(cmd[2], cmd[1]) + this.processOutgoingMessage(cmd[2], cmd[1]); break; case 'rainbow': // this is brutal, but i love it const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { - var cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); + const cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); a += `|${cc}${c}|00 `; return a; }, '').substr(0, 140).replace(/\\s\|\d*$/, ''); - this.processSentMessage(line); + this.processOutgoingMessage(line); break; case 'l33t': - this.processSentMessage(StringUtil.stylizeString(message.substr(5), 'l33t')); + this.processOutgoingMessage(StringUtil.stylizeString(message.substr(5), 'l33t')); break; case 'kewl': const text_modes = Array('f','v','V','i','M'); const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; - this.processSentMessage(StringUtil.stylizeString(message.substr(5), mode)); + this.processOutgoingMessage(StringUtil.stylizeString(message.substr(5), mode)); break; case 'whoon': - this.sendServerCommand('WHOON'); + this.sendServerMessage('WHOON'); break; case 'motd': - this.sendServerCommand('MOTD'); + this.sendServerMessage('MOTD'); break; case 'meetups': - this.sendServerCommand('MEETUPS'); + this.sendServerMessage('MEETUPS'); break; case 'bbses': - this.sendServerCommand('CONNECTED'); + this.sendServerMessage('CONNECTED'); break; case 'topic': - this.sendServerCommand(`NEWTOPIC:${this.state.room}:${message.substr(7)}`) + this.sendServerMessage(`NEWTOPIC:${this.state.room}:${message.substr(7)}`); break; case 'info': - this.sendServerCommand(`INFO ${cmd[1]}`); + this.sendServerMessage(`INFO ${cmd[1]}`); break; case 'join': @@ -313,11 +333,11 @@ exports.getModule = class mrcModule extends MenuModule { break; case 'chatters': - this.sendServerCommand('CHATTERS'); + this.sendServerMessage('CHATTERS'); break; case 'rooms': - this.sendServerCommand('LIST'); + this.sendServerMessage('LIST'); break; case 'clear': @@ -325,20 +345,23 @@ exports.getModule = class mrcModule extends MenuModule { break; case '?': - chatLogView.addText(helpText); + this.addMessageToChatLog(helpText); break; default: - + break; } // just do something to get the cursor back to the right place ¯\_(ツ)_/¯ - this.sendServerCommand('STATS'); + this.sendServerMessage('STATS'); - }; + } - sendMessage(to_user, to_site, to_room, body) { + /** + * Creates a json object, stringifies it and sends it to the MRC multiplexer + */ + sendMessageToMultiplexer(to_user, to_site, to_room, body) { const message = { from_user: this.state.alias, @@ -353,36 +376,43 @@ exports.getModule = class mrcModule extends MenuModule { // TODO: check socket still exists here this.state.socket.write(JSON.stringify(message) + '\n'); - }; - - sendServerCommand(command, to_site) { - Log.debug({ module: 'mrc', command: command }, 'Sending server command'); - this.sendMessage('SERVER', to_site || '', this.state.room, command); - }; - - - sendHeartbeat() { - this.sendServerCommand('IAMHERE'); } + /** + * Sends an MRC 'server' message + */ + sendServerMessage(command, to_site) { + Log.debug({ module: 'mrc', command: command }, 'Sending server command'); + this.sendMessageToMultiplexer('SERVER', to_site || '', this.state.room, command); + } + + /** + * Sends a heartbeat to the MRC server + */ + sendHeartbeat() { + this.sendServerMessage('IAMHERE'); + } + + /** + * Joins a room, unsurprisingly + */ joinRoom(room) { // room names are displayed with a # but referred to without. confusing. room = room.replace(/^#/, ''); this.state.room = room; - this.sendServerCommand(`NEWROOM:${this.state.room}:${room}`); - this.sendServerCommand('USERLIST') + this.sendServerMessage(`NEWROOM:${this.state.room}:${room}`); + this.sendServerMessage('USERLIST'); } + /** + * Things that happen when a local user connects to the MRC multiplexer + */ clientConnect() { - this.sendServerCommand('MOTD'); + this.sendServerMessage('MOTD'); this.joinRoom('lobby'); - this.sendServerCommand('STATS'); + this.sendServerMessage('STATS'); this.sendHeartbeat(); } - - sendChat(message, to_user) { - this.sendMessage(to_user || '', '', this.state.room, message) - } }; diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index ef0138f8..9410a237 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -6,8 +6,6 @@ const Log = require('../../logger.js').log; const { ServerModule } = require('../../server_module.js'); const Config = require('../../config.js').get; const { Errors } = require('../../enig_error.js'); -const { wordWrapText } = require('../../word_wrap.js'); -const { stripMciColorCodes } = require('../../color_codes.js'); // deps const net = require('net'); @@ -41,24 +39,26 @@ exports.getModule = class MrcModule extends ServerModule { if (!this.enabled) { return cb(null); } - + + const self = this; + const config = Config(); - const boardName = config.general.boardName - const enigmaVersion = "ENiGMA-BBS_" + require('../../../package.json').version + const boardName = config.general.boardName; + const enigmaVersion = 'ENiGMA-BBS_' + require('../../../package.json').version; const mrcConnectOpts = { port : 50000, - host : "mrc.bottomlessabyss.net" + host : 'mrc.bottomlessabyss.net' }; - const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${PROTOCOL_VERSION}` - this.log.debug({ handshake : handshake }, "Handshaking with MRC server") + const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${PROTOCOL_VERSION}`; + this.log.debug({ handshake : handshake }, 'Handshaking with MRC server'); // create connection to MRC server this.mrcClient = net.createConnection(mrcConnectOpts, () => { this.mrcClient.write(handshake); this.log.info(mrcConnectOpts, 'Connected to MRC server'); - mrcCentralConnection = this.mrcClient + mrcCentralConnection = this.mrcClient; }); // do things when we get data from MRC central @@ -66,10 +66,10 @@ exports.getModule = class MrcModule extends ServerModule { // split on \n to deal with getting messages in batches data.toString().split('\n').forEach( item => { if (item == '') return; - - this.log.debug( { data : item } , `Received data`); + + this.log.debug( { data : item } , 'Received data'); let message = this.parseMessage(item); - this.log.debug(message, `Parsed data`); + this.log.debug(message, 'Parsed data'); if (message) { this.receiveFromMRC(this.mrcClient, message); } @@ -87,20 +87,20 @@ exports.getModule = class MrcModule extends ServerModule { // start a local server for clients to connect to this.server = net.createServer( function(socket) { socket.setEncoding('ascii'); - + socket.on('data', data => { // split on \n to deal with getting messages in batches data.toString().split('\n').forEach( item => { + if (item == '') return; // save username with socket if(item.startsWith('--DUDE-ITS--')) { connectedSockets.add(socket); socket.username = item.split('|')[1]; - Log.debug( { server : 'MRC', user: socket.username } , `User connected`); - } - else { - receiveFromClient(socket.username, item); + Log.debug( { server : 'MRC', user: socket.username } , 'User connected'); + } else { + self.receiveFromClient(socket.username, item); } }); @@ -112,7 +112,7 @@ exports.getModule = class MrcModule extends ServerModule { socket.on('error', err => { if('ECONNRESET' !== err.code) { // normal - console.log(err.message); + this.log.error( { error: err.message }, 'MRC error' ); } }); }); @@ -156,26 +156,21 @@ exports.getModule = class MrcModule extends ServerModule { } receiveFromMRC(socket, message) { - + const config = Config(); - const siteName = slugify(config.general.boardName) + const siteName = slugify(config.general.boardName); if (message.from_user == 'SERVER' && message.body == 'HELLO') { // initial server hello, can ignore - return; } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { // reply to heartbeat // this.log.debug('Respond to heartbeat'); - let message = sendToMrcServer(socket, 'CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); - return message; + this.sendToMrcServer(socket, 'CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); } else { // if not a heartbeat, and we have clients then we need to send something to them - //console.log(this.connectedSockets); this.sendToClient(message); - - return; } } @@ -197,6 +192,34 @@ exports.getModule = class MrcModule extends ServerModule { }; } + receiveFromClient(username, message) { + try { + message = JSON.parse(message); + } catch (e) { + Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); + } + + this.sendToMrcServer(mrcCentralConnection, message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body); + } + + // send a message back to the mrc central server + sendToMrcServer(socket, fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { + const config = Config(); + const siteName = slugify(config.general.boardName); + + const line = [ + fromUser, + siteName, + sanitiseRoomName(fromRoom), + sanitiseName(toUser || ''), + sanitiseName(toSite || ''), + sanitiseRoomName(toRoom || ''), + sanitiseMessage(messageBody) + ].join('~') + '~'; + + Log.debug({ server : 'MRC', data : line }, 'Sending data'); + return socket.write(line + '\n'); + } }; @@ -219,41 +242,11 @@ function sanitiseMessage(message) { return message.replace(/[^\x20-\x7D]/g, ''); } -function receiveFromClient(username, message) { - try { - message = JSON.parse(message) - } catch (e) { - Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); - } - - sendToMrcServer(mrcCentralConnection, message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body) -} - -// send a message back to the mrc central server -function sendToMrcServer(socket, fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { - const config = Config(); - const siteName = slugify(config.general.boardName) - - const line = [ - fromUser, - siteName, - sanitiseRoomName(fromRoom), - sanitiseName(toUser || ''), - sanitiseName(toSite || ''), - sanitiseRoomName(toRoom || ''), - sanitiseMessage(messageBody) - ].join('~') + '~'; - - Log.debug({ server : 'MRC', data : line }, 'Sending data'); - return socket.write(line + '\n'); -} - -function slugify(text) -{ - return text.toString() - .replace(/\s+/g, '_') // Replace spaces with _ - .replace(/[^\w\-]+/g, '') // Remove all non-word chars - .replace(/\-\-+/g, '_') // Replace multiple - with single - - .replace(/^-+/, '') // Trim - from start of text - .replace(/-+$/, ''); // Trim - from end of text +function slugify(text) { + return text.toString() + .replace(/\s+/g, '_') // Replace spaces with _ + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '_') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text } \ No newline at end of file From 109b157c02846fe57cbed752389ad5baa4a21423 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 May 2019 00:14:36 +0100 Subject: [PATCH 05/36] more MRC bug squashing and tidy up --- core/mrc.js | 84 +++++++++++++++++----------- core/servers/chat/mrc_multiplexer.js | 55 +++++++++++------- 2 files changed, 88 insertions(+), 51 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 5320c999..54aa70f7 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -5,16 +5,17 @@ const Log = require('./logger.js').log; const { MenuModule } = require('./menu_module.js'); const { - pipeToAnsi + pipeToAnsi, + stripMciColorCodes } = require('./color_codes.js'); -const stringFormat = require('./string_format.js'); -const StringUtil = require('./string_util.js'); +const stringFormat = require('./string_format.js'); +const StringUtil = require('./string_util.js'); // deps const _ = require('lodash'); const async = require('async'); -const net = require('net'); -const moment = require('moment'); +const net = require('net'); +const moment = require('moment'); exports.moduleInfo = { name : 'MRC Client', @@ -107,6 +108,12 @@ exports.getModule = class mrcModule extends MenuModule { this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); return cb(null); + }, + + quit : (formData, extraArgs, cb) => { + this.sendServerMessage('LOGOFF'); + this.state.socket.destroy(); + return this.prevMenu(cb); } }; } @@ -167,12 +174,23 @@ exports.getModule = class mrcModule extends MenuModule { */ addMessageToChatLog(message) { const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); - chatLogView.addText(pipeToAnsi(message)); + const messageLength = stripMciColorCodes(message).length; + const chatWidth = chatLogView.dimens.width; + let padAmount = 0; + + if (messageLength > chatWidth) { + padAmount = chatWidth - (messageLength % chatWidth); + } else { + padAmount = chatWidth - messageLength; + } + + const padding = ' |00' + ' '.repeat(padAmount - 2); + chatLogView.addText(pipeToAnsi(message + padding)); } /** - * Processes data received back from the MRC multiplexer - */ + * Processes data received from the MRC multiplexer + */ processReceivedMessage(blob) { blob.split('\n').forEach( message => { @@ -200,13 +218,13 @@ exports.getModule = class mrcModule extends MenuModule { this.state.nicks = params[1].split(','); break; - case 'STATS': + case 'STATS': { const stats = params[1].split(' '); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcUsers).setText(stats[2]); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcBbses).setText(stats[0]); this.state.last_ping = stats[1]; - break; + } default: this.addMessageToChatLog(message.body); @@ -214,20 +232,10 @@ exports.getModule = class mrcModule extends MenuModule { } } else { - if (message.from_user == this.state.alias && message.to_user == 'NOTME') { - // don't deliver NOTME messages - return; - } else { + if (message.to_room == this.state.room) { // if we're here then we want to show it to the user const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); - - if (message.to_user == this.state.alias) { - // it's a pm - this.addMessageToChatLog('|08' + currentTime + ' |14PM|00 ' + message.body + '|00'); - } else { - // it's not a pm - this.addMessageToChatLog('|08' + currentTime + '|00 ' + message.body + '|00'); - } + this.addMessageToChatLog('|08' + currentTime + '|00 ' + message.body + '|00'); } } @@ -240,27 +248,39 @@ exports.getModule = class mrcModule extends MenuModule { */ processOutgoingMessage(message, to_user) { if (message.startsWith('/')) { - this.processSlashCommand(message); - } else { if (message == '') { + // don't do anything if message is blank, just update stats this.sendServerMessage('STATS'); return; } - // just format and send + // else just format and send const textFormatObj = { fromUserName : this.state.alias, + toUserName : to_user, message : message }; const messageFormat = this.config.messageFormat || '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; + + const privateMessageFormat = + this.config.outgoingPrivateMessageFormat || + '|00|10<|02{fromUserName}|10|14->|02{toUserName}>|00 |03{message}|00'; + let formattedMessage = ''; + if (to_user == undefined) { + // normal message + formattedMessage = stringFormat(messageFormat, textFormatObj); + } else { + // pm + formattedMessage = stringFormat(privateMessageFormat, textFormatObj); + } + try { - const formattedMessage = stringFormat(messageFormat, textFormatObj); this.sendMessageToMultiplexer(to_user || '', '', this.state.room, formattedMessage); } catch(e) { this.client.log.warn( { error : e.message }, 'MRC error'); @@ -283,7 +303,7 @@ exports.getModule = class mrcModule extends MenuModule { case 'pm': this.processOutgoingMessage(cmd[2], cmd[1]); break; - case 'rainbow': + case 'rainbow': { // this is brutal, but i love it const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { const cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); @@ -293,17 +313,17 @@ exports.getModule = class mrcModule extends MenuModule { this.processOutgoingMessage(line); break; - + } case 'l33t': - this.processOutgoingMessage(StringUtil.stylizeString(message.substr(5), 'l33t')); + this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), 'l33t')); break; - case 'kewl': + case 'kewl': { const text_modes = Array('f','v','V','i','M'); const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; - this.processOutgoingMessage(StringUtil.stylizeString(message.substr(5), mode)); + this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), mode)); break; - + } case 'whoon': this.sendServerMessage('WHOON'); break; diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 9410a237..7eaac22f 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -2,10 +2,12 @@ 'use strict'; // ENiGMA½ -const Log = require('../../logger.js').log; -const { ServerModule } = require('../../server_module.js'); -const Config = require('../../config.js').get; -const { Errors } = require('../../enig_error.js'); +const Log = require('../../logger.js').log; +const { ServerModule } = require('../../server_module.js'); +const Config = require('../../config.js').get; +const { Errors } = require('../../enig_error.js'); +const SysProps = require('../../system_property.js'); +const StatLog = require('../../stat_log.js'); // deps const net = require('net'); @@ -30,9 +32,7 @@ let mrcCentralConnection = ''; exports.getModule = class MrcModule extends ServerModule { constructor() { super(); - this.log = Log.child( { server : 'MRC' } ); - } createServer(cb) { @@ -43,12 +43,12 @@ exports.getModule = class MrcModule extends ServerModule { const self = this; const config = Config(); - const boardName = config.general.boardName; + const boardName = config.general.prettyBoardName || config.general.boardName; const enigmaVersion = 'ENiGMA-BBS_' + require('../../../package.json').version; const mrcConnectOpts = { - port : 50000, - host : 'mrc.bottomlessabyss.net' + host : config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', + port : config.chatServers.mrc.serverPort || 5000 }; const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${PROTOCOL_VERSION}`; @@ -91,7 +91,6 @@ exports.getModule = class MrcModule extends ServerModule { socket.on('data', data => { // split on \n to deal with getting messages in batches data.toString().split('\n').forEach( item => { - if (item == '') return; // save username with socket @@ -103,7 +102,6 @@ exports.getModule = class MrcModule extends ServerModule { self.receiveFromClient(socket.username, item); } }); - }); socket.on('end', function() { @@ -117,7 +115,6 @@ exports.getModule = class MrcModule extends ServerModule { }); }); - return cb(null); } @@ -146,22 +143,31 @@ exports.getModule = class MrcModule extends ServerModule { return _.isNumber(_.get(config, 'chatServers.mrc.multiplexerPort')); } + /** + * Sends received messages to local clients + */ sendToClient(message) { connectedSockets.forEach( (client) => { - if (message.to_user == '' || message.to_user == client.username || message.to_user == 'CLIENT') { - // this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user') + if (message.to_user == '' || message.to_user == client.username || message.to_user == 'CLIENT' || message.from_user == client.username || message.to_user == 'NOTME' ) { + this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user'); client.write(JSON.stringify(message) + '\n'); + } else { + this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Not forwarding message'); } }); } + /** + * Processes messages received // split raw data received into an object we can work withfrom the central MRC server + */ receiveFromMRC(socket, message) { const config = Config(); const siteName = slugify(config.general.boardName); if (message.from_user == 'SERVER' && message.body == 'HELLO') { - // initial server hello, can ignore + // reply with extra bbs info + this.sendToMrcServer(socket, 'CLIENT', '', 'SERVER', 'ALL', '', `INFOSYS:${StatLog.getSystemStat(SysProps.SysOpUsername)}`); } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { // reply to heartbeat @@ -174,7 +180,9 @@ exports.getModule = class MrcModule extends ServerModule { } } - // split raw data received into an object we can work with + /** + * Takes an MRC message and parses it into something usable + */ parseMessage(line) { const msg = line.split('~'); if (msg.length < 7) { @@ -192,6 +200,9 @@ exports.getModule = class MrcModule extends ServerModule { }; } + /** + * Receives a message from a local client and sanity checks before sending on to the central MRC server + */ receiveFromClient(username, message) { try { message = JSON.parse(message); @@ -202,7 +213,9 @@ exports.getModule = class MrcModule extends ServerModule { this.sendToMrcServer(mrcCentralConnection, message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body); } - // send a message back to the mrc central server + /** + * Converts a message back into the MRC format and sends it to the central MRC server + */ sendToMrcServer(socket, fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { const config = Config(); const siteName = slugify(config.general.boardName); @@ -222,8 +235,9 @@ exports.getModule = class MrcModule extends ServerModule { } }; - -// User / site name must be ASCII 33-125, no MCI, 30 chars max, underscores +/** + * User / site name must be ASCII 33-125, no MCI, 30 chars max, underscores + */ function sanitiseName(str) { return str.replace( /\s/g, '_' @@ -242,6 +256,9 @@ function sanitiseMessage(message) { return message.replace(/[^\x20-\x7D]/g, ''); } +/** + * SLugifies the BBS name for use as an MRC "site name" + */ function slugify(text) { return text.toString() .replace(/\s+/g, '_') // Replace spaces with _ From b30d0e189d64fe1f9f804eb1df367ebabb9832fd Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 May 2019 00:15:34 +0100 Subject: [PATCH 06/36] Add MRC config to luciano_blocktronics theme --- art/themes/luciano_blocktronics/theme.hjson | 33 +++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 998d4c34..49d459f6 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -1078,14 +1078,37 @@ } } - - //////////////////////////////// ERC /////////////////////////////// - - ercClient: { + mrc: { config: { - //chatEntryFormat: "|00|08[|03{bbsTag}|08] |10{userName}|08: |02{message}" + messageFormat: "|00|10<|02{fromUserName}|10>|00 |03{message}|00" + privateMessageFormat: "|00|10<|02{fromUserName}|15->{toUserName}|10>|00 |03{message}|00" + } + 0: { + mci: { + MT1: { + width: 72 + height: 18 + } + ET2: { + width: 69 // fnarr! + maxLength: 140 + } + TL3: { + width: 20 + } + TL4: { + width: 20 + } + TL5: { + width: 2 + } + TL6: { + width: 2 + } + } } } + } prompts: { From 0b9599755485534cfbacb77b4a0ecb8c5b0a6993 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 May 2019 00:16:49 +0100 Subject: [PATCH 07/36] MRC general config --- core/config.js | 12 ++++++++-- misc/menu_template.in.hjson | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/core/config.js b/core/config.js index 854e65c8..ea309fc6 100644 --- a/core/config.js +++ b/core/config.js @@ -452,9 +452,17 @@ function getDefaultConfig() { chatServers : { mrc: { enabled : true, + serverHostname : 'mrc.bottomlessabyss.net', + serverPort : 5000, multiplexerPort : 5000, - serverHostname : "mrc.bottomlessabyss.com", - serverPort : 5000 + bbsInfo : { + sysop : '', + telnet : '', + website : '', + ssh : '', + description : '', + + } } }, diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 9aac86f3..9ae54d17 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -1066,6 +1066,10 @@ value: { command: "UA" } action: @menu:mainMenuUserAchievementsEarned } + { + value: { command: "MRC" } + action: @menu:mrc + } { value: 1 action: @menu:mainMenu @@ -1094,6 +1098,48 @@ } } + mrc: { + desc: MRC Chat + module: mrc + art: MRC + config: { + cls: true + } + form: { + 0: { + mci: { + MT1: { + mode: preview + autoScroll: true + } + ET2: { + argName: inputArea + submit: true + focus: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + { + keys: [ "down arrow", "up arrow", "page up", "page down" ] + action: @method:movementKeyPressed + } + ] + submit: { + *: [ + { + value: { inputArea: null } + action: @method:sendChatMessage + } + ] + } + } + } + } + nodeMessage: { desc: Node Messaging module: node_msg From 27e3d50c5d9edc8e57dd0ce60e53aa665951331f Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 May 2019 23:32:22 +0100 Subject: [PATCH 08/36] Fix mrc debug menu config --- config/menu.hjson | 4237 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 4237 insertions(+) create mode 100644 config/menu.hjson diff --git a/config/menu.hjson b/config/menu.hjson new file mode 100644 index 00000000..99585ade --- /dev/null +++ b/config/menu.hjson @@ -0,0 +1,4237 @@ +{ + /* + ./\/\.' ENiGMA½ Menu Configuration -/--/-------- - -- - + + _____________________ _____ ____________________ __________\_ / + \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! + // __|___// | \// |// | \// | | \// \ /___ /_____ + /____ _____| __________ ___|__| ____| \ / _____ \ + ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ + /__ _\ + <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ + + *-----------------------------------------------------------------------------* + + General Information + ------------------------------- - - + This configuration is in HJSON (http://hjson.org/) format. Strict to-spec + JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. + + See http://hjson.org/ for more information and syntax. + + Various editors and IDEs such as Sublime Text 3, Visual Studio Code, and so + on have syntax highlighting for the HJSON format which are highly recommended. + + ------------------------------- -- - - + Menu Configuration + ------------------------------- - - + ENiGMA½ makes no assumptions about specific menu types (main, doors, etc.), + but instead allows full customization of all menus throughout the system. + Some menus such as a main menu are considered "standard" while others are + backed by a specific module. SysOps can tweak various settings about these + modules (look & feel, keyboard interation, and so on) or even fully replace + the module with something else. + + This file starts out as an example setup. Look at the examples, change + settings, menu ordering/flow, add/remove menus, implement ACS control, + etc.! + + Remember you can *live edit* this file. That is, make a change and save + while you're logged into the system and it will take effect on the next + menu change or screen refresh. + + Please see RTFM ...er, uh... see the documentation for more information, and + don't be shy to ask for help: + + BBS : Xibalba @ xibalba.l33t.codes + FTN : BBS Discussion on fsxNet + IRC : #enigma-bbs / FreeNode + Email : bryan@l33t.codes + */ + menus: { + // + // Send telnet connections to matrix where users can login, apply, etc. + // + telnetConnected: { + art: CONNECT + next: matrix + config: { nextTimeout: 1500 } + } + + // + // SSH connections are pre-authenticated via the SSH server itself. + // Jump directly to the login sequence + // + sshConnected: { + art: CONNECT + next: fullLoginSequenceLoginArt + config: { nextTimeout: 1500 } + } + + // + // Another SSH specialization: If the user logs in with a new user + // name (e.g. "new", "apply", ...) they will be directed to the + // application process. + // + sshConnectedNewUser: { + art: CONNECT + next: newUserApplicationPreSsh + config: { nextTimeout: 1500 } + } + + // Ye ol' standard matrix + matrix: { + art: matrix + form: { + 0: { + VM: { + mci: { + VM1: { + submit: true + focus: true + argName: navSelect + // + // To enable forgot password, you will need to have the web server + // enabled and mail/SMTP configured. Once that is in place, swap out + // the commented lines below as well as in the submit block + // + items: [ + { + text: login + data: login + } + { + text: apply + data: apply + } + { + text: forgot pass + data: forgot + } + { + text: log off + data: logoff + } + ] + } + } + submit: { + *: [ + { + value: { navSelect: "login" } + action: @menu:login + } + { + value: { navSelect: "apply" } + action: @menu:newUserApplicationPre + } + { + value: { navSelect: "forgot" } + action: @menu:forgotPassword + } + { + value: { navSelect: "logoff" } + action: @menu:logoff + } + ] + } + } + } + } + } + + login: { + art: USERLOG + next: fullLoginSequenceLoginArt + config: { + tooNodeMenu: loginAttemptTooNode + inactive: loginAttemptAccountInactive + disabled: loginAttemptAccountDisabled + locked: loginAttemptAccountLocked + } + form: { + 0: { + mci: { + ET1: { + maxLength: @config:users.usernameMax + argName: username + focus: true + } + ET2: { + password: true + maxLength: @config:users.passwordMax + argName: password + submit: true + } + } + submit: { + *: [ + { + value: { password: null } + action: @systemMethod:login + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + loginAttemptTooNode: { + art: TOONODE + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountLocked: { + art: ACCOUNTLOCKED + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountDisabled: { + art: ACCOUNTDISABLED + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountInactive: { + art: ACCOUNTINACTIVE + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + forgotPassword: { + desc: Forgot password + prompt: forgotPasswordPrompt + submit: [ + { + value: { username: null } + action: @systemMethod:sendForgotPasswordEmail + extraArgs: { next: "forgotPasswordSubmitted" } + } + ] + } + + forgotPasswordSubmitted: { + desc: Forgot password + art: FORGOTPWSENT + config: { + cls: true + pause: true + } + next: @systemMethod:logoff + } + + // :TODO: Prompt Yes/No for logoff confirm + fullLogoffSequence: { + desc: Logging Off + prompt: logoffConfirmation + submit: [ + { + value: { promptValue: 0 } + action: @menu:fullLogoffSequencePreAd + } + { + value: { promptValue: 1 } + action: @systemMethod:prevMenu + } + ] + } + + fullLogoffSequencePreAd: { + art: PRELOGAD + desc: Logging Off + next: fullLogoffSequenceRandomBoardAd + config: { + cls: true + nextTimeout: 1500 + } + } + + fullLogoffSequenceRandomBoardAd: { + art: OTHRBBS + desc: Logging Off + next: logoff + config: { + baudRate: 57600 + pause: true + cls: true + } + } + + logoff: { + art: LOGOFF + desc: Logging Off + next: @systemMethod:logoff + } + + // A quick preamble - defaults to warning about broken terminals + newUserApplicationPre: { + art: NEWUSER1 + next: newUserApplication + desc: Applying + config: { + pause: true + cls: true + menuFlags: [ "noHistory" ] + } + } + + newUserApplication: { + module: nua + art: NUA + next: [ + { + // Initial SysOp does not send feedback to themselves + acs: ID1 + next: fullLoginSequenceLoginArt + } + { + // ...everyone else does + next: newUserFeedbackToSysOpPreamble + } + ] + form: { + 0: { + mci: { + ET1: { + focus: true + argName: username + maxLength: @config:users.usernameMax + validate: @systemMethod:validateUserNameAvail + } + ET2: { + argName: realName + maxLength: @config:users.realNameMax + validate: @systemMethod:validateNonEmpty + } + MET3: { + argName: birthdate + maskPattern: "####/##/##" + validate: @systemMethod:validateBirthdate + } + ME4: { + argName: sex + maskPattern: A + textStyle: upper + validate: @systemMethod:validateNonEmpty + } + ET5: { + argName: location + maxLength: @config:users.locationMax + validate: @systemMethod:validateNonEmpty + } + ET6: { + argName: affils + maxLength: @config:users.affilsMax + } + ET7: { + argName: email + maxLength: @config:users.emailMax + validate: @systemMethod:validateEmailAvail + } + ET8: { + argName: web + maxLength: @config:users.webMax + } + ET9: { + 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 + items: [ "apply", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { "submission" : 0 } + action: @method:submitApplication + extraArgs: { + inactive: userNeedsActivated + error: newUserCreateError + } + } + { + value: { "submission" : 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + // A quick preamble - defaults to warning about broken terminals (SSH version) + newUserApplicationPreSsh: { + art: NEWUSER1 + next: newUserApplicationSsh + desc: Applying + config: { + pause: true + cls: true + menuFlags: [ "noHistory" ] + } + } + + // + // SSH specialization of NUA + // Canceling this form logs off vs falling back to matrix + // + newUserApplicationSsh: { + module: nua + art: NUA + fallback: logoff + next: newUserFeedbackToSysOpPreamble + form: { + 0: { + mci: { + ET1: { + focus: true + argName: username + maxLength: @config:users.usernameMax + validate: @systemMethod:validateUserNameAvail + } + ET2: { + argName: realName + maxLength: @config:users.realNameMax + validate: @systemMethod:validateNonEmpty + } + MET3: { + argName: birthdate + maskPattern: "####/##/##" + validate: @systemMethod:validateBirthdate + } + ME4: { + argName: sex + maskPattern: A + textStyle: upper + validate: @systemMethod:validateNonEmpty + } + ET5: { + argName: location + maxLength: @config:users.locationMax + validate: @systemMethod:validateNonEmpty + } + ET6: { + argName: affils + maxLength: @config:users.affilsMax + } + ET7: { + argName: email + maxLength: @config:users.emailMax + validate: @systemMethod:validateEmailAvail + } + ET8: { + argName: web + maxLength: @config:users.webMax + } + ET9: { + 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 + items: [ "apply", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { "submission" : 0 } + action: @method:submitApplication + extraArgs: { + inactive: userNeedsActivated + error: newUserCreateError + } + } + { + value: { "submission" : 1 } + action: @systemMethod:logoff + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:logoff + } + ] + } + } + } + + newUserFeedbackToSysOpPreamble: { + art: LETTER + config: { pause: true } + next: newUserFeedbackToSysOp + } + + newUserFeedbackToSysOp: { + desc: Feedback to SysOp + module: msg_area_post_fse + next: [ + { + acs: AS2 + next: fullLoginSequenceLoginArt + } + { + next: newUserInactiveDone + } + ] + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaTag: private_mail + toUserId: 1 /* always to +op */ + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + text: @sysStat:sysop_username + // :TODO: readOnly: true + } + ET3: { + argName: subject + maxLength: 72 + submit: true + text: New user feedback + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { value: "message", action: "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + newUserInactiveDone: { + desc: Finished with NUA + art: DONE + config: { pause: true } + next: @menu:logoff + } + + fullLoginSequenceLoginArt: { + desc: Logging In + art: WELCOME + config: { pause: true } + next: fullLoginSequenceLastCallers + } + + fullLoginSequenceLastCallers: { + desc: Last Callers + module: last_callers + art: LASTCALL + config: { + pause: true + font: cp437 + } + next: fullLoginSequenceWhosOnline + } + fullLoginSequenceWhosOnline: { + desc: Who's Online + module: whos_online + art: WHOSON + config: { pause: true } + next: fullLoginSequenceOnelinerz + } + + fullLoginSequenceOnelinerz: { + desc: Viewing Onelinerz + module: onelinerz + next: [ + { + // calls >= 2 + acs: NC2 + next: fullLoginSequenceNewScanConfirm + } + { + // new users - skip new scan + next: fullLoginSequenceUserStats + } + ] + config: { + cls: true + art: { + view: ONELINER + add: ONEADD + } + } + form: { + 0: { + mci: { + VM1: { + focus: false + height: 10 + } + TM2: { + argName: addOrExit + items: [ "yeah!", "nah" ] + "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } + submit: true + focus: true + } + } + submit: { + *: [ + { + value: { addOrExit: 0 } + action: @method:viewAddScreen + } + { + value: { addOrExit: null } + action: @systemMethod:nextMenu + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:nextMenu + } + ] + }, + 1: { + mci: { + ET1: { + focus: true + maxLength: 70 + argName: oneliner + } + TL2: { + width: 60 + } + TM3: { + argName: addOrCancel + items: [ "add", "cancel" ] + "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } + submit: true + } + } + + submit: { + *: [ + { + value: { addOrCancel: 0 } + action: @method:addEntry + } + { + value: { addOrCancel: 1 } + action: @method:cancelAdd + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelAdd + } + ] + } + } + } + + fullLoginSequenceNewScanConfirm: { + desc: Logging In + prompt: loginGlobalNewScan + submit: [ + { + value: { promptValue: 0 } + action: @menu:fullLoginSequenceNewScan + } + { + value: { promptValue: 1 } + action: @menu:fullLoginSequenceUserStats + } + ] + } + + fullLoginSequenceNewScan: { + desc: Performing New Scan + module: new_scan + art: NEWSCAN + next: fullLoginSequenceSysStats + config: { + messageListMenu: newScanMessageList + } + } + + fullLoginSequenceSysStats: { + desc: System Stats + art: SYSSTAT + config: { pause: true } + next: fullLoginSequenceUserStats + } + fullLoginSequenceUserStats: { + desc: User Stats + art: STATUS + config: { pause: true } + next: mainMenu + } + + newScanMessageList: { + desc: New Messages + module: msg_list + art: NEWMSGS + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + TL6: { + // theme me! + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "x", "shift + x" ] + action: @method:fullExit + } + { + keys: [ "m", "shift + m" ] + action: @method:markAllRead + } + ] + } + } + } + + newScanFileBaseList: { + module: file_area_list + desc: New Files + config: { + art: { + browse: FNEWBRWSE + details: FDETAIL + detailsGeneral: FDETGEN + detailsNfo: FDETNFO + detailsFileList: FDETLST + help: FBHELP + } + } + form: { + 0: { + mci: { + MT1: { + mode: preview + ansiView: true + } + + HM2: { + focus: true + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "help", "quit" + ] + focusItemIndex: 1 + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:prevFile + } + { + value: { navSelect: 1 } + action: @method:nextFile + } + { + value: { navSelect: 2 } + action: @method:viewDetails + } + { + value: { navSelect: 3 } + action: @method:toggleQueue + } + { + value: { navSelect: 4 } + action: @menu:fileBaseGetRatingForSelectedEntry + } + { + value: { navSelect: 5 } + action: @method:displayHelp + } + { + value: { navSelect: 6 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "w", "shift + w" ] + action: @method:showWebDownloadLink + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "t", "shift + t" ] + action: @method:toggleQueue + } + { + keys: [ "v", "shift + v" ] + action: @method:viewDetails + } + { + keys: [ "r", "shift + r" ] + action: @menu:fileBaseGetRatingForSelectedEntry + } + { + keys: [ "?" ] + action: @method:displayHelp + } + ] + } + + 1: { + mci: { + HM1: { + focus: true + submit: true + argName: navSelect + items: [ + "general", "nfo/readme", "file listing" + ] + } + } + + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @method:detailsQuit + } + ] + } + + 2: { + // details - general + mci: {} + } + + 3: { + // details - nfo/readme + mci: { + MT1: { + mode: preview + } + } + } + + 4: { + // details - file listing + mci: { + VM1: { + + } + } + } + } + } + + /////////////////////////////////////////////////////////////////////// + // Main Menu + /////////////////////////////////////////////////////////////////////// + mainMenu: { + art: MMENU + desc: Main Menu + prompt: menuCommand + config: { + font: cp437 + interrupt: realtime + } + submit: [ + { + value: { command: "MSG" } + action: @menu:nodeMessage + } + { + value: { command: "MRC" } + action: @menu:mrc + } + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + { + value: { command: "D" } + action: @menu:doorMenu + } + { + value: { command: "F" } + action: @menu:fileBase + } + { + value: { command: "U" } + action: @menu:mainMenuUserList + } + { + value: { command: "L" } + action: @menu:mainMenuLastCallers + } + { + value: { command: "W" } + action: @menu:mainMenuWhosOnline + } + { + value: { command: "Y" } + action: @menu:mainMenuUserStats + } + { + value: { command: "M" } + action: @menu:messageArea + } + { + value: { command: "E" } + action: @menu:mailMenu + } + { + value: { command: "C" } + action: @menu:mainMenuUserConfig + } + { + value: { command: "S" } + action: @menu:mainMenuSystemStats + } + { + value: { command: "!" } + action: @menu:mainMenuGlobalNewScan + } + { + value: { command: "K" } + action: @menu:mainMenuFeedbackToSysOp + } + { + value: { command: "O" } + action: @menu:mainMenuOnelinerz + } + { + value: { command: "R" } + action: @menu:mainMenuRumorz + } + { + value: { command: "BBS"} + action: @menu:bbsList + } + { + value: { command: "UA" } + action: @menu:mainMenuUserAchievementsEarned + } + { + value: 1 + action: @menu:mainMenu + } + ] + } + + mainMenuUserAchievementsEarned: { + desc: Achievements + module: user_achievements_earned + art: USERACHIEV + form: { + 0: { + mci: { + VM1: { + focus: true + } + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + ercClient: { + art: erc + module: erc_client + config: { + host: localhost + port: 5001 + bbsTag: CHANGEME + } + + form: { + 0: { + mci: { + MT1: { + width: 79 + height: 21 + mode: preview + autoScroll: true + } + ET3: { + autoScale: false + width: 77 + argName: inputArea + focus: true + submit: true + } + } + + submit: { + *: [ + { + value: { inputArea: null } + action: @method:inputAreaSubmit + } + ] + } + actionKeys: [ + { + keys: [ "tab" ] + } + { + keys: [ "up arrow" ] + action: @method:scrollDown + } + { + keys: [ "down arrow" ] + action: @method:scrollUp + } + ] + } + } + } + + mrc: { + desc: MRC Chat + module: mrc + art: MRC + config: { + cls: true + } + form: { + 0: { + mci: { + MT1: { + mode: preview + autoScroll: true + } + ET2: { + argName: inputArea + submit: true + focus: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:quit + } + { + keys: [ "down arrow", "up arrow", "page up", "page down" ] + action: @method:movementKeyPressed + } + ] + submit: { + *: [ + { + value: { inputArea: null } + action: @method:sendChatMessage + } + ] + } + } + } + } + + nodeMessage: { + desc: Node Messaging + module: node_msg + art: NODEMSG + config: { + cls: true + art: { + header: NODEMSGHDR + footer: NODEMSGFTR + } + } + form: { + 0: { + mci: { + SM1: { + argName: node + } + ET2: { + argName: message + submit: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + submit: { + *: [ + { + value: { message: null } + action: @method:sendMessage + } + ] + } + } + } + } + + mainMenuLastCallers: { + desc: Last Callers + module: last_callers + art: LASTCALL + config: { pause: true } + } + + mainMenuWhosOnline: { + desc: Who's Online + module: whos_online + art: WHOSON + config: { pause: true } + } + + mainMenuUserStats: { + desc: User Stats + art: STATUS + config: { pause: true } + } + + mainMenuSystemStats: { + desc: System Stats + art: SYSSTAT + config: { pause: true } + } + + mainMenuUserList: { + desc: User Listing + module: user_list + art: USERLST + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + } + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + mainMenuUserConfig: { + module: user_config + art: CONFSCR + form: { + 0: { + mci: { + ET1: { + argName: realName + maxLength: @config:users.realNameMax + validate: @systemMethod:validateNonEmpty + focus: true + } + ME2: { + argName: birthdate + maskPattern: "####/##/##" + } + ME3: { + argName: sex + maskPattern: A + textStyle: upper + validate: @systemMethod:validateNonEmpty + } + ET4: { + argName: location + maxLength: @config:users.locationMax + validate: @systemMethod:validateNonEmpty + } + ET5: { + argName: affils + maxLength: @config:users.affilsMax + } + ET6: { + argName: email + maxLength: @config:users.emailMax + validate: @method:validateEmailAvail + } + ET7: { + argName: web + maxLength: @config:users.webMax + } + ME8: { + maskPattern: "##" + argName: termHeight + validate: @systemMethod:validateNonEmpty + } + SM9: { + argName: theme + } + ET10: { + argName: password + maxLength: @config:users.passwordMax + password: true + validate: @method:validatePassword + } + ET11: { + argName: passwordConfirm + maxLength: @config:users.passwordMax + password: true + validate: @method:validatePassConfirmMatch + } + TM25: { + argName: submission + items: [ "save", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { submission: 0 } + action: @method:saveChanges + } + { + value: { submission: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + mainMenuGlobalNewScan: { + desc: Performing New Scan + module: new_scan + art: NEWSCAN + config: { + messageListMenu: newScanMessageList + } + } + + mainMenuFeedbackToSysOp: { + desc: Feedback to SysOp + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaTag: private_mail + toUserId: 1 /* always to +op */ + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + text: @sysStat:sysop_username + // :TODO: readOnly: true + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { value: "message", action: "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + mainMenuOnelinerz: { + desc: Viewing Onelinerz + module: onelinerz + config: { + cls: true + art: { + view: ONELINER + add: ONEADD + } + } + form: { + 0: { + mci: { + VM1: { + focus: false + height: 10 + } + TM2: { + argName: addOrExit + items: [ "yeah!", "nah" ] + "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } + submit: true + focus: true + } + } + submit: { + *: [ + { + value: { addOrExit: 0 } + action: @method:viewAddScreen + } + { + value: { addOrExit: null } + action: @systemMethod:nextMenu + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:nextMenu + } + ] + }, + 1: { + mci: { + ET1: { + focus: true + maxLength: 70 + argName: oneliner + } + TL2: { + width: 60 + } + TM3: { + argName: addOrCancel + items: [ "add", "cancel" ] + "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } + submit: true + } + } + + submit: { + *: [ + { + value: { addOrCancel: 0 } + action: @method:addEntry + } + { + value: { addOrCancel: 1 } + action: @method:cancelAdd + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelAdd + } + ] + } + } + } + + mainMenuRumorz: { + desc: Rumorz + module: rumorz + config: { + cls: true + art: { + entries: RUMORS + add: RUMORADD + } + } + form: { + 0: { + mci: { + VM1: { + focus: false + height: 10 + } + TM2: { + argName: addOrExit + items: [ "yeah!", "nah" ] + "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } + submit: true + focus: true + } + } + submit: { + *: [ + { + value: { addOrExit: 0 } + action: @method:viewAddScreen + } + { + value: { addOrExit: null } + action: @systemMethod:nextMenu + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:nextMenu + } + ] + }, + 1: { + mci: { + ET1: { + focus: true + maxLength: 70 + argName: rumor + } + TL2: { + width: 60 + } + TM3: { + argName: addOrCancel + items: [ "add", "cancel" ] + "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } + submit: true + } + } + + submit: { + *: [ + { + value: { addOrCancel: 0 } + action: @method:addEntry + } + { + value: { addOrCancel: 1 } + action: @method:cancelAdd + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelAdd + } + ] + } + } + } + + bbsList: { + desc: Viewing BBS List + module: bbs_list + config: { + cls: true + art: { + entries: BBSLIST + add: BBSADD + } + } + + form: { + 0: { + mci: { + VM1: { maxLength: 32 } + TL2: { maxLength: 32 } + TL3: { maxLength: 32 } + TL4: { maxLength: 32 } + TL5: { maxLength: 32 } + TL6: { maxLength: 32 } + TL7: { maxLength: 32 } + TL8: { maxLength: 32 } + TL9: { maxLength: 32 } + } + actionKeys: [ + { + keys: [ "a" ] + action: @method:addBBS + } + { + // :TODO: add delete key + keys: [ "d" ] + action: @method:deleteBBS + } + { + keys: [ "q", "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + ET1: { + argName: name + maxLength: 32 + validate: @systemMethod:validateNonEmpty + } + ET2: { + argName: sysop + maxLength: 32 + validate: @systemMethod:validateNonEmpty + } + ET3: { + argName: telnet + maxLength: 32 + validate: @systemMethod:validateNonEmpty + } + ET4: { + argName: www + maxLength: 32 + } + ET5: { + argName: location + maxLength: 32 + } + ET6: { + argName: software + maxLength: 32 + } + ET7: { + argName: notes + maxLength: 32 + } + TM17: { + argName: submission + items: [ "save", "cancel" ] + submit: true + } + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelSubmit + } + ] + + submit: { + *: [ + { + value: { "submission" : 0 } + action: @method:submitBBS + } + { + value: { "submission" : 1 } + action: @method:cancelSubmit + } + ] + } + } + } + } + + /////////////////////////////////////////////////////////////////////// + // Doors Menu + /////////////////////////////////////////////////////////////////////// + doorMenu: { + desc: Doors Menu + art: DOORMNU + prompt: menuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { command: "G" } + action: @menu:logoff + } + { + value: { command: "Q" } + action: @systemMethod:prevMenu + } + // + // The system supports many ways of launching doors including + // modules for DoorParty!, BBSLink, etc. + // + // Below are some examples. See the documentation for more info. + // + { + value: { command: "ABRACADABRA" } + action: @menu:doorAbracadabraExample + } + { + value: { command: "TWBBSLINK" } + action: @menu:doorTradeWars2002BBSLinkExample + } + { + value: { command: "DP" } + action: @menu:doorPartyExample + } + { + value: { command: "CN" } + action: @menu:doorCombatNetExample + } + { + value: { command: "EXODUS" } + action: @menu:doorExodusCataclysm + } + ] + } + + // + // Local Door Example via abracadabra module + // + // This example assumes launch_door.sh (which is passed args) + // launches the door. + // + doorAbracadabraExample: { + desc: Abracadabra Example + module: abracadabra + config: { + name: Example Door + dropFileType: DORINFO + cmd: /home/enigma/DOS/scripts/launch_door.sh + args: [ + "{node}", + "{dropFile}", + "{srvPort}", + ], + nodeMax: 1 + tooManyArt: DOORMANY + io: socket + } + } + + // + // BBSLink Example (TradeWars 2000) + // + // Register @ https://bbslink.net/ + // + doorTradeWars2002BBSLinkExample: { + desc: Playing TW 2002 (BBSLink) + module: bbs_link + config: { + sysCode: XXXXXXXX + authCode: XXXXXXXX + schemeCode: XXXXXXXX + door: tw + } + } + + // + // DoorParty! Example + // + // Register @ http://throwbackbbs.com/ + // + doorPartyExample: { + desc: Using DoorParty! + module: door_party + config: { + username: XXXXXXXX + password: XXXXXXXX + bbsTag: XX + } + } + + // + // CombatNet Example + // + // Register @ http://combatnet.us/ + // + doorCombatNetExample: { + desc: Using CombatNet + module: combatnet + config: { + bbsTag: CBNxxx + password: XXXXXXXXX + } + } + + // + // Exodus Example (cataclysm) + // Register @ https://oddnetwork.org/exodus/ + // + doorExodusCataclysm: { + desc: Cataclysm + module: exodus + config: { + rejectUnauthorized: false + board: XXX + key: XXXXXXXX + door: cataclysm + } + } + + /////////////////////////////////////////////////////////////////////// + // Message Area Menu + /////////////////////////////////////////////////////////////////////// + messageArea: { + art: MSGMNU + desc: Message Area + prompt: messageMenuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { command: "P" } + action: @menu:messageAreaNewPost + } + { + value: { command: "J" } + action: @menu:messageAreaChangeCurrentConference + } + { + value: { command: "C" } + action: @menu:messageAreaChangeCurrentArea + } + { + value: { command: "L" } + action: @menu:messageAreaMessageList + } + { + value: { command: "Q" } + action: @systemMethod:prevMenu + } + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + { + value: { command: "<" } + action: @systemMethod:prevConf + } + { + value: { command: ">" } + action: @systemMethod:nextConf + } + { + value: { command: "[" } + action: @systemMethod:prevArea + } + { + value: { command: "]" } + action: @systemMethod:nextArea + } + { + value: { command: "D" } + action: @menu:messageAreaSetNewScanDate + } + { + value: { command: "S" } + action: @menu:messageSearch + } + { + value: 1 + action: @menu:messageArea + } + ] + } + + messageSearch: { + desc: Message Search + module: message_base_search + art: MSEARCH + config: { + messageListMenu: messageAreaSearchMessageList + } + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + SM3: { + argName: confTag + } + SM4: { + argName: areaTag + } + ET5: { + argName: toUserName + maxLength: @config:users.usernameMax + } + ET6: { + argName: fromUserName + maxLength: @config:users.usernameMax + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaSearchMessageList: { + desc: Message Search + module: msg_list + art: MSRCHLST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + TL6: { + // theme me! + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaMyMessagesList: { + desc: Personal Messages + module: msg_list + art: MYMSGLST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageSearchNoResults: { + desc: Message Search + art: MSRCNORES + config: { + pause: true + } + } + + messageAreaChangeCurrentConference: { + art: CCHANGE + module: msg_conf_list + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: conf + } + } + submit: { + *: [ + { + value: { conf: null } + action: @method:changeConference + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaSetNewScanDate: { + module: set_newscan_date + desc: Message Base + art: SETMNSDATE + config: { + target: message + scanDateFormat: YYYYMMDD + } + form: { + 0: { + mci: { + ME1: { + focus: true + submit: true + argName: scanDate + maskPattern: "####/##/##" + } + SM2: { + argName: targetSelection + submit: false + } + } + submit: { + *: [ + { + value: { scanDate: null } + action: @method:scanDateSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + changeMessageConfPreArt: { + module: show_art + config: { + method: messageConf + key: confTag + pause: true + cls: true + menuFlags: [ "popParent", "noHistory" ] + } + } + + messageAreaChangeCurrentArea: { + // :TODO: rename this art to ACHANGE + art: CHANGE + module: msg_area_list + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: area + } + } + submit: { + *: [ + { + value: { area: null } + action: @method:changeArea + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + changeMessageAreaPreArt: { + module: show_art + config: { + method: messageArea + key: areaTag + pause: true + cls: true + menuFlags: [ "popParent", "noHistory" ] + } + } + + messageAreaMessageList: { + module: msg_list + art: MSGLIST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaViewPost: { + module: msg_area_view_fse + config: { + art: { + header: MSGVHDR + body: MSGBODY + footerView: MSGVFTR + help: MSGVHLP + }, + editorMode: view + editorType: area + } + form: { + 0: { + mci: { + // :TODO: ensure this block isn't even req. for theme to apply... + } + } + 1: { + mci: { + MT1: { + width: 79 + mode: preview + } + } + submit: { + *: [ + { + value: message + action: @method:editModeEscPressed + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + } + 2: { + TLTL: { + mci: { + TL1: { width: 5 } + TL2: { width: 4 } + } + } + } + 4: { + mci: { + HM1: { + // :TODO: (#)Jump/(L)Index (msg list)/Last + items: [ "prev", "next", "reply", "quit", "help" ] + focusItemIndex: 1 + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:prevMessage + } + { + value: { 1: 1 } + action: @method:nextMessage + } + { + value: { 1: 2 } + action: @method:replyMessage + extraArgs: { + menu: messageAreaReplyPost + } + } + { + value: { 1: 3 } + action: @systemMethod:prevMenu + } + { + value: { 1: 4 } + action: @method:viewModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "p", "shift + p" ] + action: @method:prevMessage + } + { + keys: [ "n", "shift + n" ] + action: @method:nextMessage + } + { + keys: [ "r", "shift + r" ] + action: @method:replyMessage + extraArgs: { + menu: messageAreaReplyPost + } + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "?" ] + action: @method:viewModeMenuHelp + } + { + keys: [ "down arrow", "up arrow", "page up", "page down" ] + action: @method:movementKeyPressed + } + ] + } + } + } + + messageAreaReplyPost: { + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + quote: MSGQUOT + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + } + editorMode: edit + editorType: area + } + form: { + 0: { + mci: { + // :TODO: use appropriate system properties for max lengths + TL1: { + argName: from + } + 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) + //width: 27 + //textOverflow: ... + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + MT1: { + width: 79 + height: 14 + argName: message + mode: edit + } + } + submit: { + *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ], + viewId: 1 + } + ] + } + + 3: { + mci: { + HM1: { + items: [ "save", "discard", "quote", "help" ] + } + } + + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 }, + action: @method:editModeMenuQuote + } + { + value: { 1: 3 } + action: @method:editModeMenuHelp + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "s", "shift + s" ] + action: @method:editModeMenuSave + } + { + keys: [ "d", "shift + d" ] + action: @systemMethod:prevMenu + } + { + keys: [ "q", "shift + q" ] + action: @method:editModeMenuQuote + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + + // Quote builder + 5: { + mci: { + MT1: { + width: 79 + height: 7 + } + VM3: { + width: 79 + height: 4 + argName: quote + } + } + + submit: { + *: [ + { + value: { quote: null } + action: @method:appendQuoteEntry + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @method:quoteBuilderEscPressed + } + ] + } + } + } + // :TODO: messageAreaSelect (change msg areas -> call @systemMethod -> fallback to menu + messageAreaNewPost: { + desc: Posting message, + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + } + editorMode: edit + editorType: area + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + 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 + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + + 1: { + "mci" : { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + } + 2: { + TLTL: { + mci: { + TL1: { width: 5 } + TL2: { width: 4 } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + "items" : [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + // :TODO: something like the following for overriding keymap + // this should only override specified entries. others will default + /* + "keyMap" : { + "accept" : [ "return" ] + } + */ + } + } + } + } + + + // + // User to User mail aka Email Menu + // + mailMenu: { + art: MAILMNU + desc: Mail Menu + prompt: menuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { command: "C" } + action: @menu:mailMenuCreateMessage + } + { + value: { command: "I" } + action: @menu:mailMenuInbox + } + { + value: { command: "Q" } + action: @systemMethod:prevMenu + } + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + { + value: 1 + action: @menu:mailMenu + } + ] + } + + mailMenuCreateMessage: { + desc: Mailing Someone + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaTag: private_mail + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + validate: @systemMethod:validateGeneralMailAddressedTo + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { value: "message", action: "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + mailMenuInbox: { + module: msg_list + art: PRVMSGLIST + config: { + menuViewPost: messageAreaViewPost + messageAreaTag: private_mail + } + form: { + 0: { // main list + mci: { + VM1: { + focus: true + submit: true + argName: message + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "delete", "d", "shift + d" ] + action: @method:deleteSelected + } + ] + } + 1: { // delete prompt form + submit: { + *: [ + { + value: { promptValue: 0 } + action: @method:deleteMessageYes + } + { + value: { promptValue: 1 } + action: @method:deleteMessageNo + } + ] + } + } + } + } + + //////////////////////////////////////////////////////////////////////// + // File Base + //////////////////////////////////////////////////////////////////////// + + fileBase: { + desc: File Base + art: FMENU + prompt: fileMenuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { menuOption: "L" } + action: @menu:fileBaseListEntries + } + { + value: { menuOption: "B" } + action: @menu:fileBaseBrowseByAreaSelect + } + { + value: { menuOption: "F" } + action: @menu:fileAreaFilterEditor + } + { + value: { menuOption: "Q" } + action: @systemMethod:prevMenu + } + { + value: { menuOption: "G" } + action: @menu:fullLogoffSequence + } + { + value: { menuOption: "D" } + action: @menu:fileBaseDownloadManager + } + { + value: { menuOption: "W" } + action: @menu:fileBaseWebDownloadManager + } + { + value: { menuOption: "U" } + action: @menu:fileBaseUploadFiles + } + { + value: { menuOption: "S" } + action: @menu:fileBaseSearch + } + { + value: { menuOption: "P" } + action: @menu:fileBaseSetNewScanDate + } + { + value: { menuOption: "E" } + action: @menu:fileBaseExportListFilter + } + ] + } + + fileBaseExportListFilter: { + module: file_base_search + art: FBLISTEXPSEARCH + config: { + fileBaseListEntriesMenu: fileBaseExportList + } + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + ET3: { + maxLength: 64 + argName: tags + } + SM4: { + maxLength: 64 + argName: areaIndex + } + SM5: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + "filename" + ] + argName: sortByIndex + } + SM6: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseExportList: { + module: file_base_user_list_export + art: FBLISTEXP + config: { + pause: true + templates: { + entry: file_list_entry.asc + } + } + form: { + 0: { + mci: { + TL1: { } + TL2: { } + } + } + } + } + + fileBaseExportListNoResults: { + desc: Browsing Files + art: FBNORES + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + fileBaseSetNewScanDate: { + module: set_newscan_date + desc: File Base + art: SETFNSDATE + config: { + target: file + scanDateFormat: YYYYMMDD + } + form: { + 0: { + mci: { + ME1: { + focus: true + submit: true + argName: scanDate + maskPattern: "####/##/##" + } + } + submit: { + *: [ + { + value: { scanDate: null } + action: @method:scanDateSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseListEntries: { + module: file_area_list + desc: Browsing Files + config: { + art: { + browse: FBRWSE + details: FDETAIL + detailsGeneral: FDETGEN + detailsNfo: FDETNFO + detailsFileList: FDETLST + help: FBHELP + } + } + form: { + 0: { + mci: { + MT1: { + mode: preview + } + + HM2: { + focus: true + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "change filter", "help", "quit" + ] + focusItemIndex: 1 + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:prevFile + } + { + value: { navSelect: 1 } + action: @method:nextFile + } + { + value: { navSelect: 2 } + action: @method:viewDetails + } + { + value: { navSelect: 3 } + action: @method:toggleQueue + } + { + value: { navSelect: 4 } + action: @menu:fileBaseGetRatingForSelectedEntry + } + { + value: { navSelect: 5 } + action: @menu:fileAreaFilterEditor + } + { + value: { navSelect: 6 } + action: @method:displayHelp + } + { + value: { navSelect: 7 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "w", "shift + w" ] + action: @method:showWebDownloadLink + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "t", "shift + t" ] + action: @method:toggleQueue + } + { + keys: [ "f", "shift + f" ] + action: @menu:fileAreaFilterEditor + } + { + keys: [ "v", "shift + v" ] + action: @method:viewDetails + } + { + keys: [ "r", "shift + r" ] + action: @menu:fileBaseGetRatingForSelectedEntry + } + { + keys: [ "?" ] + action: @method:displayHelp + } + ] + } + + 1: { + mci: { + HM1: { + focus: true + submit: true + argName: navSelect + items: [ + "general", "nfo/readme", "file listing" + ] + } + } + + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @method:detailsQuit + } + ] + } + + 2: { + // details - general + mci: {} + } + + 3: { + // details - nfo/readme + mci: { + MT1: { + mode: preview + } + } + } + + 4: { + // details - file listing + mci: { + VM1: { + + } + } + } + } + } + + fileBaseBrowseByAreaSelect: { + desc: Browsing File Areas + module: file_base_area_select + art: FAREASEL + form: { + 0: { + mci: { + VM1: { + focus: true + argName: areaTag + } + } + + submit: { + *: [ + { + value: { areaTag: null } + action: @method:selectArea + } + ] + } + + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseGetRatingForSelectedEntry: { + desc: Rating a File + prompt: fileBaseRateEntryPrompt + config: { + cls: true + } + submit: [ + // :TODO: handle esc/q + { + // pass data back to caller + value: { rating: null } + action: @systemMethod:prevMenu + } + ] + } + + fileBaseListEntriesNoResults: { + desc: Browsing Files + art: FBNORES + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + fileBaseSearch: { + module: file_base_search + desc: Searching Files + art: FSEARCH + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + ET3: { + maxLength: 64 + argName: tags + } + SM4: { + maxLength: 64 + argName: areaIndex + } + SM5: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + "filename", + ] + argName: sortByIndex + } + SM6: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileAreaFilterEditor: { + desc: File Filter Editor + module: file_area_filter_edit + art: FFILEDT + form: { + 0: { + mci: { + ET1: { + argName: searchTerms + } + ET2: { + maxLength: 64 + argName: tags + } + SM3: { + maxLength: 64 + argName: areaIndex + } + SM4: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + ] + argName: sortByIndex + } + SM5: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + ET6: { + maxLength: 64 + argName: name + validate: @systemMethod:validateNonEmpty + } + HM7: { + focus: true + items: [ + "prev", "next", "make active", "save", "new", "delete" + ] + argName: navSelect + focusItemIndex: 1 + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:prevFilter + } + { + value: { navSelect: 1 } + action: @method:nextFilter + } + { + value: { navSelect: 2 } + action: @method:makeFilterActive + } + { + value: { navSelect: 3 } + action: @method:saveFilter + } + { + value: { navSelect: 4 } + action: @method:newFilter + } + { + value: { navSelect: 5 } + action: @method:deleteFilter + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseDownloadManager: { + desc: Download Manager + module: file_base_download_manager + config: { + art: { + queueManager: FDLMGR + /* + NYI + details: FDLDET + */ + } + emptyQueueMenu: fileBaseDownloadManagerEmptyQueue + } + form: { + 0: { + mci: { + VM1: { + argName: queueItem + } + HM2: { + focus: true + items: [ "download all", "quit" ] + argName: navSelect + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:downloadAll + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "a", "shift + a" ] + action: @method:downloadAll + } + { + keys: [ "delete", "r", "shift + r" ] + action: @method:removeItem + } + { + keys: [ "c", "shift + c" ] + action: @method:clearQueue + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseWebDownloadManager: { + desc: Web D/L Manager + module: file_base_web_download_manager + config: { + art: { + queueManager: FWDLMGR + batchList: BATDLINF + } + emptyQueueMenu: fileBaseDownloadManagerEmptyQueue + } + form: { + 0: { + mci: { + VM1: { + argName: queueItem + } + HM2: { + focus: true + items: [ "get batch link", "quit", "help" ] + argName: navSelect + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:getBatchLink + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "b", "shift + b" ] + action: @method:getBatchLink + } + { + keys: [ "delete", "r", "shift + r" ] + action: @method:removeItem + } + { + keys: [ "c", "shift + c" ] + action: @method:clearQueue + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseDownloadManagerEmptyQueue: { + desc: Empty Download Queue + art: FEMPTYQ + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + fileTransferProtocolSelection: { + desc: Protocol selection + module: file_transfer_protocol_select + art: FPROSEL + form: { + 0: { + mci: { + VM1: { + focus: true + argName: protocol + } + } + + submit: { + *: [ + { + value: { protocol: null } + action: @method:selectProtocol + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseUploadFiles: { + desc: Uploading + module: upload + config: { + interrupt: never + art: { + options: ULOPTS + fileDetails: ULDETAIL + processing: ULCHECK + dupes: ULDUPES + } + } + + form: { + // options + 0: { + mci: { + SM1: { + argName: areaSelect + focus: true + } + TM2: { + argName: uploadType + items: [ "blind", "supply filename" ] + } + ET3: { + argName: fileName + maxLength: 255 + validate: @method:validateNonBlindFileName + } + HM4: { + argName: navSelect + items: [ "continue", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:optionsNavContinue + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + "actionKeys" : [ + { + "keys" : [ "escape" ], + action: @systemMethod:prevMenu + } + ] + } + + 1: { + mci: { } + } + + // file details entry + 2: { + mci: { + MT1: { + argName: shortDesc + tabSwitchesView: true + focus: true + } + + ET2: { + argName: tags + } + + ME3: { + argName: estYear + maskPattern: "####" + } + + BT4: { + argName: continue + text: continue + submit: true + } + } + + submit: { + *: [ + { + value: { continue: null } + action: @method:fileDetailsContinue + } + ] + } + } + + // dupes + 3: { + mci: { + VM1: { + /* + Use 'dupeInfoFormat' to custom format: + + areaDesc + areaName + areaTag + desc + descLong + fileId + fileName + fileSha256 + storageTag + uploadTimestamp + + */ + + mode: preview + } + } + } + } + } + + fileBaseNoUploadAreasAvail: { + desc: File Base + art: ULNOAREA + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + sendFilesToUser: { + desc: Downloading + module: file_transfer + config: { + // defaults - generally use extraArgs + protocol: zmodem8kSexyz + direction: send + } + } + + recvFilesFromUser: { + desc: Uploading + module: file_transfer + config: { + // defaults - generally use extraArgs + protocol: zmodem8kSexyz + direction: recv + } + } + + + //////////////////////////////////////////////////////////////////////// + // Required entries + //////////////////////////////////////////////////////////////////////// + idleLogoff: { + art: IDLELOG + next: @systemMethod:logoff + } + //////////////////////////////////////////////////////////////////////// + // Demo Section + // :TODO: This entire section needs updated!!! + //////////////////////////////////////////////////////////////////////// + "demoMain" : { + "art" : "demo_selection_vm.ans", + "form" : { + "0" : { + "VM" : { + "mci" : { + "VM1" : { + "items" : [ + "Single Line Text Editing Views", + "Spinner & Toggle Views", + "Mask Edit Views", + "Multi Line Text Editor", + "Vertical Menu Views", + "Horizontal Menu Views", + "Art Display", + "Full Screen Editor" + ], + "height" : 10, + "itemSpacing" : 1, + "justify" : "center", + "focusTextStyle" : "small i" + } + }, + "submit" : { + "*" : [ + { + "value" : { "1" : 0 }, + "action" : "@menu:demoEditTextView" + }, + { + "value" : { "1" : 1 }, + "action" : "@menu:demoSpinAndToggleView" + }, + { + "value" : { "1" : 2 }, + "action" : "@menu:demoMaskEditView" + }, + { + "value" : { "1" : 3 }, + "action" : "@menu:demoMultiLineEditTextView" + }, + { + "value" : { "1" : 4 }, + "action" : "@menu:demoVerticalMenuView" + }, + { + "value" : { "1" : 5 }, + "action" : "@menu:demoHorizontalMenuView" + }, + { + "value" : { "1" : 6 }, + "action" : "@menu:demoArtDisplay" + }, + { + "value" : { "1" : 7 }, + "action" : "@menu:demoFullScreenEditor" + } + ] + } + } + } + } + }, + "demoEditTextView" : { + "art" : "demo_edit_text_view1.ans", + "form" : { + "0" : { + "BTETETETET" : { + "mci" : { + "ET1" : { + "width" : 20, + "maxLength" : 20 + }, + "ET2" : { + "width" : 20, + "maxLength" : 40, + "textOverflow" : "..." + }, + "ET3" : { + "width" : 20, + "fillChar" : "-", + "styleSGR1" : "|00|36", + "maxLength" : 20 + }, + "ET4" : { + "width" : 20, + "maxLength" : 20, + "password" : true + }, + "BT5" : { + "width" : 8, + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + }, + "demoSpinAndToggleView" : { + "art" : "demo_spin_and_toggle.ans", + "form" : { + "0" : { + "BTSMSMTM" : { + "mci" : { + "SM1" : { + "items" : [ "Henry Morgan", "François l'Ollonais", "Roche Braziliano", "Black Bart", "Blackbeard" ] + }, + "SM2" : { + "items" : [ "Razor 1911", "DrinkOrDie", "TRSI" ] + }, + "TM3" : { + "items" : [ "Yarly", "Nowaii" ], + "styleSGR1" : "|00|30|01", + "hotKeys" : { "Y" : 0, "N" : 1 } + }, + "BT8" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 8, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 8 + } + ] + } + } + } + }, + "demoMaskEditView" : { + "art" : "demo_mask_edit_text_view1.ans", + "form" : { + "0" : { + "BTMEME" : { + "mci" : { + "ME1" : { + "maskPattern" : "##/##/##", + "styleSGR1" : "|00|30|01", + //"styleSGR2" : "|00|45|01", + "styleSGR3" : "|00|30|35", + "fillChar" : "#" + }, + "BT5" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + }, + "demoMultiLineEditTextView" : { + "art" : "demo_multi_line_edit_text_view1.ans", + "form" : { + "0" : { + "BTMT" : { + "mci" : { + "MT1" : { + "width" : 70, + "height" : 17, + //"text" : "@art:demo_multi_line_edit_text_view_text.txt", + // "text" : "@systemMethod:textFromFile" + text: "Hints:\n\t* Insert / CTRL-V toggles overtype mode\n\t* CTRL-Y deletes the current line\n\t* Try Page Up / Page Down\n\t* Home goes to the start of line text\n\t* End goes to the end of a line\n\n\nTab handling:\n-------------------------------------------------\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\nA0\tBB\t1\tCCC\t2\tDDD\t3EEEE\nW\t\tX\t\tY\t\tZ\n\nAn excerpt from A Clockwork Orange:\n\"What sloochatted then, of course, was that my cellmates woke up and started joining in, tolchocking a bit wild in the near-dark, and the shoom seemed to wake up the whole tier, so that you could slooshy a lot of creeching and banging about with tin mugs on the wall, as though all the plennies in all the cells thought a big break was about to commence, O my brothers.\n", + "focus" : true + }, + "BT5" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + }, + "demoHorizontalMenuView" : { + "art" : "demo_horizontal_menu_view1.ans", + "form" : { + "0" : { + "BTHMHM" : { + "mci" : { + "HM1" : { + "items" : [ "One", "Two", "Three" ], + "hotKeys" : { "1" : 0, "2" : 1, "3" : 2 } + }, + "HM2" : { + "items" : [ "Uno", "Dos", "Tres" ], + "hotKeys" : { "U" : 0, "D" : 1, "T" : 2 } + }, + "BT5" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + }, + "demoVerticalMenuView" : { + "art" : "demo_vertical_menu_view1.ans", + "form" : { + "0" : { + "BTVM" : { + "mci" : { + "VM1" : { + "items" : [ + "|33Oblivion/2", + "|33iNiQUiTY", + "|33ViSiON/X" + ], + "focusItems" : [ + "|33Oblivion|01/|00|332", + "|01|33i|00|33N|01i|00|33QU|01i|00|33TY", + "|33ViSiON/X" + ] + // + // :TODO: how to do the following: + // 1) Supply a view a string for a standard vs focused item + // "items" : [...], "focusItems" : [ ... ] ? + // "draw" : "@method:drawItemX", then items: [...] + }, + "BT5" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + + }, + "demoArtDisplay" : { + "art" : "demo_selection_vm.ans", + "form" : { + "0" : { + "VM" : { + "mci" : { + "VM1" : { + "items" : [ + "Defaults - DOS ANSI", + "bw_mindgames.ans - DOS", + "test.ans - DOS", + "Defaults - Amiga", + "Pause at Term Height" + ], + // :TODO: justify not working?? + "focusTextStyle" : "small i" + } + }, + "submit" : { + "*" : [ + { + "value" : { "1" : 0 }, + "action" : "@menu:demoDefaultsDosAnsi" + }, + { + "value" : { "1" : 1 }, + "action" : "@menu:demoDefaultsDosAnsi_bw_mindgames" + }, + { + "value" : { "1" : 2 }, + "action" : "@menu:demoDefaultsDosAnsi_test" + } + ] + } + } + } + } + }, + "demoDefaultsDosAnsi" : { + "art" : "DM-ENIG2.ANS" + }, + "demoDefaultsDosAnsi_bw_mindgames" : { + "art" : "bw_mindgames.ans" + }, + "demoDefaultsDosAnsi_test" : { + "art" : "test.ans" + }, + "demoFullScreenEditor" : { + "module" : "fse", + "config" : { + "editorType" : "netMail", + "art" : { + "header" : "demo_fse_netmail_header.ans", + "body" : "demo_fse_netmail_body.ans", + "footerEditor" : "demo_fse_netmail_footer_edit.ans", + "footerEditorMenu" : "demo_fse_netmail_footer_edit_menu.ans", + "footerView" : "demo_fse_netmail_footer_view.ans", + "help" : "demo_fse_netmail_help.ans" + } + }, + "form" : { + "0" : { + "ETETET" : { + "mci" : { + "ET1" : { + // :TODO: from/to may be set by args + // :TODO: focus may change dep on view vs edit + "width" : 36, + "focus" : true, + "argName" : "to" + }, + "ET2" : { + "width" : 36, + "argName" : "from" + }, + "ET3" : { + "width" : 65, + "maxLength" : 72, + "submit" : [ "enter" ], + "argName" : "subject" + } + }, + "submit" : { + "3" : [ + { + "value" : { "subject" : null }, + "action" : "@method:headerSubmit" + } + ] + } + } + }, + "1" : { + "MT" : { + "mci" : { + "MT1" : { + "width" : 79, + "height" : 17, + "text" : "", // :TODO: should not be req. + "argName" : "message" + } + }, + "submit" : { + "*" : [ + { + "value" : "message", + "action" : "@method:editModeEscPressed" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 1 + } + ] + } + }, + "2" : { + "TLTL" : { + "mci" : { + "TL1" : { + "width" : 5 + }, + "TL2" : { + "width" : 4 + } + } + } + }, + "3" : { + "HM" : { + "mci" : { + "HM1" : { + // :TODO: Continue, Save, Discard, Clear, Quote, Help + "items" : [ "Save", "Discard", "Quote", "Help" ] + } + }, + "submit" : { + "*" : [ + { + "value" : { "1" : 0 }, + "action" : "@method:editModeMenuSave" + }, + { + "value" : { "1" : 1 }, + "action" : "@menu:demoMain" + }, + { + "value" : { "1" : 2 }, + "action" : "@method:editModeMenuQuote" + }, + { + "value" : { "1" : 3 }, + "action" : "@method:editModeMenuHelp" + }, + { + "value" : 1, + "action" : "@method:editModeEscPressed" + } + ] + }, + "actionKeys" : [ // :TODO: Need better name + { + "keys" : [ "escape" ], + "action" : "@method:editModeEscPressed" + } + ] + } + } + } + } + } +} From 2c3219fc67279d0aa16711138726d81d7559e823 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 May 2019 23:44:38 +0100 Subject: [PATCH 09/36] Remove heartbeat when client exits mrc --- core/mrc.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/mrc.js b/core/mrc.js index 54aa70f7..d696c9e7 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -112,6 +112,7 @@ exports.getModule = class mrcModule extends MenuModule { quit : (formData, extraArgs, cb) => { this.sendServerMessage('LOGOFF'); + clearInterval(this.heartbeat); this.state.socket.destroy(); return this.prevMenu(cb); } @@ -147,7 +148,7 @@ exports.getModule = class mrcModule extends MenuModule { self.clientConnect(); // send register to central MRC and get stats every 60s - setInterval(function () { + self.heartbeat = setInterval(function () { self.sendHeartbeat(); self.sendServerMessage('STATS'); }, 60000); From 072246ae1b125ba6b4c5d1eebc7b07eca72c759b Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 May 2019 23:45:00 +0100 Subject: [PATCH 10/36] Main menu ansi to include MRC --- art/themes/luciano_blocktronics/MMENU.ANS | Bin 3610 -> 3653 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/MMENU.ANS b/art/themes/luciano_blocktronics/MMENU.ANS index bd8a483f56406abf49f17d727730aab0a4a157ff..9db39ca9e7afc13910caf7d3549d931dc59f72fd 100644 GIT binary patch delta 63 zcmbOwb5v%76|b(5fwOe9VXi`MQL=QjL9T*yw6R%kZfQl delta 20 ccmX>qGfQTJ6)%&C<>YkU9gOQIEAXiT07rlZQvd(} From ca8add36b52587d67e4fa55b6dd53821374d3af3 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Mon, 27 May 2019 15:50:07 +0100 Subject: [PATCH 11/36] reenable disabled jobs --- core/config.js | 56 +++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/core/config.js b/core/config.js index ea309fc6..37dfbd90 100644 --- a/core/config.js +++ b/core/config.js @@ -984,38 +984,38 @@ function getDefaultConfig() { eventScheduler : { events : { - // dailyMaintenance : { - // schedule : 'at 11:59pm', - // action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', - // }, - // trimMessageAreas : { - // // may optionally use [or ]@watch:/path/to/file - // schedule : 'every 24 hours', + dailyMaintenance : { + schedule : 'at 11:59pm', + action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', + }, + trimMessageAreas : { + // may optionally use [or ]@watch:/path/to/file + schedule : 'every 24 hours', - // // action: - // // - @method:path/to/module.js:theMethodName - // // (path is relative to ENiGMA base dir) - // // - // // - @execute:/path/to/something/executable.sh - // // - // action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', - // }, + // action: + // - @method:path/to/module.js:theMethodName + // (path is relative to ENiGMA base dir) + // + // - @execute:/path/to/something/executable.sh + // + action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', + }, - // nntpMaintenance : { - // schedule : 'every 12 hours', // should generally be < trimMessageAreas interval - // action : '@method:core/servers/content/nntp.js:performMaintenanceTask', - // }, + nntpMaintenance : { + schedule : 'every 12 hours', // should generally be < trimMessageAreas interval + action : '@method:core/servers/content/nntp.js:performMaintenanceTask', + }, - // updateFileAreaStats : { - // schedule : 'every 1 hours', - // action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', - // }, + updateFileAreaStats : { + schedule : 'every 1 hours', + action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', + }, - // forgotPasswordMaintenance : { - // schedule : 'every 24 hours', - // action : '@method:core/web_password_reset.js:performMaintenanceTask', - // args : [ '24 hours' ] // items older than this will be removed - // }, + forgotPasswordMaintenance : { + schedule : 'every 24 hours', + action : '@method:core/web_password_reset.js:performMaintenanceTask', + args : [ '24 hours' ] // items older than this will be removed + }, // // Enable the following entry in your config.hjson to periodically create/update From b4e37118dd1b3849870cfe9e94deab1189ec355d Mon Sep 17 00:00:00 2001 From: David Stephens Date: Tue, 28 May 2019 23:45:22 +0100 Subject: [PATCH 12/36] Fixed user achievements ansi --- .../{USERACHIEV.ANS => USERACHIEV.ans} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename art/themes/luciano_blocktronics/{USERACHIEV.ANS => USERACHIEV.ans} (100%) diff --git a/art/themes/luciano_blocktronics/USERACHIEV.ANS b/art/themes/luciano_blocktronics/USERACHIEV.ans similarity index 100% rename from art/themes/luciano_blocktronics/USERACHIEV.ANS rename to art/themes/luciano_blocktronics/USERACHIEV.ans From 593bf67b45a7bf45bd2db19b850d3e69a79a17ef Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 01:16:32 +0100 Subject: [PATCH 13/36] Stop passing sockets all over the place. General tidy up and refactor. Add reconnection to MRC logic. --- core/servers/chat/mrc_multiplexer.js | 170 ++++++++++++++++++--------- 1 file changed, 113 insertions(+), 57 deletions(-) diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 7eaac22f..e0ef4a54 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -16,7 +16,8 @@ const os = require('os'); // MRC -const PROTOCOL_VERSION = '1.2.9'; +const protocolVersion = '1.2.9'; +const lineDelimiter = new RegExp('\r\n|\r|\n'); const ModuleInfo = exports.moduleInfo = { name : 'MRC', @@ -27,62 +28,130 @@ const ModuleInfo = exports.moduleInfo = { }; const connectedSockets = new Set(); -let mrcCentralConnection = ''; exports.getModule = class MrcModule extends ServerModule { constructor() { super(); - this.log = Log.child( { server : 'MRC' } ); + + this.log = Log.child( { server : 'MRC' } ); + this.config = Config(); + this.mrcConnectOpts = { + host : this.config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', + port : this.config.chatServers.mrc.serverPort || 5000, + retryDelay : this.config.chatServers.mrc.retryDelay || 10000 + }; } - createServer(cb) { - if (!this.enabled) { - return cb(null); - } - - const self = this; - + _connectionHandler() { const config = Config(); const boardName = config.general.prettyBoardName || config.general.boardName; const enigmaVersion = 'ENiGMA-BBS_' + require('../../../package.json').version; - const mrcConnectOpts = { - host : config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', - port : config.chatServers.mrc.serverPort || 5000 - }; - - const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${PROTOCOL_VERSION}`; + const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${protocolVersion}`; this.log.debug({ handshake : handshake }, 'Handshaking with MRC server'); + this.mrcClient.write(handshake); + this.log.info(this.mrcConnectOpts, 'Connected to MRC server'); + } + + createServer(cb) { + + if (!this.enabled) { + return cb(null); + } + + this.connectToMrc(); + this.createLocalListener(); + + return cb(null); + } + + listen(cb) { + if (!this.enabled) { + return cb(null); + } + + const config = Config(); + + const port = parseInt(config.chatServers.mrc.multiplexerPort); + if(isNaN(port)) { + this.log.warn( { port : config.chatServers.mrc.multiplexerPort, server : ModuleInfo.name }, 'Invalid port' ); + return cb(Errors.Invalid(`Invalid port: ${config.chatServers.mrc.multiplexerPort}`)); + } + Log.info( { server : ModuleInfo.name, port : config.chatServers.mrc.multiplexerPort }, 'MRC multiplexer local listener starting up'); + return this.server.listen(port, cb); + } + + /** + * Handles connecting to to the MRC server + */ + connectToMrc() { + const self = this; + // create connection to MRC server - this.mrcClient = net.createConnection(mrcConnectOpts, () => { - this.mrcClient.write(handshake); - this.log.info(mrcConnectOpts, 'Connected to MRC server'); - mrcCentralConnection = this.mrcClient; - }); + this.mrcClient = net.createConnection(this.mrcConnectOpts, self._connectionHandler.bind(self)); + + this.mrcClient.requestedDisconnect = false; // do things when we get data from MRC central - this.mrcClient.on('data', (data) => { - // split on \n to deal with getting messages in batches - data.toString().split('\n').forEach( item => { - if (item == '') return; + var buffer = new Buffer.from(''); - this.log.debug( { data : item } , 'Received data'); - let message = this.parseMessage(item); - this.log.debug(message, 'Parsed data'); - if (message) { - this.receiveFromMRC(this.mrcClient, message); + function handleData(chunk) { + if (typeof (chunk) === 'string') { + buffer += chunk; + } else { + buffer = Buffer.concat([buffer, chunk]); + } + + var lines = buffer.toString().split(lineDelimiter); + + if (lines.pop()) { + // if buffer is not ended with \r\n, there's more chunks. + return; + } else { + // else, initialize the buffer. + buffer = new Buffer.from(''); + } + + lines.forEach(function iterator(line) { + if (line.length) { + let message = self.parseMessage(line); + if (message) { + self.receiveFromMRC(message); + } } }); + } + + this.mrcClient.on('data', (data) => { + handleData(data); }); this.mrcClient.on('end', () => { - this.log.info(mrcConnectOpts, 'Disconnected from MRC server'); + this.log.info(this.mrcConnectOpts, 'Disconnected from MRC server'); + }); + + this.mrcClient.on('close', () => { + + if (this.mrcClient && this.mrcClient.requestedDisconnect) + return; + this.log.info(this.mrcConnectOpts, 'Disconnected from MRC server, reconnecting'); + + this.log.debug('Waiting ' + this.mrcConnectOpts.retryDelay + 'ms before retrying'); + + + setTimeout(function() { + self.connectToMrc(); + }, this.mrcConnectOpts.retryDelay); }); this.mrcClient.on('error', err => { this.log.info( { error : err.message }, 'MRC server error'); }); + } + + createLocalListener() { + const self = this; // start a local server for clients to connect to this.server = net.createServer( function(socket) { @@ -90,7 +159,7 @@ exports.getModule = class MrcModule extends ServerModule { socket.on('data', data => { // split on \n to deal with getting messages in batches - data.toString().split('\n').forEach( item => { + data.toString().split(lineDelimiter).forEach( item => { if (item == '') return; // save username with socket @@ -114,24 +183,6 @@ exports.getModule = class MrcModule extends ServerModule { } }); }); - - return cb(null); - } - - listen(cb) { - if (!this.enabled) { - return cb(null); - } - - const config = Config(); - - const port = parseInt(config.chatServers.mrc.multiplexerPort); - if(isNaN(port)) { - this.log.warn( { port : config.chatServers.mrc.multiplexerPort, server : ModuleInfo.name }, 'Invalid port' ); - return cb(Errors.Invalid(`Invalid port: ${config.chatServers.mrc.multiplexerPort}`)); - } - Log.info( { server : ModuleInfo.name, port : config.chatServers.mrc.multiplexerPort }, 'MRC multiplexer local listener starting up'); - return this.server.listen(port, cb); } get enabled() { @@ -158,21 +209,21 @@ exports.getModule = class MrcModule extends ServerModule { } /** - * Processes messages received // split raw data received into an object we can work withfrom the central MRC server + * Processes messages received from the central MRC server */ - receiveFromMRC(socket, message) { + receiveFromMRC(message) { const config = Config(); const siteName = slugify(config.general.boardName); if (message.from_user == 'SERVER' && message.body == 'HELLO') { // reply with extra bbs info - this.sendToMrcServer(socket, 'CLIENT', '', 'SERVER', 'ALL', '', `INFOSYS:${StatLog.getSystemStat(SysProps.SysOpUsername)}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOSYS:${StatLog.getSystemStat(SysProps.SysOpUsername)}`); } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { // reply to heartbeat // this.log.debug('Respond to heartbeat'); - this.sendToMrcServer(socket, 'CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); } else { // if not a heartbeat, and we have clients then we need to send something to them @@ -210,13 +261,13 @@ exports.getModule = class MrcModule extends ServerModule { Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); } - this.sendToMrcServer(mrcCentralConnection, message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body); + this.sendToMrcServer(message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body); } /** * Converts a message back into the MRC format and sends it to the central MRC server */ - sendToMrcServer(socket, fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { + sendToMrcServer(fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { const config = Config(); const siteName = slugify(config.general.boardName); @@ -231,7 +282,12 @@ exports.getModule = class MrcModule extends ServerModule { ].join('~') + '~'; Log.debug({ server : 'MRC', data : line }, 'Sending data'); - return socket.write(line + '\n'); + this.sendRaw(line); + } + + sendRaw(message) { + // optionally log messages here + this.mrcClient.write(message + '\r\n'); } }; From 136854017a26582a0684c0b23f8840f510e8d401 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 20:32:16 +0100 Subject: [PATCH 14/36] Lots of tidy-up from PR #235 feedback --- core/servers/chat/mrc_multiplexer.js | 44 +++++++++++++--------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index e0ef4a54..641353c2 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -45,12 +45,12 @@ exports.getModule = class MrcModule extends ServerModule { _connectionHandler() { const config = Config(); const boardName = config.general.prettyBoardName || config.general.boardName; - const enigmaVersion = 'ENiGMA-BBS_' + require('../../../package.json').version; + const enigmaVersion = 'ENiGMA½-BBS_' + require('../../../package.json').version; const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${protocolVersion}`; this.log.debug({ handshake : handshake }, 'Handshaking with MRC server'); - this.mrcClient.write(handshake); + this.sendRaw(handshake); this.log.info(this.mrcConnectOpts, 'Connected to MRC server'); } @@ -94,10 +94,10 @@ exports.getModule = class MrcModule extends ServerModule { this.mrcClient.requestedDisconnect = false; // do things when we get data from MRC central - var buffer = new Buffer.from(''); + let buffer = new Buffer.from(''); function handleData(chunk) { - if (typeof (chunk) === 'string') { + if(_.isString(chunk)) { buffer += chunk; } else { buffer = Buffer.concat([buffer, chunk]); @@ -113,7 +113,7 @@ exports.getModule = class MrcModule extends ServerModule { buffer = new Buffer.from(''); } - lines.forEach(function iterator(line) { + lines.forEach( line => { if (line.length) { let message = self.parseMessage(line); if (message) { @@ -151,10 +151,9 @@ exports.getModule = class MrcModule extends ServerModule { } createLocalListener() { - const self = this; - // start a local server for clients to connect to - this.server = net.createServer( function(socket) { + + this.server = net.createServer( socket => { socket.setEncoding('ascii'); socket.on('data', data => { @@ -168,7 +167,7 @@ exports.getModule = class MrcModule extends ServerModule { socket.username = item.split('|')[1]; Log.debug( { server : 'MRC', user: socket.username } , 'User connected'); } else { - self.receiveFromClient(socket.username, item); + this.receiveFromClient(socket.username, item); } }); }); @@ -219,6 +218,10 @@ exports.getModule = class MrcModule extends ServerModule { if (message.from_user == 'SERVER' && message.body == 'HELLO') { // reply with extra bbs info this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOSYS:${StatLog.getSystemStat(SysProps.SysOpUsername)}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOWEB:${config.general.website}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOTEL:${config.general.telnetHostname}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOSSH:${config.general.sshHostname}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFODSC:${config.general.description}`); } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { // reply to heartbeat @@ -235,20 +238,15 @@ exports.getModule = class MrcModule extends ServerModule { * Takes an MRC message and parses it into something usable */ parseMessage(line) { - const msg = line.split('~'); - if (msg.length < 7) { - return; - } - return { - from_user: msg[0], - from_site: msg[1], - from_room: msg[2], - to_user: msg[3], - to_site: msg[4], - to_room: msg[5], - body: msg[6] - }; + const [from_user, from_site, from_room, to_user, to_site, to_room, body ] = line.split('~'); + + // const msg = line.split('~'); + // if (msg.length < 7) { + // return; + // } + + return { from_user, from_site, from_room, to_user, to_site, to_room, body }; } /** @@ -287,7 +285,7 @@ exports.getModule = class MrcModule extends ServerModule { sendRaw(message) { // optionally log messages here - this.mrcClient.write(message + '\r\n'); + this.mrcClient.write(message + '\n'); } }; From ce16f17081e34f2588775410cf2272dab5450b80 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 20:34:07 +0100 Subject: [PATCH 15/36] Fix MRC MLTEV padding bug --- core/mrc.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index d696c9e7..172466a9 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -178,14 +178,17 @@ exports.getModule = class mrcModule extends MenuModule { const messageLength = stripMciColorCodes(message).length; const chatWidth = chatLogView.dimens.width; let padAmount = 0; + let spaces = 2; if (messageLength > chatWidth) { - padAmount = chatWidth - (messageLength % chatWidth); + padAmount = chatWidth - (messageLength % chatWidth) - spaces; } else { - padAmount = chatWidth - messageLength; + padAmount = chatWidth - messageLength - spaces ; } - const padding = ' |00' + ' '.repeat(padAmount - 2); + if (padAmount < 0) padAmount = 0; + + const padding = ' |00' + ' '.repeat(padAmount); chatLogView.addText(pipeToAnsi(message + padding)); } From 793c05ee82f70e296dfdd115c53dbcef1e84b1ec Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 21:08:22 +0100 Subject: [PATCH 16/36] Add config options to template --- misc/config_template.in.hjson | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 5e523e72..960a9896 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -50,6 +50,21 @@ general: { // Your BBS Name! boardName: XXXXX + + // Your BBS name, with pipe codes for styling + prettyBoardName : '|08XXXXX' + + // Telnet hostname and port for your board + telnetHostname : 'xibalba.l33t.codes:44510' + + // SSH hostname and port for your board + sshHostname : 'xibalba.l33t.codes:44511' + + // Your board's website + website : 'https://enigma-bbs.github.io' + + // Short board description + description : 'Yet another awesome ENiGMA½ BBS' } paths: { @@ -274,6 +289,16 @@ } } + chatServers: { + // multi relay chat settings. No need to sign up, just enable it. + // More info: https://bbswiki.bottomlessabyss.net/index.php?title=MRC_Chat_platform + mrc: { + enabled : false + serverHostname : 'mrc.bottomlessabyss.net' + serverPort : 5000 + } + } + // // Currently, ENiGMA½ can use external email to mail // users for password resets. Additional functionality will From 19e10bb09656983c267578a2fd6c7d1780c025f3 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 21:19:29 +0100 Subject: [PATCH 17/36] Tweak to MRC handshake --- core/servers/chat/mrc_multiplexer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 641353c2..2b7c54a3 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -47,7 +47,7 @@ exports.getModule = class MrcModule extends ServerModule { const boardName = config.general.prettyBoardName || config.general.boardName; const enigmaVersion = 'ENiGMA½-BBS_' + require('../../../package.json').version; - const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${protocolVersion}`; + const handshake = `${boardName}~${enigmaVersion}/${os.platform()}.${os.arch()}/${protocolVersion}`; this.log.debug({ handshake : handshake }, 'Handshaking with MRC server'); this.sendRaw(handshake); From b6d0d0d95e53972f7947272423f8fb17ed627efe Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 21:20:18 +0100 Subject: [PATCH 18/36] Move generic config options to general config section --- core/config.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core/config.js b/core/config.js index 37dfbd90..a4762e91 100644 --- a/core/config.js +++ b/core/config.js @@ -169,6 +169,11 @@ function getDefaultConfig() { return { general : { boardName : 'Another Fine ENiGMA½ BBS', + prettyBoardName : '|08A|07nother |07F|08ine |07E|08NiGMA|07½ B|08BS', + telnetHostname : '', + sshHostname : '', + website : 'https://enigma-bbs.github.io', + description : 'An ENiGMA½ BBS', // :TODO: closedSystem prob belongs under users{}? closedSystem : false, // is the system closed to new users? @@ -454,15 +459,8 @@ function getDefaultConfig() { enabled : true, serverHostname : 'mrc.bottomlessabyss.net', serverPort : 5000, + retryDelay : 10000, multiplexerPort : 5000, - bbsInfo : { - sysop : '', - telnet : '', - website : '', - ssh : '', - description : '', - - } } }, From 8153473b89f4dfab160bf7b74c35e4cee6315be8 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 22:00:48 +0100 Subject: [PATCH 19/36] Dial down MRC logging --- core/mrc.js | 2 -- core/servers/chat/mrc_multiplexer.js | 19 ++++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 172466a9..bca11656 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -396,9 +396,7 @@ exports.getModule = class mrcModule extends MenuModule { body: body }; - this.log.debug({ message: message }, 'Sending message to MRC multiplexer'); // TODO: check socket still exists here - this.state.socket.write(JSON.stringify(message) + '\n'); } diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 2b7c54a3..69b3b248 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -34,11 +34,12 @@ exports.getModule = class MrcModule extends ServerModule { super(); this.log = Log.child( { server : 'MRC' } ); - this.config = Config(); + + const config = Config(); this.mrcConnectOpts = { - host : this.config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', - port : this.config.chatServers.mrc.serverPort || 5000, - retryDelay : this.config.chatServers.mrc.retryDelay || 10000 + host : config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', + port : config.chatServers.mrc.serverPort || 5000, + retryDelay : config.chatServers.mrc.retryDelay || 10000 }; } @@ -78,7 +79,7 @@ exports.getModule = class MrcModule extends ServerModule { this.log.warn( { port : config.chatServers.mrc.multiplexerPort, server : ModuleInfo.name }, 'Invalid port' ); return cb(Errors.Invalid(`Invalid port: ${config.chatServers.mrc.multiplexerPort}`)); } - Log.info( { server : ModuleInfo.name, port : config.chatServers.mrc.multiplexerPort }, 'MRC multiplexer local listener starting up'); + Log.info( { server : ModuleInfo.name, port : config.chatServers.mrc.multiplexerPort }, 'MRC multiplexer starting up'); return this.server.listen(port, cb); } @@ -135,11 +136,10 @@ exports.getModule = class MrcModule extends ServerModule { if (this.mrcClient && this.mrcClient.requestedDisconnect) return; + this.log.info(this.mrcConnectOpts, 'Disconnected from MRC server, reconnecting'); - this.log.debug('Waiting ' + this.mrcConnectOpts.retryDelay + 'ms before retrying'); - setTimeout(function() { self.connectToMrc(); }, this.mrcConnectOpts.retryDelay); @@ -199,10 +199,8 @@ exports.getModule = class MrcModule extends ServerModule { sendToClient(message) { connectedSockets.forEach( (client) => { if (message.to_user == '' || message.to_user == client.username || message.to_user == 'CLIENT' || message.from_user == client.username || message.to_user == 'NOTME' ) { - this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user'); + // this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user'); client.write(JSON.stringify(message) + '\n'); - } else { - this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Not forwarding message'); } }); } @@ -225,7 +223,6 @@ exports.getModule = class MrcModule extends ServerModule { } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { // reply to heartbeat - // this.log.debug('Respond to heartbeat'); this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); } else { From a97c5bbe822f3ab3f4055c40a88a446a2f14c978 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 22:18:35 +0100 Subject: [PATCH 20/36] Add extra line to MRC chat area --- art/themes/luciano_blocktronics/mrc.ans | Bin 881 -> 721 bytes art/themes/luciano_blocktronics/theme.hjson | 32 +++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/art/themes/luciano_blocktronics/mrc.ans b/art/themes/luciano_blocktronics/mrc.ans index a1bbd030f30251860bafab4e8e98625373ec9843..a3079f3c879ba6ccb6f8aac3571711279406892e 100644 GIT binary patch delta 163 zcmey!c9C^MFrx>TbhLrBu|ckMw7I#nbhKga{X1OufgCgE`&{?$02!vvcerlfQLt42 zD$G@N4Kb3AHnRY!1nIhSpX>H*u%gNGOsc{_HBjL@_Z9AdbXZSLV^U*{x~04QW)0u#D_hwDB_B}mlF z2q+5H3e{<709VF!{|;18aa73qpA|n z^|>I=806l$f9F0(50D4;5ZFyX&g9FCYV1&n$^RHtd5{%NR$*#a<6vfBU}OwnkYE4< QVNWMtg)nzV2oFvI0HiZg?EnA( diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 49d459f6..0e21329c 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -1087,7 +1087,7 @@ mci: { MT1: { width: 72 - height: 18 + height: 19 } ET2: { width: 69 // fnarr! @@ -1109,6 +1109,36 @@ } } + irc: { + config: { + messageFormat: "|00|10<|02{fromUserName}|10>|00 |03{message}|00" + privateMessageFormat: "|00|10<|02{fromUserName}|15->{toUserName}|10>|00 |03{message}|00" + } + 0: { + mci: { + MT1: { + width: 72 + height: 17 + } + ET2: { + width: 69 // fnarr! + maxLength: 140 + } + TL3: { + width: 20 + } + TL4: { + width: 20 + } + TL5: { + width: 2 + } + TL6: { + width: 2 + } + } + } + } } prompts: { From 9cc59f8ff8829db4a73cdcfdd00749992c5c90f6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 1 Jun 2019 14:38:48 -0600 Subject: [PATCH 21/36] Hopefully a little better handling of external doors/processes --- core/door.js | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/core/door.js b/core/door.js index b07c89bc..c8dd3796 100644 --- a/core/door.js +++ b/core/door.js @@ -70,14 +70,13 @@ module.exports = class Door { const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) ); - this.client.log.debug( + this.client.log.info( { cmd : exeInfo.cmd, args, io : this.io }, - 'Executing door' + 'Executing external door process' ); - let door; try { - door = pty.spawn(exeInfo.cmd, args, { + this.doorPty = pty.spawn(exeInfo.cmd, args, { cols : this.client.term.termWidth, rows : this.client.term.termHeight, cwd : cwd, @@ -88,15 +87,19 @@ module.exports = class Door { return cb(e); } + this.client.log.debug( + { processId : this.doorPty.pid }, 'External door process spawned' + ); + if('stdio' === this.io) { this.client.log.debug('Using stdio for door I/O'); - this.client.term.output.pipe(door); + this.client.term.output.pipe(this.doorPty); - door.on('data', this.doorDataHandler.bind(this)); + this.doorPty.on('data', this.doorDataHandler.bind(this)); - door.once('close', () => { - return this.restoreIo(door); + this.doorPty.once('close', () => { + return this.restoreIo(this.doorPty); }); } else if('socket' === this.io) { this.client.log.debug( @@ -105,7 +108,7 @@ module.exports = class Door { ); } - door.once('exit', exitCode => { + this.doorPty.once('exit', exitCode => { this.client.log.info( { exitCode : exitCode }, 'Door exited'); if(this.sockServer) { @@ -114,10 +117,11 @@ module.exports = class Door { // we may not get a close if('stdio' === this.io) { - this.restoreIo(door); + this.restoreIo(this.doorPty); } - door.removeAllListeners(); + this.doorPty.removeAllListeners(); + delete this.doorPty; return cb(null); }); @@ -128,9 +132,15 @@ module.exports = class Door { } restoreIo(piped) { - if(!this.restored && this.client.term.output) { - this.client.term.output.unpipe(piped); - this.client.term.output.resume(); + if(!this.restored) { + if(this.doorPty) { + this.doorPty.kill(); + } + + if(this.client.term.output) { + this.client.term.output.unpipe(piped); + this.client.term.output.resume(); + } this.restored = true; } } From 5d73a224be55ce011294b2d6305c1c1e57063c4c Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 2 Jun 2019 15:24:08 +0100 Subject: [PATCH 22/36] Compress MRC help text --- core/mrc.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index bca11656..6fd8ac35 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -49,17 +49,14 @@ var MciViewIds = { // TODO: this is a bit shit, could maybe do it with an ansi instead const helpText = ` General Chat: -/rooms - List of current rooms -/join - Join a room +/rooms & /join - Send a private message -/clear - Clear the chat log ---- /whoon - Who's on what BBS /chatters - Who's in what room -/topic - Set the topic -/meetups - MRC MeetUps -/bbses - BBS's connected -/info - Info about specific BBS +/topic - Set the room topic +/bbses & /info - Info about BBS's connected +/meetups - Info about MRC MeetUps --- /l33t - l337 5p34k /kewl - BBS KeWL SPeaK From 8726128cda8e96ba8128f2d0457f490af0d7f933 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 2 Jun 2019 15:47:15 +0100 Subject: [PATCH 23/36] Turn off MRC by default --- core/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index a4762e91..c8a4c46a 100644 --- a/core/config.js +++ b/core/config.js @@ -456,7 +456,7 @@ function getDefaultConfig() { chatServers : { mrc: { - enabled : true, + enabled : false, serverHostname : 'mrc.bottomlessabyss.net', serverPort : 5000, retryDelay : 10000, From 43e22cfc742d0629b1b1ea89cfad21642aeeb5ff Mon Sep 17 00:00:00 2001 From: David Stephens Date: Wed, 5 Jun 2019 23:41:28 +0100 Subject: [PATCH 24/36] Turn off debug logging of MRC message sending --- core/servers/chat/mrc_multiplexer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 69b3b248..9b976eff 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -276,7 +276,7 @@ exports.getModule = class MrcModule extends ServerModule { sanitiseMessage(messageBody) ].join('~') + '~'; - Log.debug({ server : 'MRC', data : line }, 'Sending data'); + // Log.debug({ server : 'MRC', data : line }, 'Sending data'); this.sendRaw(line); } From c79e2bc97118d41e7c665a4b3730fa07841e5eba Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 5 Jun 2019 22:42:20 -0600 Subject: [PATCH 25/36] Remove menu.hjson - use config templates --- config/menu.hjson | 4237 --------------------------------------------- 1 file changed, 4237 deletions(-) delete mode 100644 config/menu.hjson diff --git a/config/menu.hjson b/config/menu.hjson deleted file mode 100644 index 99585ade..00000000 --- a/config/menu.hjson +++ /dev/null @@ -1,4237 +0,0 @@ -{ - /* - ./\/\.' ENiGMA½ Menu Configuration -/--/-------- - -- - - - _____________________ _____ ____________________ __________\_ / - \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! - // __|___// | \// |// | \// | | \// \ /___ /_____ - /____ _____| __________ ___|__| ____| \ / _____ \ - ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ - /__ _\ - <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ - - *-----------------------------------------------------------------------------* - - General Information - ------------------------------- - - - This configuration is in HJSON (http://hjson.org/) format. Strict to-spec - JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. - - See http://hjson.org/ for more information and syntax. - - Various editors and IDEs such as Sublime Text 3, Visual Studio Code, and so - on have syntax highlighting for the HJSON format which are highly recommended. - - ------------------------------- -- - - - Menu Configuration - ------------------------------- - - - ENiGMA½ makes no assumptions about specific menu types (main, doors, etc.), - but instead allows full customization of all menus throughout the system. - Some menus such as a main menu are considered "standard" while others are - backed by a specific module. SysOps can tweak various settings about these - modules (look & feel, keyboard interation, and so on) or even fully replace - the module with something else. - - This file starts out as an example setup. Look at the examples, change - settings, menu ordering/flow, add/remove menus, implement ACS control, - etc.! - - Remember you can *live edit* this file. That is, make a change and save - while you're logged into the system and it will take effect on the next - menu change or screen refresh. - - Please see RTFM ...er, uh... see the documentation for more information, and - don't be shy to ask for help: - - BBS : Xibalba @ xibalba.l33t.codes - FTN : BBS Discussion on fsxNet - IRC : #enigma-bbs / FreeNode - Email : bryan@l33t.codes - */ - menus: { - // - // Send telnet connections to matrix where users can login, apply, etc. - // - telnetConnected: { - art: CONNECT - next: matrix - config: { nextTimeout: 1500 } - } - - // - // SSH connections are pre-authenticated via the SSH server itself. - // Jump directly to the login sequence - // - sshConnected: { - art: CONNECT - next: fullLoginSequenceLoginArt - config: { nextTimeout: 1500 } - } - - // - // Another SSH specialization: If the user logs in with a new user - // name (e.g. "new", "apply", ...) they will be directed to the - // application process. - // - sshConnectedNewUser: { - art: CONNECT - next: newUserApplicationPreSsh - config: { nextTimeout: 1500 } - } - - // Ye ol' standard matrix - matrix: { - art: matrix - form: { - 0: { - VM: { - mci: { - VM1: { - submit: true - focus: true - argName: navSelect - // - // To enable forgot password, you will need to have the web server - // enabled and mail/SMTP configured. Once that is in place, swap out - // the commented lines below as well as in the submit block - // - items: [ - { - text: login - data: login - } - { - text: apply - data: apply - } - { - text: forgot pass - data: forgot - } - { - text: log off - data: logoff - } - ] - } - } - submit: { - *: [ - { - value: { navSelect: "login" } - action: @menu:login - } - { - value: { navSelect: "apply" } - action: @menu:newUserApplicationPre - } - { - value: { navSelect: "forgot" } - action: @menu:forgotPassword - } - { - value: { navSelect: "logoff" } - action: @menu:logoff - } - ] - } - } - } - } - } - - login: { - art: USERLOG - next: fullLoginSequenceLoginArt - config: { - tooNodeMenu: loginAttemptTooNode - inactive: loginAttemptAccountInactive - disabled: loginAttemptAccountDisabled - locked: loginAttemptAccountLocked - } - form: { - 0: { - mci: { - ET1: { - maxLength: @config:users.usernameMax - argName: username - focus: true - } - ET2: { - password: true - maxLength: @config:users.passwordMax - argName: password - submit: true - } - } - submit: { - *: [ - { - value: { password: null } - action: @systemMethod:login - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - loginAttemptTooNode: { - art: TOONODE - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - loginAttemptAccountLocked: { - art: ACCOUNTLOCKED - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - loginAttemptAccountDisabled: { - art: ACCOUNTDISABLED - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - loginAttemptAccountInactive: { - art: ACCOUNTINACTIVE - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - forgotPassword: { - desc: Forgot password - prompt: forgotPasswordPrompt - submit: [ - { - value: { username: null } - action: @systemMethod:sendForgotPasswordEmail - extraArgs: { next: "forgotPasswordSubmitted" } - } - ] - } - - forgotPasswordSubmitted: { - desc: Forgot password - art: FORGOTPWSENT - config: { - cls: true - pause: true - } - next: @systemMethod:logoff - } - - // :TODO: Prompt Yes/No for logoff confirm - fullLogoffSequence: { - desc: Logging Off - prompt: logoffConfirmation - submit: [ - { - value: { promptValue: 0 } - action: @menu:fullLogoffSequencePreAd - } - { - value: { promptValue: 1 } - action: @systemMethod:prevMenu - } - ] - } - - fullLogoffSequencePreAd: { - art: PRELOGAD - desc: Logging Off - next: fullLogoffSequenceRandomBoardAd - config: { - cls: true - nextTimeout: 1500 - } - } - - fullLogoffSequenceRandomBoardAd: { - art: OTHRBBS - desc: Logging Off - next: logoff - config: { - baudRate: 57600 - pause: true - cls: true - } - } - - logoff: { - art: LOGOFF - desc: Logging Off - next: @systemMethod:logoff - } - - // A quick preamble - defaults to warning about broken terminals - newUserApplicationPre: { - art: NEWUSER1 - next: newUserApplication - desc: Applying - config: { - pause: true - cls: true - menuFlags: [ "noHistory" ] - } - } - - newUserApplication: { - module: nua - art: NUA - next: [ - { - // Initial SysOp does not send feedback to themselves - acs: ID1 - next: fullLoginSequenceLoginArt - } - { - // ...everyone else does - next: newUserFeedbackToSysOpPreamble - } - ] - form: { - 0: { - mci: { - ET1: { - focus: true - argName: username - maxLength: @config:users.usernameMax - validate: @systemMethod:validateUserNameAvail - } - ET2: { - argName: realName - maxLength: @config:users.realNameMax - validate: @systemMethod:validateNonEmpty - } - MET3: { - argName: birthdate - maskPattern: "####/##/##" - validate: @systemMethod:validateBirthdate - } - ME4: { - argName: sex - maskPattern: A - textStyle: upper - validate: @systemMethod:validateNonEmpty - } - ET5: { - argName: location - maxLength: @config:users.locationMax - validate: @systemMethod:validateNonEmpty - } - ET6: { - argName: affils - maxLength: @config:users.affilsMax - } - ET7: { - argName: email - maxLength: @config:users.emailMax - validate: @systemMethod:validateEmailAvail - } - ET8: { - argName: web - maxLength: @config:users.webMax - } - ET9: { - 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 - items: [ "apply", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { "submission" : 0 } - action: @method:submitApplication - extraArgs: { - inactive: userNeedsActivated - error: newUserCreateError - } - } - { - value: { "submission" : 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - // A quick preamble - defaults to warning about broken terminals (SSH version) - newUserApplicationPreSsh: { - art: NEWUSER1 - next: newUserApplicationSsh - desc: Applying - config: { - pause: true - cls: true - menuFlags: [ "noHistory" ] - } - } - - // - // SSH specialization of NUA - // Canceling this form logs off vs falling back to matrix - // - newUserApplicationSsh: { - module: nua - art: NUA - fallback: logoff - next: newUserFeedbackToSysOpPreamble - form: { - 0: { - mci: { - ET1: { - focus: true - argName: username - maxLength: @config:users.usernameMax - validate: @systemMethod:validateUserNameAvail - } - ET2: { - argName: realName - maxLength: @config:users.realNameMax - validate: @systemMethod:validateNonEmpty - } - MET3: { - argName: birthdate - maskPattern: "####/##/##" - validate: @systemMethod:validateBirthdate - } - ME4: { - argName: sex - maskPattern: A - textStyle: upper - validate: @systemMethod:validateNonEmpty - } - ET5: { - argName: location - maxLength: @config:users.locationMax - validate: @systemMethod:validateNonEmpty - } - ET6: { - argName: affils - maxLength: @config:users.affilsMax - } - ET7: { - argName: email - maxLength: @config:users.emailMax - validate: @systemMethod:validateEmailAvail - } - ET8: { - argName: web - maxLength: @config:users.webMax - } - ET9: { - 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 - items: [ "apply", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { "submission" : 0 } - action: @method:submitApplication - extraArgs: { - inactive: userNeedsActivated - error: newUserCreateError - } - } - { - value: { "submission" : 1 } - action: @systemMethod:logoff - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:logoff - } - ] - } - } - } - - newUserFeedbackToSysOpPreamble: { - art: LETTER - config: { pause: true } - next: newUserFeedbackToSysOp - } - - newUserFeedbackToSysOp: { - desc: Feedback to SysOp - module: msg_area_post_fse - next: [ - { - acs: AS2 - next: fullLoginSequenceLoginArt - } - { - next: newUserInactiveDone - } - ] - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - }, - editorMode: edit - editorType: email - messageAreaTag: private_mail - toUserId: 1 /* always to +op */ - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - text: @sysStat:sysop_username - // :TODO: readOnly: true - } - ET3: { - argName: subject - maxLength: 72 - submit: true - text: New user feedback - validate: @systemMethod:validateMessageSubject - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - } - 1: { - mci: { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { value: "message", action: "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - }, - 2: { - TLTL: { - mci: { - TL1: { - width: 5 - } - TL2: { - width: 4 - } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - items: [ "save", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - } - } - } - - newUserInactiveDone: { - desc: Finished with NUA - art: DONE - config: { pause: true } - next: @menu:logoff - } - - fullLoginSequenceLoginArt: { - desc: Logging In - art: WELCOME - config: { pause: true } - next: fullLoginSequenceLastCallers - } - - fullLoginSequenceLastCallers: { - desc: Last Callers - module: last_callers - art: LASTCALL - config: { - pause: true - font: cp437 - } - next: fullLoginSequenceWhosOnline - } - fullLoginSequenceWhosOnline: { - desc: Who's Online - module: whos_online - art: WHOSON - config: { pause: true } - next: fullLoginSequenceOnelinerz - } - - fullLoginSequenceOnelinerz: { - desc: Viewing Onelinerz - module: onelinerz - next: [ - { - // calls >= 2 - acs: NC2 - next: fullLoginSequenceNewScanConfirm - } - { - // new users - skip new scan - next: fullLoginSequenceUserStats - } - ] - config: { - cls: true - art: { - view: ONELINER - add: ONEADD - } - } - form: { - 0: { - mci: { - VM1: { - focus: false - height: 10 - } - TM2: { - argName: addOrExit - items: [ "yeah!", "nah" ] - "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } - submit: true - focus: true - } - } - submit: { - *: [ - { - value: { addOrExit: 0 } - action: @method:viewAddScreen - } - { - value: { addOrExit: null } - action: @systemMethod:nextMenu - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:nextMenu - } - ] - }, - 1: { - mci: { - ET1: { - focus: true - maxLength: 70 - argName: oneliner - } - TL2: { - width: 60 - } - TM3: { - argName: addOrCancel - items: [ "add", "cancel" ] - "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } - submit: true - } - } - - submit: { - *: [ - { - value: { addOrCancel: 0 } - action: @method:addEntry - } - { - value: { addOrCancel: 1 } - action: @method:cancelAdd - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelAdd - } - ] - } - } - } - - fullLoginSequenceNewScanConfirm: { - desc: Logging In - prompt: loginGlobalNewScan - submit: [ - { - value: { promptValue: 0 } - action: @menu:fullLoginSequenceNewScan - } - { - value: { promptValue: 1 } - action: @menu:fullLoginSequenceUserStats - } - ] - } - - fullLoginSequenceNewScan: { - desc: Performing New Scan - module: new_scan - art: NEWSCAN - next: fullLoginSequenceSysStats - config: { - messageListMenu: newScanMessageList - } - } - - fullLoginSequenceSysStats: { - desc: System Stats - art: SYSSTAT - config: { pause: true } - next: fullLoginSequenceUserStats - } - fullLoginSequenceUserStats: { - desc: User Stats - art: STATUS - config: { pause: true } - next: mainMenu - } - - newScanMessageList: { - desc: New Messages - module: msg_list - art: NEWMSGS - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - TL6: { - // theme me! - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "x", "shift + x" ] - action: @method:fullExit - } - { - keys: [ "m", "shift + m" ] - action: @method:markAllRead - } - ] - } - } - } - - newScanFileBaseList: { - module: file_area_list - desc: New Files - config: { - art: { - browse: FNEWBRWSE - details: FDETAIL - detailsGeneral: FDETGEN - detailsNfo: FDETNFO - detailsFileList: FDETLST - help: FBHELP - } - } - form: { - 0: { - mci: { - MT1: { - mode: preview - ansiView: true - } - - HM2: { - focus: true - submit: true - argName: navSelect - items: [ - "prev", "next", "details", "toggle queue", "rate", "help", "quit" - ] - focusItemIndex: 1 - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:prevFile - } - { - value: { navSelect: 1 } - action: @method:nextFile - } - { - value: { navSelect: 2 } - action: @method:viewDetails - } - { - value: { navSelect: 3 } - action: @method:toggleQueue - } - { - value: { navSelect: 4 } - action: @menu:fileBaseGetRatingForSelectedEntry - } - { - value: { navSelect: 5 } - action: @method:displayHelp - } - { - value: { navSelect: 6 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "w", "shift + w" ] - action: @method:showWebDownloadLink - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "t", "shift + t" ] - action: @method:toggleQueue - } - { - keys: [ "v", "shift + v" ] - action: @method:viewDetails - } - { - keys: [ "r", "shift + r" ] - action: @menu:fileBaseGetRatingForSelectedEntry - } - { - keys: [ "?" ] - action: @method:displayHelp - } - ] - } - - 1: { - mci: { - HM1: { - focus: true - submit: true - argName: navSelect - items: [ - "general", "nfo/readme", "file listing" - ] - } - } - - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @method:detailsQuit - } - ] - } - - 2: { - // details - general - mci: {} - } - - 3: { - // details - nfo/readme - mci: { - MT1: { - mode: preview - } - } - } - - 4: { - // details - file listing - mci: { - VM1: { - - } - } - } - } - } - - /////////////////////////////////////////////////////////////////////// - // Main Menu - /////////////////////////////////////////////////////////////////////// - mainMenu: { - art: MMENU - desc: Main Menu - prompt: menuCommand - config: { - font: cp437 - interrupt: realtime - } - submit: [ - { - value: { command: "MSG" } - action: @menu:nodeMessage - } - { - value: { command: "MRC" } - action: @menu:mrc - } - { - value: { command: "G" } - action: @menu:fullLogoffSequence - } - { - value: { command: "D" } - action: @menu:doorMenu - } - { - value: { command: "F" } - action: @menu:fileBase - } - { - value: { command: "U" } - action: @menu:mainMenuUserList - } - { - value: { command: "L" } - action: @menu:mainMenuLastCallers - } - { - value: { command: "W" } - action: @menu:mainMenuWhosOnline - } - { - value: { command: "Y" } - action: @menu:mainMenuUserStats - } - { - value: { command: "M" } - action: @menu:messageArea - } - { - value: { command: "E" } - action: @menu:mailMenu - } - { - value: { command: "C" } - action: @menu:mainMenuUserConfig - } - { - value: { command: "S" } - action: @menu:mainMenuSystemStats - } - { - value: { command: "!" } - action: @menu:mainMenuGlobalNewScan - } - { - value: { command: "K" } - action: @menu:mainMenuFeedbackToSysOp - } - { - value: { command: "O" } - action: @menu:mainMenuOnelinerz - } - { - value: { command: "R" } - action: @menu:mainMenuRumorz - } - { - value: { command: "BBS"} - action: @menu:bbsList - } - { - value: { command: "UA" } - action: @menu:mainMenuUserAchievementsEarned - } - { - value: 1 - action: @menu:mainMenu - } - ] - } - - mainMenuUserAchievementsEarned: { - desc: Achievements - module: user_achievements_earned - art: USERACHIEV - form: { - 0: { - mci: { - VM1: { - focus: true - } - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - ercClient: { - art: erc - module: erc_client - config: { - host: localhost - port: 5001 - bbsTag: CHANGEME - } - - form: { - 0: { - mci: { - MT1: { - width: 79 - height: 21 - mode: preview - autoScroll: true - } - ET3: { - autoScale: false - width: 77 - argName: inputArea - focus: true - submit: true - } - } - - submit: { - *: [ - { - value: { inputArea: null } - action: @method:inputAreaSubmit - } - ] - } - actionKeys: [ - { - keys: [ "tab" ] - } - { - keys: [ "up arrow" ] - action: @method:scrollDown - } - { - keys: [ "down arrow" ] - action: @method:scrollUp - } - ] - } - } - } - - mrc: { - desc: MRC Chat - module: mrc - art: MRC - config: { - cls: true - } - form: { - 0: { - mci: { - MT1: { - mode: preview - autoScroll: true - } - ET2: { - argName: inputArea - submit: true - focus: true - } - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:quit - } - { - keys: [ "down arrow", "up arrow", "page up", "page down" ] - action: @method:movementKeyPressed - } - ] - submit: { - *: [ - { - value: { inputArea: null } - action: @method:sendChatMessage - } - ] - } - } - } - } - - nodeMessage: { - desc: Node Messaging - module: node_msg - art: NODEMSG - config: { - cls: true - art: { - header: NODEMSGHDR - footer: NODEMSGFTR - } - } - form: { - 0: { - mci: { - SM1: { - argName: node - } - ET2: { - argName: message - submit: true - } - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - submit: { - *: [ - { - value: { message: null } - action: @method:sendMessage - } - ] - } - } - } - } - - mainMenuLastCallers: { - desc: Last Callers - module: last_callers - art: LASTCALL - config: { pause: true } - } - - mainMenuWhosOnline: { - desc: Who's Online - module: whos_online - art: WHOSON - config: { pause: true } - } - - mainMenuUserStats: { - desc: User Stats - art: STATUS - config: { pause: true } - } - - mainMenuSystemStats: { - desc: System Stats - art: SYSSTAT - config: { pause: true } - } - - mainMenuUserList: { - desc: User Listing - module: user_list - art: USERLST - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - } - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - mainMenuUserConfig: { - module: user_config - art: CONFSCR - form: { - 0: { - mci: { - ET1: { - argName: realName - maxLength: @config:users.realNameMax - validate: @systemMethod:validateNonEmpty - focus: true - } - ME2: { - argName: birthdate - maskPattern: "####/##/##" - } - ME3: { - argName: sex - maskPattern: A - textStyle: upper - validate: @systemMethod:validateNonEmpty - } - ET4: { - argName: location - maxLength: @config:users.locationMax - validate: @systemMethod:validateNonEmpty - } - ET5: { - argName: affils - maxLength: @config:users.affilsMax - } - ET6: { - argName: email - maxLength: @config:users.emailMax - validate: @method:validateEmailAvail - } - ET7: { - argName: web - maxLength: @config:users.webMax - } - ME8: { - maskPattern: "##" - argName: termHeight - validate: @systemMethod:validateNonEmpty - } - SM9: { - argName: theme - } - ET10: { - argName: password - maxLength: @config:users.passwordMax - password: true - validate: @method:validatePassword - } - ET11: { - argName: passwordConfirm - maxLength: @config:users.passwordMax - password: true - validate: @method:validatePassConfirmMatch - } - TM25: { - argName: submission - items: [ "save", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { submission: 0 } - action: @method:saveChanges - } - { - value: { submission: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - mainMenuGlobalNewScan: { - desc: Performing New Scan - module: new_scan - art: NEWSCAN - config: { - messageListMenu: newScanMessageList - } - } - - mainMenuFeedbackToSysOp: { - desc: Feedback to SysOp - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - }, - editorMode: edit - editorType: email - messageAreaTag: private_mail - toUserId: 1 /* always to +op */ - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - text: @sysStat:sysop_username - // :TODO: readOnly: true - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateMessageSubject - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { value: "message", action: "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - }, - 2: { - TLTL: { - mci: { - TL1: { - width: 5 - } - TL2: { - width: 4 - } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - items: [ "save", "discard", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - } - } - } - - mainMenuOnelinerz: { - desc: Viewing Onelinerz - module: onelinerz - config: { - cls: true - art: { - view: ONELINER - add: ONEADD - } - } - form: { - 0: { - mci: { - VM1: { - focus: false - height: 10 - } - TM2: { - argName: addOrExit - items: [ "yeah!", "nah" ] - "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } - submit: true - focus: true - } - } - submit: { - *: [ - { - value: { addOrExit: 0 } - action: @method:viewAddScreen - } - { - value: { addOrExit: null } - action: @systemMethod:nextMenu - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:nextMenu - } - ] - }, - 1: { - mci: { - ET1: { - focus: true - maxLength: 70 - argName: oneliner - } - TL2: { - width: 60 - } - TM3: { - argName: addOrCancel - items: [ "add", "cancel" ] - "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } - submit: true - } - } - - submit: { - *: [ - { - value: { addOrCancel: 0 } - action: @method:addEntry - } - { - value: { addOrCancel: 1 } - action: @method:cancelAdd - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelAdd - } - ] - } - } - } - - mainMenuRumorz: { - desc: Rumorz - module: rumorz - config: { - cls: true - art: { - entries: RUMORS - add: RUMORADD - } - } - form: { - 0: { - mci: { - VM1: { - focus: false - height: 10 - } - TM2: { - argName: addOrExit - items: [ "yeah!", "nah" ] - "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } - submit: true - focus: true - } - } - submit: { - *: [ - { - value: { addOrExit: 0 } - action: @method:viewAddScreen - } - { - value: { addOrExit: null } - action: @systemMethod:nextMenu - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:nextMenu - } - ] - }, - 1: { - mci: { - ET1: { - focus: true - maxLength: 70 - argName: rumor - } - TL2: { - width: 60 - } - TM3: { - argName: addOrCancel - items: [ "add", "cancel" ] - "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } - submit: true - } - } - - submit: { - *: [ - { - value: { addOrCancel: 0 } - action: @method:addEntry - } - { - value: { addOrCancel: 1 } - action: @method:cancelAdd - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelAdd - } - ] - } - } - } - - bbsList: { - desc: Viewing BBS List - module: bbs_list - config: { - cls: true - art: { - entries: BBSLIST - add: BBSADD - } - } - - form: { - 0: { - mci: { - VM1: { maxLength: 32 } - TL2: { maxLength: 32 } - TL3: { maxLength: 32 } - TL4: { maxLength: 32 } - TL5: { maxLength: 32 } - TL6: { maxLength: 32 } - TL7: { maxLength: 32 } - TL8: { maxLength: 32 } - TL9: { maxLength: 32 } - } - actionKeys: [ - { - keys: [ "a" ] - action: @method:addBBS - } - { - // :TODO: add delete key - keys: [ "d" ] - action: @method:deleteBBS - } - { - keys: [ "q", "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - ET1: { - argName: name - maxLength: 32 - validate: @systemMethod:validateNonEmpty - } - ET2: { - argName: sysop - maxLength: 32 - validate: @systemMethod:validateNonEmpty - } - ET3: { - argName: telnet - maxLength: 32 - validate: @systemMethod:validateNonEmpty - } - ET4: { - argName: www - maxLength: 32 - } - ET5: { - argName: location - maxLength: 32 - } - ET6: { - argName: software - maxLength: 32 - } - ET7: { - argName: notes - maxLength: 32 - } - TM17: { - argName: submission - items: [ "save", "cancel" ] - submit: true - } - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelSubmit - } - ] - - submit: { - *: [ - { - value: { "submission" : 0 } - action: @method:submitBBS - } - { - value: { "submission" : 1 } - action: @method:cancelSubmit - } - ] - } - } - } - } - - /////////////////////////////////////////////////////////////////////// - // Doors Menu - /////////////////////////////////////////////////////////////////////// - doorMenu: { - desc: Doors Menu - art: DOORMNU - prompt: menuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { command: "G" } - action: @menu:logoff - } - { - value: { command: "Q" } - action: @systemMethod:prevMenu - } - // - // The system supports many ways of launching doors including - // modules for DoorParty!, BBSLink, etc. - // - // Below are some examples. See the documentation for more info. - // - { - value: { command: "ABRACADABRA" } - action: @menu:doorAbracadabraExample - } - { - value: { command: "TWBBSLINK" } - action: @menu:doorTradeWars2002BBSLinkExample - } - { - value: { command: "DP" } - action: @menu:doorPartyExample - } - { - value: { command: "CN" } - action: @menu:doorCombatNetExample - } - { - value: { command: "EXODUS" } - action: @menu:doorExodusCataclysm - } - ] - } - - // - // Local Door Example via abracadabra module - // - // This example assumes launch_door.sh (which is passed args) - // launches the door. - // - doorAbracadabraExample: { - desc: Abracadabra Example - module: abracadabra - config: { - name: Example Door - dropFileType: DORINFO - cmd: /home/enigma/DOS/scripts/launch_door.sh - args: [ - "{node}", - "{dropFile}", - "{srvPort}", - ], - nodeMax: 1 - tooManyArt: DOORMANY - io: socket - } - } - - // - // BBSLink Example (TradeWars 2000) - // - // Register @ https://bbslink.net/ - // - doorTradeWars2002BBSLinkExample: { - desc: Playing TW 2002 (BBSLink) - module: bbs_link - config: { - sysCode: XXXXXXXX - authCode: XXXXXXXX - schemeCode: XXXXXXXX - door: tw - } - } - - // - // DoorParty! Example - // - // Register @ http://throwbackbbs.com/ - // - doorPartyExample: { - desc: Using DoorParty! - module: door_party - config: { - username: XXXXXXXX - password: XXXXXXXX - bbsTag: XX - } - } - - // - // CombatNet Example - // - // Register @ http://combatnet.us/ - // - doorCombatNetExample: { - desc: Using CombatNet - module: combatnet - config: { - bbsTag: CBNxxx - password: XXXXXXXXX - } - } - - // - // Exodus Example (cataclysm) - // Register @ https://oddnetwork.org/exodus/ - // - doorExodusCataclysm: { - desc: Cataclysm - module: exodus - config: { - rejectUnauthorized: false - board: XXX - key: XXXXXXXX - door: cataclysm - } - } - - /////////////////////////////////////////////////////////////////////// - // Message Area Menu - /////////////////////////////////////////////////////////////////////// - messageArea: { - art: MSGMNU - desc: Message Area - prompt: messageMenuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { command: "P" } - action: @menu:messageAreaNewPost - } - { - value: { command: "J" } - action: @menu:messageAreaChangeCurrentConference - } - { - value: { command: "C" } - action: @menu:messageAreaChangeCurrentArea - } - { - value: { command: "L" } - action: @menu:messageAreaMessageList - } - { - value: { command: "Q" } - action: @systemMethod:prevMenu - } - { - value: { command: "G" } - action: @menu:fullLogoffSequence - } - { - value: { command: "<" } - action: @systemMethod:prevConf - } - { - value: { command: ">" } - action: @systemMethod:nextConf - } - { - value: { command: "[" } - action: @systemMethod:prevArea - } - { - value: { command: "]" } - action: @systemMethod:nextArea - } - { - value: { command: "D" } - action: @menu:messageAreaSetNewScanDate - } - { - value: { command: "S" } - action: @menu:messageSearch - } - { - value: 1 - action: @menu:messageArea - } - ] - } - - messageSearch: { - desc: Message Search - module: message_base_search - art: MSEARCH - config: { - messageListMenu: messageAreaSearchMessageList - } - form: { - 0: { - mci: { - ET1: { - focus: true - argName: searchTerms - } - BT2: { - argName: search - text: search - submit: true - } - SM3: { - argName: confTag - } - SM4: { - argName: areaTag - } - ET5: { - argName: toUserName - maxLength: @config:users.usernameMax - } - ET6: { - argName: fromUserName - maxLength: @config:users.usernameMax - } - BT7: { - argName: advancedSearch - text: advanced search - submit: true - } - } - - submit: { - *: [ - { - value: { search: null } - action: @method:search - } - { - value: { advancedSearch: null } - action: @method:search - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaSearchMessageList: { - desc: Message Search - module: msg_list - art: MSRCHLST - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - TL6: { - // theme me! - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaMyMessagesList: { - desc: Personal Messages - module: msg_list - art: MYMSGLST - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageSearchNoResults: { - desc: Message Search - art: MSRCNORES - config: { - pause: true - } - } - - messageAreaChangeCurrentConference: { - art: CCHANGE - module: msg_conf_list - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: conf - } - } - submit: { - *: [ - { - value: { conf: null } - action: @method:changeConference - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaSetNewScanDate: { - module: set_newscan_date - desc: Message Base - art: SETMNSDATE - config: { - target: message - scanDateFormat: YYYYMMDD - } - form: { - 0: { - mci: { - ME1: { - focus: true - submit: true - argName: scanDate - maskPattern: "####/##/##" - } - SM2: { - argName: targetSelection - submit: false - } - } - submit: { - *: [ - { - value: { scanDate: null } - action: @method:scanDateSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - changeMessageConfPreArt: { - module: show_art - config: { - method: messageConf - key: confTag - pause: true - cls: true - menuFlags: [ "popParent", "noHistory" ] - } - } - - messageAreaChangeCurrentArea: { - // :TODO: rename this art to ACHANGE - art: CHANGE - module: msg_area_list - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: area - } - } - submit: { - *: [ - { - value: { area: null } - action: @method:changeArea - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - changeMessageAreaPreArt: { - module: show_art - config: { - method: messageArea - key: areaTag - pause: true - cls: true - menuFlags: [ "popParent", "noHistory" ] - } - } - - messageAreaMessageList: { - module: msg_list - art: MSGLIST - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaViewPost: { - module: msg_area_view_fse - config: { - art: { - header: MSGVHDR - body: MSGBODY - footerView: MSGVFTR - help: MSGVHLP - }, - editorMode: view - editorType: area - } - form: { - 0: { - mci: { - // :TODO: ensure this block isn't even req. for theme to apply... - } - } - 1: { - mci: { - MT1: { - width: 79 - mode: preview - } - } - submit: { - *: [ - { - value: message - action: @method:editModeEscPressed - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - } - 2: { - TLTL: { - mci: { - TL1: { width: 5 } - TL2: { width: 4 } - } - } - } - 4: { - mci: { - HM1: { - // :TODO: (#)Jump/(L)Index (msg list)/Last - items: [ "prev", "next", "reply", "quit", "help" ] - focusItemIndex: 1 - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:prevMessage - } - { - value: { 1: 1 } - action: @method:nextMessage - } - { - value: { 1: 2 } - action: @method:replyMessage - extraArgs: { - menu: messageAreaReplyPost - } - } - { - value: { 1: 3 } - action: @systemMethod:prevMenu - } - { - value: { 1: 4 } - action: @method:viewModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "p", "shift + p" ] - action: @method:prevMessage - } - { - keys: [ "n", "shift + n" ] - action: @method:nextMessage - } - { - keys: [ "r", "shift + r" ] - action: @method:replyMessage - extraArgs: { - menu: messageAreaReplyPost - } - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "?" ] - action: @method:viewModeMenuHelp - } - { - keys: [ "down arrow", "up arrow", "page up", "page down" ] - action: @method:movementKeyPressed - } - ] - } - } - } - - messageAreaReplyPost: { - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - quote: MSGQUOT - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - } - editorMode: edit - editorType: area - } - form: { - 0: { - mci: { - // :TODO: use appropriate system properties for max lengths - TL1: { - argName: from - } - 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) - //width: 27 - //textOverflow: ... - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - MT1: { - width: 79 - height: 14 - argName: message - mode: edit - } - } - submit: { - *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ], - viewId: 1 - } - ] - } - - 3: { - mci: { - HM1: { - items: [ "save", "discard", "quote", "help" ] - } - } - - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 }, - action: @method:editModeMenuQuote - } - { - value: { 1: 3 } - action: @method:editModeMenuHelp - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "s", "shift + s" ] - action: @method:editModeMenuSave - } - { - keys: [ "d", "shift + d" ] - action: @systemMethod:prevMenu - } - { - keys: [ "q", "shift + q" ] - action: @method:editModeMenuQuote - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - - // Quote builder - 5: { - mci: { - MT1: { - width: 79 - height: 7 - } - VM3: { - width: 79 - height: 4 - argName: quote - } - } - - submit: { - *: [ - { - value: { quote: null } - action: @method:appendQuoteEntry - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @method:quoteBuilderEscPressed - } - ] - } - } - } - // :TODO: messageAreaSelect (change msg areas -> call @systemMethod -> fallback to menu - messageAreaNewPost: { - desc: Posting message, - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - } - editorMode: edit - editorType: area - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - 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 - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - - 1: { - "mci" : { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - } - 2: { - TLTL: { - mci: { - TL1: { width: 5 } - TL2: { width: 4 } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - "items" : [ "save", "discard", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - // :TODO: something like the following for overriding keymap - // this should only override specified entries. others will default - /* - "keyMap" : { - "accept" : [ "return" ] - } - */ - } - } - } - } - - - // - // User to User mail aka Email Menu - // - mailMenu: { - art: MAILMNU - desc: Mail Menu - prompt: menuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { command: "C" } - action: @menu:mailMenuCreateMessage - } - { - value: { command: "I" } - action: @menu:mailMenuInbox - } - { - value: { command: "Q" } - action: @systemMethod:prevMenu - } - { - value: { command: "G" } - action: @menu:fullLogoffSequence - } - { - value: 1 - action: @menu:mailMenu - } - ] - } - - mailMenuCreateMessage: { - desc: Mailing Someone - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - }, - editorMode: edit - editorType: email - messageAreaTag: private_mail - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - validate: @systemMethod:validateGeneralMailAddressedTo - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateMessageSubject - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { value: "message", action: "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - }, - 2: { - TLTL: { - mci: { - TL1: { - width: 5 - } - TL2: { - width: 4 - } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - items: [ "save", "discard", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - } - } - } - - mailMenuInbox: { - module: msg_list - art: PRVMSGLIST - config: { - menuViewPost: messageAreaViewPost - messageAreaTag: private_mail - } - form: { - 0: { // main list - mci: { - VM1: { - focus: true - submit: true - argName: message - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "delete", "d", "shift + d" ] - action: @method:deleteSelected - } - ] - } - 1: { // delete prompt form - submit: { - *: [ - { - value: { promptValue: 0 } - action: @method:deleteMessageYes - } - { - value: { promptValue: 1 } - action: @method:deleteMessageNo - } - ] - } - } - } - } - - //////////////////////////////////////////////////////////////////////// - // File Base - //////////////////////////////////////////////////////////////////////// - - fileBase: { - desc: File Base - art: FMENU - prompt: fileMenuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { menuOption: "L" } - action: @menu:fileBaseListEntries - } - { - value: { menuOption: "B" } - action: @menu:fileBaseBrowseByAreaSelect - } - { - value: { menuOption: "F" } - action: @menu:fileAreaFilterEditor - } - { - value: { menuOption: "Q" } - action: @systemMethod:prevMenu - } - { - value: { menuOption: "G" } - action: @menu:fullLogoffSequence - } - { - value: { menuOption: "D" } - action: @menu:fileBaseDownloadManager - } - { - value: { menuOption: "W" } - action: @menu:fileBaseWebDownloadManager - } - { - value: { menuOption: "U" } - action: @menu:fileBaseUploadFiles - } - { - value: { menuOption: "S" } - action: @menu:fileBaseSearch - } - { - value: { menuOption: "P" } - action: @menu:fileBaseSetNewScanDate - } - { - value: { menuOption: "E" } - action: @menu:fileBaseExportListFilter - } - ] - } - - fileBaseExportListFilter: { - module: file_base_search - art: FBLISTEXPSEARCH - config: { - fileBaseListEntriesMenu: fileBaseExportList - } - form: { - 0: { - mci: { - ET1: { - focus: true - argName: searchTerms - } - BT2: { - argName: search - text: search - submit: true - } - ET3: { - maxLength: 64 - argName: tags - } - SM4: { - maxLength: 64 - argName: areaIndex - } - SM5: { - items: [ - "upload date", - "uploaded by", - "downloads", - "rating", - "estimated year", - "size", - "filename" - ] - argName: sortByIndex - } - SM6: { - items: [ - "decending", - "ascending" - ] - argName: orderByIndex - } - BT7: { - argName: advancedSearch - text: advanced search - submit: true - } - } - - submit: { - *: [ - { - value: { search: null } - action: @method:search - } - { - value: { advancedSearch: null } - action: @method:search - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseExportList: { - module: file_base_user_list_export - art: FBLISTEXP - config: { - pause: true - templates: { - entry: file_list_entry.asc - } - } - form: { - 0: { - mci: { - TL1: { } - TL2: { } - } - } - } - } - - fileBaseExportListNoResults: { - desc: Browsing Files - art: FBNORES - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - fileBaseSetNewScanDate: { - module: set_newscan_date - desc: File Base - art: SETFNSDATE - config: { - target: file - scanDateFormat: YYYYMMDD - } - form: { - 0: { - mci: { - ME1: { - focus: true - submit: true - argName: scanDate - maskPattern: "####/##/##" - } - } - submit: { - *: [ - { - value: { scanDate: null } - action: @method:scanDateSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseListEntries: { - module: file_area_list - desc: Browsing Files - config: { - art: { - browse: FBRWSE - details: FDETAIL - detailsGeneral: FDETGEN - detailsNfo: FDETNFO - detailsFileList: FDETLST - help: FBHELP - } - } - form: { - 0: { - mci: { - MT1: { - mode: preview - } - - HM2: { - focus: true - submit: true - argName: navSelect - items: [ - "prev", "next", "details", "toggle queue", "rate", "change filter", "help", "quit" - ] - focusItemIndex: 1 - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:prevFile - } - { - value: { navSelect: 1 } - action: @method:nextFile - } - { - value: { navSelect: 2 } - action: @method:viewDetails - } - { - value: { navSelect: 3 } - action: @method:toggleQueue - } - { - value: { navSelect: 4 } - action: @menu:fileBaseGetRatingForSelectedEntry - } - { - value: { navSelect: 5 } - action: @menu:fileAreaFilterEditor - } - { - value: { navSelect: 6 } - action: @method:displayHelp - } - { - value: { navSelect: 7 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "w", "shift + w" ] - action: @method:showWebDownloadLink - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "t", "shift + t" ] - action: @method:toggleQueue - } - { - keys: [ "f", "shift + f" ] - action: @menu:fileAreaFilterEditor - } - { - keys: [ "v", "shift + v" ] - action: @method:viewDetails - } - { - keys: [ "r", "shift + r" ] - action: @menu:fileBaseGetRatingForSelectedEntry - } - { - keys: [ "?" ] - action: @method:displayHelp - } - ] - } - - 1: { - mci: { - HM1: { - focus: true - submit: true - argName: navSelect - items: [ - "general", "nfo/readme", "file listing" - ] - } - } - - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @method:detailsQuit - } - ] - } - - 2: { - // details - general - mci: {} - } - - 3: { - // details - nfo/readme - mci: { - MT1: { - mode: preview - } - } - } - - 4: { - // details - file listing - mci: { - VM1: { - - } - } - } - } - } - - fileBaseBrowseByAreaSelect: { - desc: Browsing File Areas - module: file_base_area_select - art: FAREASEL - form: { - 0: { - mci: { - VM1: { - focus: true - argName: areaTag - } - } - - submit: { - *: [ - { - value: { areaTag: null } - action: @method:selectArea - } - ] - } - - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseGetRatingForSelectedEntry: { - desc: Rating a File - prompt: fileBaseRateEntryPrompt - config: { - cls: true - } - submit: [ - // :TODO: handle esc/q - { - // pass data back to caller - value: { rating: null } - action: @systemMethod:prevMenu - } - ] - } - - fileBaseListEntriesNoResults: { - desc: Browsing Files - art: FBNORES - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - fileBaseSearch: { - module: file_base_search - desc: Searching Files - art: FSEARCH - form: { - 0: { - mci: { - ET1: { - focus: true - argName: searchTerms - } - BT2: { - argName: search - text: search - submit: true - } - ET3: { - maxLength: 64 - argName: tags - } - SM4: { - maxLength: 64 - argName: areaIndex - } - SM5: { - items: [ - "upload date", - "uploaded by", - "downloads", - "rating", - "estimated year", - "size", - "filename", - ] - argName: sortByIndex - } - SM6: { - items: [ - "decending", - "ascending" - ] - argName: orderByIndex - } - BT7: { - argName: advancedSearch - text: advanced search - submit: true - } - } - - submit: { - *: [ - { - value: { search: null } - action: @method:search - } - { - value: { advancedSearch: null } - action: @method:search - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileAreaFilterEditor: { - desc: File Filter Editor - module: file_area_filter_edit - art: FFILEDT - form: { - 0: { - mci: { - ET1: { - argName: searchTerms - } - ET2: { - maxLength: 64 - argName: tags - } - SM3: { - maxLength: 64 - argName: areaIndex - } - SM4: { - items: [ - "upload date", - "uploaded by", - "downloads", - "rating", - "estimated year", - "size", - ] - argName: sortByIndex - } - SM5: { - items: [ - "decending", - "ascending" - ] - argName: orderByIndex - } - ET6: { - maxLength: 64 - argName: name - validate: @systemMethod:validateNonEmpty - } - HM7: { - focus: true - items: [ - "prev", "next", "make active", "save", "new", "delete" - ] - argName: navSelect - focusItemIndex: 1 - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:prevFilter - } - { - value: { navSelect: 1 } - action: @method:nextFilter - } - { - value: { navSelect: 2 } - action: @method:makeFilterActive - } - { - value: { navSelect: 3 } - action: @method:saveFilter - } - { - value: { navSelect: 4 } - action: @method:newFilter - } - { - value: { navSelect: 5 } - action: @method:deleteFilter - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseDownloadManager: { - desc: Download Manager - module: file_base_download_manager - config: { - art: { - queueManager: FDLMGR - /* - NYI - details: FDLDET - */ - } - emptyQueueMenu: fileBaseDownloadManagerEmptyQueue - } - form: { - 0: { - mci: { - VM1: { - argName: queueItem - } - HM2: { - focus: true - items: [ "download all", "quit" ] - argName: navSelect - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:downloadAll - } - { - value: { navSelect: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "a", "shift + a" ] - action: @method:downloadAll - } - { - keys: [ "delete", "r", "shift + r" ] - action: @method:removeItem - } - { - keys: [ "c", "shift + c" ] - action: @method:clearQueue - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseWebDownloadManager: { - desc: Web D/L Manager - module: file_base_web_download_manager - config: { - art: { - queueManager: FWDLMGR - batchList: BATDLINF - } - emptyQueueMenu: fileBaseDownloadManagerEmptyQueue - } - form: { - 0: { - mci: { - VM1: { - argName: queueItem - } - HM2: { - focus: true - items: [ "get batch link", "quit", "help" ] - argName: navSelect - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:getBatchLink - } - { - value: { navSelect: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "b", "shift + b" ] - action: @method:getBatchLink - } - { - keys: [ "delete", "r", "shift + r" ] - action: @method:removeItem - } - { - keys: [ "c", "shift + c" ] - action: @method:clearQueue - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseDownloadManagerEmptyQueue: { - desc: Empty Download Queue - art: FEMPTYQ - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - fileTransferProtocolSelection: { - desc: Protocol selection - module: file_transfer_protocol_select - art: FPROSEL - form: { - 0: { - mci: { - VM1: { - focus: true - argName: protocol - } - } - - submit: { - *: [ - { - value: { protocol: null } - action: @method:selectProtocol - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseUploadFiles: { - desc: Uploading - module: upload - config: { - interrupt: never - art: { - options: ULOPTS - fileDetails: ULDETAIL - processing: ULCHECK - dupes: ULDUPES - } - } - - form: { - // options - 0: { - mci: { - SM1: { - argName: areaSelect - focus: true - } - TM2: { - argName: uploadType - items: [ "blind", "supply filename" ] - } - ET3: { - argName: fileName - maxLength: 255 - validate: @method:validateNonBlindFileName - } - HM4: { - argName: navSelect - items: [ "continue", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:optionsNavContinue - } - { - value: { navSelect: 1 } - action: @systemMethod:prevMenu - } - ] - } - - "actionKeys" : [ - { - "keys" : [ "escape" ], - action: @systemMethod:prevMenu - } - ] - } - - 1: { - mci: { } - } - - // file details entry - 2: { - mci: { - MT1: { - argName: shortDesc - tabSwitchesView: true - focus: true - } - - ET2: { - argName: tags - } - - ME3: { - argName: estYear - maskPattern: "####" - } - - BT4: { - argName: continue - text: continue - submit: true - } - } - - submit: { - *: [ - { - value: { continue: null } - action: @method:fileDetailsContinue - } - ] - } - } - - // dupes - 3: { - mci: { - VM1: { - /* - Use 'dupeInfoFormat' to custom format: - - areaDesc - areaName - areaTag - desc - descLong - fileId - fileName - fileSha256 - storageTag - uploadTimestamp - - */ - - mode: preview - } - } - } - } - } - - fileBaseNoUploadAreasAvail: { - desc: File Base - art: ULNOAREA - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - sendFilesToUser: { - desc: Downloading - module: file_transfer - config: { - // defaults - generally use extraArgs - protocol: zmodem8kSexyz - direction: send - } - } - - recvFilesFromUser: { - desc: Uploading - module: file_transfer - config: { - // defaults - generally use extraArgs - protocol: zmodem8kSexyz - direction: recv - } - } - - - //////////////////////////////////////////////////////////////////////// - // Required entries - //////////////////////////////////////////////////////////////////////// - idleLogoff: { - art: IDLELOG - next: @systemMethod:logoff - } - //////////////////////////////////////////////////////////////////////// - // Demo Section - // :TODO: This entire section needs updated!!! - //////////////////////////////////////////////////////////////////////// - "demoMain" : { - "art" : "demo_selection_vm.ans", - "form" : { - "0" : { - "VM" : { - "mci" : { - "VM1" : { - "items" : [ - "Single Line Text Editing Views", - "Spinner & Toggle Views", - "Mask Edit Views", - "Multi Line Text Editor", - "Vertical Menu Views", - "Horizontal Menu Views", - "Art Display", - "Full Screen Editor" - ], - "height" : 10, - "itemSpacing" : 1, - "justify" : "center", - "focusTextStyle" : "small i" - } - }, - "submit" : { - "*" : [ - { - "value" : { "1" : 0 }, - "action" : "@menu:demoEditTextView" - }, - { - "value" : { "1" : 1 }, - "action" : "@menu:demoSpinAndToggleView" - }, - { - "value" : { "1" : 2 }, - "action" : "@menu:demoMaskEditView" - }, - { - "value" : { "1" : 3 }, - "action" : "@menu:demoMultiLineEditTextView" - }, - { - "value" : { "1" : 4 }, - "action" : "@menu:demoVerticalMenuView" - }, - { - "value" : { "1" : 5 }, - "action" : "@menu:demoHorizontalMenuView" - }, - { - "value" : { "1" : 6 }, - "action" : "@menu:demoArtDisplay" - }, - { - "value" : { "1" : 7 }, - "action" : "@menu:demoFullScreenEditor" - } - ] - } - } - } - } - }, - "demoEditTextView" : { - "art" : "demo_edit_text_view1.ans", - "form" : { - "0" : { - "BTETETETET" : { - "mci" : { - "ET1" : { - "width" : 20, - "maxLength" : 20 - }, - "ET2" : { - "width" : 20, - "maxLength" : 40, - "textOverflow" : "..." - }, - "ET3" : { - "width" : 20, - "fillChar" : "-", - "styleSGR1" : "|00|36", - "maxLength" : 20 - }, - "ET4" : { - "width" : 20, - "maxLength" : 20, - "password" : true - }, - "BT5" : { - "width" : 8, - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoSpinAndToggleView" : { - "art" : "demo_spin_and_toggle.ans", - "form" : { - "0" : { - "BTSMSMTM" : { - "mci" : { - "SM1" : { - "items" : [ "Henry Morgan", "François l'Ollonais", "Roche Braziliano", "Black Bart", "Blackbeard" ] - }, - "SM2" : { - "items" : [ "Razor 1911", "DrinkOrDie", "TRSI" ] - }, - "TM3" : { - "items" : [ "Yarly", "Nowaii" ], - "styleSGR1" : "|00|30|01", - "hotKeys" : { "Y" : 0, "N" : 1 } - }, - "BT8" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 8, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 8 - } - ] - } - } - } - }, - "demoMaskEditView" : { - "art" : "demo_mask_edit_text_view1.ans", - "form" : { - "0" : { - "BTMEME" : { - "mci" : { - "ME1" : { - "maskPattern" : "##/##/##", - "styleSGR1" : "|00|30|01", - //"styleSGR2" : "|00|45|01", - "styleSGR3" : "|00|30|35", - "fillChar" : "#" - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoMultiLineEditTextView" : { - "art" : "demo_multi_line_edit_text_view1.ans", - "form" : { - "0" : { - "BTMT" : { - "mci" : { - "MT1" : { - "width" : 70, - "height" : 17, - //"text" : "@art:demo_multi_line_edit_text_view_text.txt", - // "text" : "@systemMethod:textFromFile" - text: "Hints:\n\t* Insert / CTRL-V toggles overtype mode\n\t* CTRL-Y deletes the current line\n\t* Try Page Up / Page Down\n\t* Home goes to the start of line text\n\t* End goes to the end of a line\n\n\nTab handling:\n-------------------------------------------------\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\nA0\tBB\t1\tCCC\t2\tDDD\t3EEEE\nW\t\tX\t\tY\t\tZ\n\nAn excerpt from A Clockwork Orange:\n\"What sloochatted then, of course, was that my cellmates woke up and started joining in, tolchocking a bit wild in the near-dark, and the shoom seemed to wake up the whole tier, so that you could slooshy a lot of creeching and banging about with tin mugs on the wall, as though all the plennies in all the cells thought a big break was about to commence, O my brothers.\n", - "focus" : true - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoHorizontalMenuView" : { - "art" : "demo_horizontal_menu_view1.ans", - "form" : { - "0" : { - "BTHMHM" : { - "mci" : { - "HM1" : { - "items" : [ "One", "Two", "Three" ], - "hotKeys" : { "1" : 0, "2" : 1, "3" : 2 } - }, - "HM2" : { - "items" : [ "Uno", "Dos", "Tres" ], - "hotKeys" : { "U" : 0, "D" : 1, "T" : 2 } - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoVerticalMenuView" : { - "art" : "demo_vertical_menu_view1.ans", - "form" : { - "0" : { - "BTVM" : { - "mci" : { - "VM1" : { - "items" : [ - "|33Oblivion/2", - "|33iNiQUiTY", - "|33ViSiON/X" - ], - "focusItems" : [ - "|33Oblivion|01/|00|332", - "|01|33i|00|33N|01i|00|33QU|01i|00|33TY", - "|33ViSiON/X" - ] - // - // :TODO: how to do the following: - // 1) Supply a view a string for a standard vs focused item - // "items" : [...], "focusItems" : [ ... ] ? - // "draw" : "@method:drawItemX", then items: [...] - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - - }, - "demoArtDisplay" : { - "art" : "demo_selection_vm.ans", - "form" : { - "0" : { - "VM" : { - "mci" : { - "VM1" : { - "items" : [ - "Defaults - DOS ANSI", - "bw_mindgames.ans - DOS", - "test.ans - DOS", - "Defaults - Amiga", - "Pause at Term Height" - ], - // :TODO: justify not working?? - "focusTextStyle" : "small i" - } - }, - "submit" : { - "*" : [ - { - "value" : { "1" : 0 }, - "action" : "@menu:demoDefaultsDosAnsi" - }, - { - "value" : { "1" : 1 }, - "action" : "@menu:demoDefaultsDosAnsi_bw_mindgames" - }, - { - "value" : { "1" : 2 }, - "action" : "@menu:demoDefaultsDosAnsi_test" - } - ] - } - } - } - } - }, - "demoDefaultsDosAnsi" : { - "art" : "DM-ENIG2.ANS" - }, - "demoDefaultsDosAnsi_bw_mindgames" : { - "art" : "bw_mindgames.ans" - }, - "demoDefaultsDosAnsi_test" : { - "art" : "test.ans" - }, - "demoFullScreenEditor" : { - "module" : "fse", - "config" : { - "editorType" : "netMail", - "art" : { - "header" : "demo_fse_netmail_header.ans", - "body" : "demo_fse_netmail_body.ans", - "footerEditor" : "demo_fse_netmail_footer_edit.ans", - "footerEditorMenu" : "demo_fse_netmail_footer_edit_menu.ans", - "footerView" : "demo_fse_netmail_footer_view.ans", - "help" : "demo_fse_netmail_help.ans" - } - }, - "form" : { - "0" : { - "ETETET" : { - "mci" : { - "ET1" : { - // :TODO: from/to may be set by args - // :TODO: focus may change dep on view vs edit - "width" : 36, - "focus" : true, - "argName" : "to" - }, - "ET2" : { - "width" : 36, - "argName" : "from" - }, - "ET3" : { - "width" : 65, - "maxLength" : 72, - "submit" : [ "enter" ], - "argName" : "subject" - } - }, - "submit" : { - "3" : [ - { - "value" : { "subject" : null }, - "action" : "@method:headerSubmit" - } - ] - } - } - }, - "1" : { - "MT" : { - "mci" : { - "MT1" : { - "width" : 79, - "height" : 17, - "text" : "", // :TODO: should not be req. - "argName" : "message" - } - }, - "submit" : { - "*" : [ - { - "value" : "message", - "action" : "@method:editModeEscPressed" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 1 - } - ] - } - }, - "2" : { - "TLTL" : { - "mci" : { - "TL1" : { - "width" : 5 - }, - "TL2" : { - "width" : 4 - } - } - } - }, - "3" : { - "HM" : { - "mci" : { - "HM1" : { - // :TODO: Continue, Save, Discard, Clear, Quote, Help - "items" : [ "Save", "Discard", "Quote", "Help" ] - } - }, - "submit" : { - "*" : [ - { - "value" : { "1" : 0 }, - "action" : "@method:editModeMenuSave" - }, - { - "value" : { "1" : 1 }, - "action" : "@menu:demoMain" - }, - { - "value" : { "1" : 2 }, - "action" : "@method:editModeMenuQuote" - }, - { - "value" : { "1" : 3 }, - "action" : "@method:editModeMenuHelp" - }, - { - "value" : 1, - "action" : "@method:editModeEscPressed" - } - ] - }, - "actionKeys" : [ // :TODO: Need better name - { - "keys" : [ "escape" ], - "action" : "@method:editModeEscPressed" - } - ] - } - } - } - } - } -} From 094385a1507b8bed31c5faf79c2eb5bdd8c567f2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 7 Jun 2019 23:23:06 -0600 Subject: [PATCH 26/36] Minor bug fixes and improvements * Color help text a bit * Split lines for buffer so padding works - may need to do elsewhere also * Fix a couple crashes --- core/mrc.js | 116 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 69 insertions(+), 47 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 6fd8ac35..36475d70 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -32,7 +32,7 @@ const FormIds = { mrcChat : 0, }; -var MciViewIds = { +const MciViewIds = { mrcChat : { chatLog : 1, inputArea : 2, @@ -48,19 +48,20 @@ var MciViewIds = { // TODO: this is a bit shit, could maybe do it with an ansi instead const helpText = ` -General Chat: -/rooms & /join - Send a private message +|15General Chat|08: +|03/|11rooms |08& |03/|11join |03 |08- |07List all or join a room +|03/|11pm |03 |08- |07Send a private message ---- -/whoon - Who's on what BBS -/chatters - Who's in what room -/topic - Set the room topic -/bbses & /info - Info about BBS's connected -/meetups - Info about MRC MeetUps +|03/|11whoon |08- |07Who's on what BBS +|03/|11chatters |08- |07Who's in what room +|03/|11clear |08- |07Clear back buffer +|03/|11topic |08- |07Set the room topic +|03/|11bbses & |03/|11info |08- |07Info about BBS's connected +|03/|11meetups |08- |07Info about MRC MeetUps --- -/l33t - l337 5p34k -/kewl - BBS KeWL SPeaK -/rainbow - Crazy rainbow text +|03/|11l33t |08- |07l337 5p34k +|03/|11kewl |08- |07BBS KeWL SPeaK +|03/|11rainbow |08- |07Crazy rainbow text `; @@ -94,7 +95,7 @@ exports.getModule = class mrcModule extends MenuModule { }, movementKeyPressed : (formData, extraArgs, cb) => { - const bodyView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); // :TODO: use const here vs magic # + const bodyView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); switch(formData.key.name) { case 'down arrow' : bodyView.scrollDocumentUp(); break; case 'up arrow' : bodyView.scrollDocumentDown(); break; @@ -108,9 +109,6 @@ exports.getModule = class mrcModule extends MenuModule { }, quit : (formData, extraArgs, cb) => { - this.sendServerMessage('LOGOFF'); - clearInterval(this.heartbeat); - this.state.socket.destroy(); return this.prevMenu(cb); } }; @@ -138,16 +136,19 @@ exports.getModule = class mrcModule extends MenuModule { // connect to multiplexer this.state.socket = net.createConnection(connectOpts, () => { - const self = this; - // handshake with multiplexer - self.state.socket.write(`--DUDE-ITS--|${self.state.alias}\n`); + this.client.once('end', () => { + this.quitServer(); + }); - self.clientConnect(); + // handshake with multiplexer + this.state.socket.write(`--DUDE-ITS--|${this.state.alias}\n`); + + this.clientConnect(); // send register to central MRC and get stats every 60s - self.heartbeat = setInterval(function () { - self.sendHeartbeat(); - self.sendServerMessage('STATS'); + this.heartbeat = setInterval( () => { + this.sendHeartbeat(); + this.sendServerMessage('STATS'); }, 60000); }); @@ -167,26 +168,46 @@ exports.getModule = class mrcModule extends MenuModule { }); } + leave() { + this.quitServer(); + return super.leave(); + } + + quitServer() { + clearInterval(this.heartbeat); + + if(this.state.socket) { + this.sendServerMessage('LOGOFF'); + this.state.socket.destroy(); + } + } + /** * Adds a message to the chat log on screen */ addMessageToChatLog(message) { - const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); - const messageLength = stripMciColorCodes(message).length; - const chatWidth = chatLogView.dimens.width; - let padAmount = 0; - let spaces = 2; - - if (messageLength > chatWidth) { - padAmount = chatWidth - (messageLength % chatWidth) - spaces; - } else { - padAmount = chatWidth - messageLength - spaces ; + if(!Array.isArray(message)) { + message = [ message ]; } - if (padAmount < 0) padAmount = 0; + message.forEach(msg => { + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + const messageLength = stripMciColorCodes(msg).length; + const chatWidth = chatLogView.dimens.width; + let padAmount = 0; + let spaces = 2; - const padding = ' |00' + ' '.repeat(padAmount); - chatLogView.addText(pipeToAnsi(message + padding)); + if (messageLength > chatWidth) { + padAmount = chatWidth - (messageLength % chatWidth) - spaces; + } else { + padAmount = chatWidth - messageLength - spaces; + } + + if (padAmount < 0) padAmount = 0; + + const padding = ' |00' + ' '.repeat(padAmount); + chatLogView.addText(pipeToAnsi(msg + padding)); + }); } /** @@ -267,7 +288,7 @@ exports.getModule = class mrcModule extends MenuModule { const messageFormat = this.config.messageFormat || '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; - + const privateMessageFormat = this.config.outgoingPrivateMessageFormat || '|00|10<|02{fromUserName}|10|14->|02{toUserName}>|00 |03{message}|00'; @@ -280,7 +301,7 @@ exports.getModule = class mrcModule extends MenuModule { // pm formattedMessage = stringFormat(privateMessageFormat, textFormatObj); } - + try { this.sendMessageToMultiplexer(to_user || '', '', this.state.room, formattedMessage); } catch(e) { @@ -366,7 +387,7 @@ exports.getModule = class mrcModule extends MenuModule { break; case '?': - this.addMessageToChatLog(helpText); + this.addMessageToChatLog(helpText.split(/\n/g)); break; default: @@ -385,16 +406,17 @@ exports.getModule = class mrcModule extends MenuModule { sendMessageToMultiplexer(to_user, to_site, to_room, body) { const message = { - from_user: this.state.alias, - from_room: this.state.room, - to_user: to_user, - to_site: to_site, - to_room: to_room, - body: body + to_user, + to_site, + to_room, + body, + from_user : this.state.alias, + from_room : this.state.room, }; - // TODO: check socket still exists here - this.state.socket.write(JSON.stringify(message) + '\n'); + if(this.state.socket) { + this.state.socket.write(JSON.stringify(message) + '\n'); + } } /** From afb7854ea50f4bdf00d337d6045a094db0cf4281 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 8 Jun 2019 11:57:36 -0600 Subject: [PATCH 27/36] Clean up logging a bit & implement 'omit' * 'omit' can be set to omit views from form submission * Don't log as much noise --- core/view.js | 6 ++++++ core/view_controller.js | 23 +++++++++-------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/core/view.js b/core/view.js index 7d3c5693..58a305e4 100644 --- a/core/view.js +++ b/core/view.js @@ -220,6 +220,12 @@ View.prototype.setPropertyValue = function(propName, value) { case 'argName' : this.submitArgName = value; break; + case 'omit' : + if(_.isBoolean(value)) { + this.omitFromSubmission = value; break; + } + break; + case 'validate' : if(_.isFunction(value)) { this.validate = value; diff --git a/core/view_controller.js b/core/view_controller.js index 84f33756..9c0ffe19 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -46,6 +46,8 @@ function ViewController(options) { return; // ignore until this is finished! } + self.client.log.trace( { actionBlock }, 'Action match' ); + self.waitActionCompletion = true; menuUtil.handleAction(self.client, formData, actionBlock, (err) => { if(err) { @@ -121,9 +123,7 @@ function ViewController(options) { self.emit('submit', this.getFormData(key)); }; - // :TODO: replace this in favor of overriding toJSON() for various things such that logging will *never* output them this.getLogFriendlyFormData = function(formData) { - // :TODO: these fields should be part of menu.json sensitiveMembers[] var safeFormData = _.cloneDeep(formData); if(safeFormData.value.password) { safeFormData.value.password = '*****'; @@ -330,15 +330,6 @@ function ViewController(options) { } } } - - self.client.log.trace( - { - formValue : formValue, - actionValue : actionValue - }, - 'Action match' - ); - return true; }; @@ -577,7 +568,7 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { if(false === self.noInput) { self.on('submit', function promptSubmit(formData) { - self.client.log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Prompt submit'); + self.client.log.trace( { formData }, 'Prompt submit'); const doSubmitNotify = () => { if(options.submitNotify) { @@ -752,8 +743,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { } self.on('submit', function formSubmit(formData) { - - self.client.log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Form submit'); + self.client.log.trace( { formData }, 'Form submit'); // // Locate configuration for this form ID @@ -870,6 +860,11 @@ ViewController.prototype.getFormData = function(key) { return; } + // some form values may be omitted from submission all together + if(view.omitFromSubmission) { + return; + } + viewData = view.getData(); if(_.isUndefined(viewData)) { return; From f2e769a27ab2f26d75505ada456f1ee3ad6dc106 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 8 Jun 2019 11:58:21 -0600 Subject: [PATCH 28/36] Fix up colors, clear out socket --- core/mrc.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 36475d70..f2bfeec6 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -55,13 +55,13 @@ const helpText = ` |03/|11whoon |08- |07Who's on what BBS |03/|11chatters |08- |07Who's in what room |03/|11clear |08- |07Clear back buffer -|03/|11topic |08- |07Set the room topic -|03/|11bbses & |03/|11info |08- |07Info about BBS's connected +|03/|11topic |03 |08- |07Set the room topic +|03/|11bbses |08& |03/|11info |08- |07Info about BBS's connected |03/|11meetups |08- |07Info about MRC MeetUps --- -|03/|11l33t |08- |07l337 5p34k -|03/|11kewl |08- |07BBS KeWL SPeaK -|03/|11rainbow |08- |07Crazy rainbow text +|03/|11l33t |03 |08- |07l337 5p34k +|03/|11kewl |03 |08- |07BBS KeWL SPeaK +|03/|11rainbow |03 |08- |07Crazy rainbow text `; @@ -179,6 +179,7 @@ exports.getModule = class mrcModule extends MenuModule { if(this.state.socket) { this.sendServerMessage('LOGOFF'); this.state.socket.destroy(); + delete this.state.socket; } } From 33969448204ed7bd2e31cb1ed6083632a96a425e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 8 Jun 2019 11:58:28 -0600 Subject: [PATCH 29/36] Note on 'omit' --- WHATSNEW.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 7137038a..d64f7d1d 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -32,7 +32,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Performing a file scan/import using `oputil.js fb scan` now recognizes various `FILES.BBS` formats. * Usernames found in the `config.users.badUserNames` are now not only disallowed from applying, but disconnected at any login attempt. * Total minutes online is now tracked for users. Of course, it only starts after you get the update :) - +* Form entries in `menu.hjson` can now be omitted from submission handlers using `omit: true` ## 0.0.8-alpha * [Mystic BBS style](http://wiki.mysticbbs.com/doku.php?id=displaycodes) extended pipe color codes. These allow for example, to set "iCE" background colors. From ad305b4cccd6086befa758b793db5b9315d85b7e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 8 Jun 2019 12:12:29 -0600 Subject: [PATCH 30/36] Implement 'maxScrollbackLines' config for MRC module --- core/mrc.js | 6 ++++++ misc/menu_template.in.hjson | 3 +++ 2 files changed, 9 insertions(+) diff --git a/core/mrc.js b/core/mrc.js index f2bfeec6..3ab1e91b 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -72,6 +72,8 @@ exports.getModule = class mrcModule extends MenuModule { this.log = Log.child( { module : 'MRC' } ); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + this.config.maxScrollbackLines = this.config.maxScrollbackLines || 500; + this.state = { socket: '', alias: this.client.user.username, @@ -208,6 +210,10 @@ exports.getModule = class mrcModule extends MenuModule { const padding = ' |00' + ' '.repeat(padAmount); chatLogView.addText(pipeToAnsi(msg + padding)); + + if(chatLogView.getLineCount() > this.config.maxScrollbackLines) { + chatLogView.deleteLine(0); + } }); } diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 9ae54d17..5c7eba83 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -1104,6 +1104,9 @@ art: MRC config: { cls: true + + // max lines kept in scrollback buffer + maxScrollbackLines: 500 } form: { 0: { From 433ad72752e26803d610c4c5eb693663cfc73d52 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Jun 2019 00:09:07 -0600 Subject: [PATCH 31/36] Fixes & /clear * Fix use port from config * Add /clear & menu method: clearMessages() --- core/mrc.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 3ab1e91b..c80adf80 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -10,6 +10,7 @@ const { } = require('./color_codes.js'); const stringFormat = require('./string_format.js'); const StringUtil = require('./string_util.js'); +const Config = require('./config.js').get; // deps const _ = require('lodash'); @@ -112,6 +113,11 @@ exports.getModule = class mrcModule extends MenuModule { quit : (formData, extraArgs, cb) => { return this.prevMenu(cb); + }, + + clearMessages : (formData, extraArgs, cb) => { + this.clearMessages(); + return cb(null); } }; } @@ -132,7 +138,7 @@ exports.getModule = class mrcModule extends MenuModule { }, (callback) => { const connectOpts = { - port : 5000, + port : _.get(Config(), 'chatServers.mrc.serverPort', 5000), host : 'localhost', }; @@ -160,6 +166,15 @@ exports.getModule = class mrcModule extends MenuModule { this.processReceivedMessage(data); }); + this.state.socket.once('error', err => { + this.log.warn( { error : err.message }, 'MRC multiplexer socket error' ); + this.state.socket.destroy(); + delete this.state.socket; + + // bail with error - fall back to prev menu + return callback(err); + }); + return(callback); } ], @@ -389,8 +404,11 @@ exports.getModule = class mrcModule extends MenuModule { this.sendServerMessage('LIST'); break; + case 'quit' : + return this.prevMenu(); + case 'clear': - chatLogView.setText(''); + this.clearMessages(); break; case '?': @@ -404,7 +422,11 @@ exports.getModule = class mrcModule extends MenuModule { // just do something to get the cursor back to the right place ¯\_(ツ)_/¯ this.sendServerMessage('STATS'); + } + clearMessages() { + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + chatLogView.setText(''); } /** From 487968dac94e7453d317e4647f772e30c6208047 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Jun 2019 09:19:34 -0600 Subject: [PATCH 32/36] Fix my previous dumb --- core/mrc.js | 2 +- core/servers/chat/mrc_multiplexer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index c80adf80..6987be32 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -138,7 +138,7 @@ exports.getModule = class mrcModule extends MenuModule { }, (callback) => { const connectOpts = { - port : _.get(Config(), 'chatServers.mrc.serverPort', 5000), + port : _.get(Config(), 'chatServers.mrc.multiplexerPort', 5000), host : 'localhost', }; diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 9b976eff..fd83d462 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -104,7 +104,7 @@ exports.getModule = class MrcModule extends ServerModule { buffer = Buffer.concat([buffer, chunk]); } - var lines = buffer.toString().split(lineDelimiter); + let lines = buffer.toString().split(lineDelimiter); if (lines.pop()) { // if buffer is not ended with \r\n, there's more chunks. From 5c978e05bf468ffa7b10deb78ef113a5ac570af7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Jun 2019 13:41:11 -0600 Subject: [PATCH 33/36] Ability to override idle time and/or temporary disable from MRC --- core/client.js | 21 ++++++++++++++++++++- core/mrc.js | 16 ++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/core/client.js b/core/client.js index d340981b..76d56529 100644 --- a/core/client.js +++ b/core/client.js @@ -439,6 +439,11 @@ Client.prototype.setTermType = function(termType) { }; Client.prototype.startIdleMonitor = function() { + // clear existing, if any + if(this.idleCheck) { + this.stopIdleMonitor(); + } + this.lastKeyPressMs = Date.now(); // @@ -474,6 +479,9 @@ Client.prototype.startIdleMonitor = function() { idleLogoutSeconds = Config().users.preAuthIdleLogoutSeconds; } + // use override value if set + idleLogoutSeconds = this.idleLogoutSecondsOverride || idleLogoutSeconds; + if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { this.emit('idle timeout'); } @@ -481,7 +489,18 @@ Client.prototype.startIdleMonitor = function() { }; Client.prototype.stopIdleMonitor = function() { - clearInterval(this.idleCheck); + if(this.idleCheck) { + clearInterval(this.idleCheck); + delete this.idleCheck; + } +}; + +Client.prototype.overrideIdleLogoutSeconds = function(seconds) { + this.idleLogoutSecondsOverride = seconds; +}; + +Client.prototype.restoreIdleLogoutSeconds = function() { + delete this.idleLogoutSecondsOverride; }; Client.prototype.end = function () { diff --git a/core/mrc.js b/core/mrc.js index 6987be32..96637dfa 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -158,6 +158,16 @@ exports.getModule = class mrcModule extends MenuModule { this.sendHeartbeat(); this.sendServerMessage('STATS'); }, 60000); + + // override idle logout seconds if configured + const idleLogoutSeconds = parseInt(this.config.idleLogoutSeconds); + if(0 === idleLogoutSeconds) { + this.log.debug('Temporary disable idle monitor due to config'); + this.client.stopIdleMonitor(); + } else if (!isNaN(idleLogoutSeconds) && idleLogoutSeconds >= 60) { + this.log.debug( { idleLogoutSeconds }, 'Temporary override idle logout seconds due to config'); + this.client.overrideIdleLogoutSeconds(idleLogoutSeconds); + } }); // when we get data, process it @@ -187,6 +197,12 @@ exports.getModule = class mrcModule extends MenuModule { leave() { this.quitServer(); + + // restore idle monitor to previous state + this.log.debug('Restoring idle monitor to previous state'); + this.client.restoreIdleLogoutSeconds(); + this.client.startIdleMonitor(); + return super.leave(); } From da93fd53e9de052d0e9845ae6879a97853674674 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Jun 2019 22:36:24 -0600 Subject: [PATCH 34/36] Start of custom format obj stuff. Want to add more info --- core/mrc.js | 50 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 96637dfa..8832d103 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -41,7 +41,8 @@ const MciViewIds = { roomTopic : 4, mrcUsers : 5, mrcBbses : 6, - customRangeStart : 10, // 10+ = customs + + customRangeStart : 20, // 20+ = customs } }; @@ -84,6 +85,16 @@ exports.getModule = class mrcModule extends MenuModule { last_ping: 0 }; + this.customFormatObj = { + roomName : '', + roomTopic : '', + roomUserCount : 0, + userCount : 0, + boardCount : 0, + roomCount : 0, + //latencyMs : 0, + }; + this.menuMethods = { sendChatMessage : (formData, extraArgs, cb) => { @@ -269,19 +280,33 @@ exports.getModule = class mrcModule extends MenuModule { break; case 'ROOMTOPIC': - this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomName).setText(`#${params[1]}`); - this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(pipeToAnsi(params[2])); + this.setText(MciViewIds.mrcChat.roomName, `#${params[1]}`); + this.setText(MciViewIds.mrcChat.roomTopic, params[2]); + + this.customFormatObj.roomName = params[1]; + this.customFormatObj.roomTopic = params[2]; + this.updateCustomViews(); + this.state.room = params[1]; break; case 'USERLIST': this.state.nicks = params[1].split(','); + + this.customFormatObj.roomUserCount = this.state.nicks.length; + this.updateCustomViews(); break; case 'STATS': { const stats = params[1].split(' '); - this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcUsers).setText(stats[2]); - this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcBbses).setText(stats[0]); + this.setText(MciViewIds.mrcChat.mrcUsers, stats[2]); + this.setText(MciViewIds.mrcChat.mrcBbses, stats[0]); + + this.customFormatObj.boardCount = parseInt(stats[0]); + this.customFormatObj.roomCount = parseInt(stats[1]); + this.customFormatObj.userCount = parseInt(stats[2]); + this.updateCustomViews(); + this.state.last_ping = stats[1]; break; } @@ -303,6 +328,18 @@ exports.getModule = class mrcModule extends MenuModule { }); } + setText(mciId, text) { + return this.setViewText('mrcChat', mciId, text); + } + + updateCustomViews() { + return this.updateCustomViewTextsWithFilter( + 'mrcChat', + MciViewIds.mrcChat.customRangeStart, + this.customFormatObj + ); + } + /** * Receives the message input from the user and does something with it based on what it is */ @@ -353,9 +390,6 @@ exports.getModule = class mrcModule extends MenuModule { * Processes a message that begins with a slash */ processSlashCommand(message) { - // get the chat log view in case we need it - const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); - const cmd = message.split(' '); cmd[0] = cmd[0].substr(1).toLowerCase(); From 80eb8ad38df5d7c9a0c6fce9b940bed7cf7c928a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 10 Jun 2019 20:07:45 -0600 Subject: [PATCH 35/36] Add latency est., activity level indicators, etc. --- core/mrc.js | 67 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 8832d103..b8d752ee 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -82,17 +82,19 @@ exports.getModule = class mrcModule extends MenuModule { room: '', room_topic: '', nicks: [], - last_ping: 0 + lastSentMsg : {}, // used for latency est. }; this.customFormatObj = { - roomName : '', - roomTopic : '', - roomUserCount : 0, - userCount : 0, - boardCount : 0, - roomCount : 0, - //latencyMs : 0, + roomName : '', + roomTopic : '', + roomUserCount : 0, + userCount : 0, + boardCount : 0, + roomCount : 0, + //latencyMs : 0, + activityLevel : 0, + activityLevelIndicator : ' ', }; this.menuMethods = { @@ -298,16 +300,27 @@ exports.getModule = class mrcModule extends MenuModule { break; case 'STATS': { - const stats = params[1].split(' '); - this.setText(MciViewIds.mrcChat.mrcUsers, stats[2]); - this.setText(MciViewIds.mrcChat.mrcBbses, stats[0]); + const [ + boardCount, + roomCount, + userCount, + activityLevel + ] = params[1].split(' ').map(v => parseInt(v)); + + const activityLevelIndicator = this.getActivityLevelIndicator(activityLevel); + + Object.assign( + this.customFormatObj, + { + boardCount, roomCount, userCount, + activityLevel, activityLevelIndicator + } + ); + + this.setText(MciViewIds.mrcChat.mrcUsers, userCount); + this.setText(MciViewIds.mrcChat.mrcBbses, boardCount); - this.customFormatObj.boardCount = parseInt(stats[0]); - this.customFormatObj.roomCount = parseInt(stats[1]); - this.customFormatObj.userCount = parseInt(stats[2]); this.updateCustomViews(); - - this.state.last_ping = stats[1]; break; } @@ -317,6 +330,12 @@ exports.getModule = class mrcModule extends MenuModule { } } else { + if(message.body === this.state.lastSentMsg.msg) { + this.customFormatObj.latencyMs = + moment.duration(moment().diff(this.state.lastSentMsg.time)).asMilliseconds(); + delete this.state.lastSentMsg.msg; + } + if (message.to_room == this.state.room) { // if we're here then we want to show it to the user const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); @@ -328,6 +347,14 @@ exports.getModule = class mrcModule extends MenuModule { }); } + getActivityLevelIndicator(level) { + let indicators = this.config.activityLevelIndicators; + if(!Array.isArray(indicators) || indicators.length < level + 1) { + indicators = [ ' ', '░', '▒', '▓' ]; + } + return indicators[level].charAt(0); + } + setText(mciId, text) { return this.setViewText('mrcChat', mciId, text); } @@ -378,6 +405,10 @@ exports.getModule = class mrcModule extends MenuModule { } try { + this.state.lastSentMsg = { + msg : formattedMessage, + time : moment(), + }; this.sendMessageToMultiplexer(to_user || '', '', this.state.room, formattedMessage); } catch(e) { this.client.log.warn( { error : e.message }, 'MRC error'); @@ -397,6 +428,7 @@ exports.getModule = class mrcModule extends MenuModule { case 'pm': this.processOutgoingMessage(cmd[2], cmd[1]); break; + case 'rainbow': { // this is brutal, but i love it const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { @@ -408,6 +440,7 @@ exports.getModule = class mrcModule extends MenuModule { this.processOutgoingMessage(line); break; } + case 'l33t': this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), 'l33t')); break; @@ -418,6 +451,7 @@ exports.getModule = class mrcModule extends MenuModule { this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), mode)); break; } + case 'whoon': this.sendServerMessage('WHOON'); break; @@ -471,6 +505,7 @@ exports.getModule = class mrcModule extends MenuModule { } // just do something to get the cursor back to the right place ¯\_(ツ)_/¯ + // :TODO: fix me! this.sendServerMessage('STATS'); } From d6af9a1caafc6a089a7b642912e1b9617499e91e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 10 Jun 2019 20:12:17 -0600 Subject: [PATCH 36/36] Minor --- core/mrc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index b8d752ee..754e2728 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -92,7 +92,7 @@ exports.getModule = class mrcModule extends MenuModule { userCount : 0, boardCount : 0, roomCount : 0, - //latencyMs : 0, + latencyMs : 0, activityLevel : 0, activityLevelIndicator : ' ', }; @@ -352,7 +352,7 @@ exports.getModule = class mrcModule extends MenuModule { if(!Array.isArray(indicators) || indicators.length < level + 1) { indicators = [ ' ', '░', '▒', '▓' ]; } - return indicators[level].charAt(0); + return indicators[level]; } setText(mciId, text) {