diff --git a/core/ansi_term.js b/core/ansi_term.js index 28a3f20f..869768b9 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -12,9 +12,13 @@ // * http://www.inwap.com/pdp10/ansicode.txt // -const assert = require('assert'); +// ENiGMA½ const miscUtil = require('./misc_util.js'); +// deps +const assert = require('assert'); +const _ = require('lodash'); + exports.getFGColorValue = getFGColorValue; exports.getBGColorValue = getBGColorValue; exports.sgr = sgr; @@ -23,7 +27,6 @@ exports.clearScreen = clearScreen; exports.resetScreen = resetScreen; exports.normal = normal; exports.goHome = goHome; -//exports.deleteLine = deleteLine; exports.disableVT100LineWrapping = disableVT100LineWrapping; exports.setSyncTERMFont = setSyncTERMFont; exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias; @@ -35,9 +38,9 @@ exports.setEmulatedBaudRate = setEmulatedBaudRate; // See also // https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js -var ESC_CSI = '\u001b['; +const ESC_CSI = '\u001b['; -var CONTROL = { +const CONTROL = { up : 'A', down : 'B', @@ -124,7 +127,7 @@ var CONTROL = { // Select Graphics Rendition // See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt // -var SGRValues = { +const SGRValues = { reset : 0, bold : 1, dim : 2, @@ -180,7 +183,7 @@ function getBGColorValue(name) { // // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // -var SYNCTERM_FONT_AND_ENCODING_TABLE = [ +const SYNCTERM_FONT_AND_ENCODING_TABLE = [ 'cp437', 'cp1251', 'koi8_r', @@ -233,7 +236,7 @@ var SYNCTERM_FONT_AND_ENCODING_TABLE = [ // This table contains lowercased entries with any spaces // replaced with '_' for lookup purposes. // -var FONT_ALIAS_TO_SYNCTERM_MAP = { +const FONT_ALIAS_TO_SYNCTERM_MAP = { 'cp437' : 'cp437', 'ibm_vga' : 'cp437', 'ibmpc' : 'cp437', @@ -267,8 +270,8 @@ var FONT_ALIAS_TO_SYNCTERM_MAP = { 'amiga_p0t-noodle' : 'pot_noodle', 'mo_soul' : 'mo_soul', - 'mosoul' : 'mo_soul', - 'mO\'sOul' : 'mo_soul', + 'mosoul' : 'mo_soul', + 'mO\'sOul' : 'mo_soul', 'amiga_microknight' : 'microknight', 'amiga_microknight+' : 'microknight_plus', @@ -280,13 +283,13 @@ var FONT_ALIAS_TO_SYNCTERM_MAP = { }; function setSyncTERMFont(name, fontPage) { - var p1 = miscUtil.valueWithDefault(fontPage, 0); + const p1 = miscUtil.valueWithDefault(fontPage, 0); assert(p1 >= 0 && p1 <= 3); - var p2 = SYNCTERM_FONT_AND_ENCODING_TABLE.indexOf(name); + const p2 = SYNCTERM_FONT_AND_ENCODING_TABLE.indexOf(name); if(p2 > -1) { - return ESC_CSI + p1 + ';' + p2 + ' D'; + return `${ESC_CSI}${p1};${p2} D`; } return ''; @@ -296,20 +299,20 @@ function getSyncTERMFontFromAlias(alias) { return FONT_ALIAS_TO_SYNCTERM_MAP[alias.toLowerCase().replace(/ /g, '_')]; } -var DEC_CURSOR_STYLE = { - 'blinking block' : 0, - 'default' : 1, - 'steady block' : 2, +const DEC_CURSOR_STYLE = { + 'blinking block' : 0, + 'default' : 1, + 'steady block' : 2, 'blinking underline' : 3, - 'steady underline' : 4, - 'blinking bar' : 5, - 'steady bar' : 6, + 'steady underline' : 4, + 'blinking bar' : 5, + 'steady bar' : 6, }; function setCursorStyle(cursorStyle) { - var ps = DEC_CURSOR_STYLE[cursorStyle]; + const ps = DEC_CURSOR_STYLE[cursorStyle]; if(ps) { - return ESC_CSI + ps + ' q'; + return `${ESC_CSI}${ps} q`; } return ''; @@ -317,24 +320,24 @@ function setCursorStyle(cursorStyle) { // Create methods such as up(), nextLine(),... Object.keys(CONTROL).forEach(function onControlName(name) { - var code = CONTROL[name]; + const code = CONTROL[name]; exports[name] = function() { - var c = code; + let c = code; if(arguments.length > 0) { // arguments are array like -- we want an array c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code; } - return ESC_CSI + c; + return `${ESC_CSI}${c}`; }; }); // Create various color methods such as white(), yellowBG(), reset(), ... -Object.keys(SGRValues).forEach(function onSgrName(name) { - var code = SGRValues[name]; +Object.keys(SGRValues).forEach( name => { + const code = SGRValues[name]; exports[name] = function() { - return ESC_CSI + code + 'm'; + return `${ESC_CSI}${code}m`; }; }); @@ -347,28 +350,20 @@ function sgr() { if(arguments.length <= 0) { return ''; } - - var result = ''; - // :TODO: this method needs a lot of cleanup! + let result = []; + const args = Array.isArray(arguments[0]) ? arguments[0] : arguments; - var args = Array.isArray(arguments[0]) ? arguments[0] : arguments; - for(var i = 0; i < args.length; i++) { - if(typeof args[i] === 'string') { - if(args[i] in SGRValues) { - if(result.length > 0) { - result += ';'; - } - result += SGRValues[args[i]]; - } - } else if(typeof args[i] === 'number') { - if(result.length > 0) { - result += ';'; - } - result += args[i]; + for(let i = 0; i < args.length; ++i) { + const arg = args[i]; + if(_.isString(arg) && arg in SGRValues) { + result.push(SGRValues[arg]); + } else if(_.isNumber(arg)) { + result.push(arg); } } - return ESC_CSI + result + 'm'; + + return `${ESC_CSI}${result.join(';')}m`; } // @@ -376,10 +371,10 @@ function sgr() { // to a ANSI SGR sequence. // function getSGRFromGraphicRendition(graphicRendition, initialReset) { - var sgrSeq = []; + let sgrSeq = []; + let styleCount = 0; - var styleCount = 0; - [ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach(function style(s) { + [ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => { if(graphicRendition[s]) { sgrSeq.push(graphicRendition[s]); ++styleCount; @@ -414,11 +409,11 @@ function clearScreen() { } function resetScreen() { - return exports.reset() + exports.eraseData(2) + exports.goHome(); + return `${exports.reset()}${exports.eraseData(2)}${exports.goHome()}`; } function normal() { - return sgr(['normal', 'reset']); + return sgr( [ 'normal', 'reset' ] ); } function goHome() { @@ -426,40 +421,23 @@ function goHome() { } // -// Delete line(s) -// This method acts like ESC[ p1 M but should work -// for all terminals via using eraseLine and movement +// Disable auto line wraping @ termWidth // -/* -function deleteLine(count) { - count = count || 1; - - console.log(exports.eraseLine) - var seq = exports.eraseLine(2); // 2 = entire line - var i; - for(i = 1; i < count; ++i) { - seq += - '\n' + // down a line - exports.eraseLine(2); // erase it - } - - // now, move back up any we lines we went down - if(count > 1) { - seq += exports.up(count - 1); - } - return seq; -} -*/ - +// See: +// http://stjarnhimlen.se/snippets/vt100.txt +// https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // -// See http://www.termsys.demon.co.uk/vtANSI_BBS.htm +// WARNING: +// * Not honored by all clients +// * If it is honored, ANSI's that rely on this (e.g. do not have \r\n endings +// and use term width -- generally 80 columns -- will display garbled! // function disableVT100LineWrapping() { - return ESC_CSI + '7l'; + return `${ESC_CSI}?7l`; } function setEmulatedBaudRate(rate) { - var speed = { + const speed = { unlimited : 0, off : 0, 0 : 0, diff --git a/core/asset.js b/core/asset.js index 063c115b..ae74e07b 100644 --- a/core/asset.js +++ b/core/asset.js @@ -1,10 +1,12 @@ /* jslint node: true */ 'use strict'; -var Config = require('./config.js').config; +// ENiGMA½ +const Config = require('./config.js').config; -var _ = require('lodash'); -var assert = require('assert'); +// deps +const _ = require('lodash'); +const assert = require('assert'); exports.parseAsset = parseAsset; exports.getAssetWithShorthand = getAssetWithShorthand; @@ -17,19 +19,20 @@ const ALL_ASSETS = [ 'art', 'menu', 'method', + 'module', 'systemMethod', 'systemModule', 'prompt', 'config', ]; -var ASSET_RE = new RegExp('\\@(' + ALL_ASSETS.join('|') + ')\\:([\\w\\d\\.]*)(?:\\/([\\w\\d\\_]+))*'); +const ASSET_RE = new RegExp('\\@(' + ALL_ASSETS.join('|') + ')\\:([\\w\\d\\.]*)(?:\\/([\\w\\d\\_]+))*'); function parseAsset(s) { - var m = ASSET_RE.exec(s); + const m = ASSET_RE.exec(s); if(m) { - var result = { type : m[1] }; + let result = { type : m[1] }; if(m[3]) { result.location = m[2]; @@ -48,7 +51,7 @@ function getAssetWithShorthand(spec, defaultType) { } if('@' === spec[0]) { - var asset = parseAsset(spec); + const asset = parseAsset(spec); assert(_.isString(asset.type)); return asset; @@ -56,63 +59,48 @@ function getAssetWithShorthand(spec, defaultType) { return { type : defaultType, asset : spec, - } - } -} - -// :TODO: Convert these to getAssetWithShorthand() -function getArtAsset(art) { - if(!_.isString(art)) { - return null; - } - - if('@' === art[0]) { - var artAsset = parseAsset(art); - assert('art' === artAsset.type || 'method' === artAsset.type); - - return artAsset; - } else { - return { - type : 'art', - asset : art, }; } } -function getModuleAsset(module) { - if(!_.isString(module)) { +function getArtAsset(spec) { + const asset = getAssetWithShorthand(spec, 'art'); + + if(!asset) { return null; } - if('@' === module[0]) { - var modAsset = parseAsset(module); - assert('module' === modAsset.type || 'systemModule' === modAsset.type); - - return modAsset; - } else { - return { - type : 'module', - asset : module, - } - } + assert( ['art', 'method' ].indexOf(asset.type) > -1); + return asset; } -function resolveConfigAsset(from) { - var asset = parseAsset(from); +function getModuleAsset(spec) { + const asset = getAssetWithShorthand(spec, 'module'); + + if(!asset) { + return null; + } + + assert( ['module', 'systemModule' ].indexOf(asset.type) > -1); + return asset; +} + +function resolveConfigAsset(spec) { + const asset = parseAsset(spec); if(asset) { assert('config' === asset.type); - var path = asset.asset.split('.'); - var conf = Config; - for(var i = 0; i < path.length; ++i) { + const path = asset.asset.split('.'); + let conf = Config; + for(let i = 0; i < path.length; ++i) { if(_.isUndefined(conf[path[i]])) { - return from; + return spec; } conf = conf[path[i]]; } return conf; } else { - return from; + return spec; } } @@ -122,4 +110,4 @@ function getViewPropertyAsset(src) { } return parseAsset(src); -}; +} diff --git a/core/config.js b/core/config.js index 4436004a..eac3dc53 100644 --- a/core/config.js +++ b/core/config.js @@ -180,6 +180,7 @@ function getDefaultConfig() { themes : paths.join(__dirname, './../mods/themes/'), logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such db : paths.join(__dirname, './../db/'), + modsDb : paths.join(__dirname, './../db/mods/'), dropFiles : paths.join(__dirname, './../dropfiles/'), // + "/node/ misc : paths.join(__dirname, './../misc/'), }, diff --git a/core/connect.js b/core/connect.js index 67ab1c1f..2698fd44 100644 --- a/core/connect.js +++ b/core/connect.js @@ -72,7 +72,7 @@ function ansiQueryTermSizeIfNeeded(client, cb) { function prepareTerminal(term) { term.rawWrite(ansi.normal()); - term.rawWrite(ansi.disableVT100LineWrapping()); + //term.rawWrite(ansi.disableVT100LineWrapping()); // :TODO: set xterm stuff -- see x84/others } diff --git a/core/database.js b/core/database.js index 5029d7e1..a3385509 100644 --- a/core/database.js +++ b/core/database.js @@ -7,15 +7,42 @@ var sqlite3 = require('sqlite3'); var paths = require('path'); var async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); + // database handles var dbs = {}; +exports.getModDatabasePath = getModDatabasePath; exports.initializeDatabases = initializeDatabases; exports.dbs = dbs; function getDatabasePath(name) { - return paths.join(conf.config.paths.db, name + '.sqlite3'); + return paths.join(conf.config.paths.db, `${name}.sqlite3`); +} + +function getModDatabasePath(moduleInfo, suffix) { + // + // Mods that use a database are stored in Config.paths.modsDb (e.g. enigma-bbs/db/mods) + // We expect that moduleInfo defines packageName which will be the base of the modules + // filename. An optional suffix may be supplied as well. + // + const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/; + + assert(_.isObject(moduleInfo)); + assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!'); + + let full = moduleInfo.packageName; + if(suffix) { + full += `.${suffix}`; + } + + assert( + (full.split('.').length > 1 && HOST_RE.test(full)), + 'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation'); + + return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`); } function initializeDatabases(cb) { diff --git a/core/fse.js b/core/fse.js index 7592a11e..b1455e5d 100644 --- a/core/fse.js +++ b/core/fse.js @@ -317,7 +317,7 @@ function FullScreenEditorModule(options) { // in NetRunner: self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3)); - self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2)) + self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2)); } callback(null); }, @@ -424,8 +424,7 @@ function FullScreenEditorModule(options) { async.series( [ function beforeDisplayArt(callback) { - self.beforeArt(); - callback(null); + self.beforeArt(callback); }, function displayHeaderAndBodyArt(callback) { assert(_.isString(art.header)); diff --git a/core/menu_module.js b/core/menu_module.js index e454ff58..03f7e2d6 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -46,8 +46,7 @@ function MenuModule(options) { async.series( [ function beforeDisplayArt(callback) { - self.beforeArt(); - callback(null); + self.beforeArt(callback); }, function displayMenuArt(callback) { if(_.isString(self.menuConfig.art)) { @@ -247,7 +246,7 @@ MenuModule.prototype.leave = function() { this.detachViewControllers(); }; -MenuModule.prototype.beforeArt = function() { +MenuModule.prototype.beforeArt = function(cb) { if(this.cls) { this.client.term.write(ansi.resetScreen()); } @@ -255,6 +254,8 @@ MenuModule.prototype.beforeArt = function() { if(_.isNumber(this.menuConfig.options.baudRate)) { this.client.term.write(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate)); } + + return cb(null); }; MenuModule.prototype.mciReady = function(mciData, cb) { diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 63c5e20b..c9c5ceb2 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -1,15 +1,14 @@ /* jslint node: true */ 'use strict'; -var View = require('./view.js').View; -var miscUtil = require('./misc_util.js'); -var strUtil = require('./string_util.js'); -var ansi = require('./ansi_term.js'); -var colorCodes = require('./color_codes.js'); -var wordWrapText = require('./word_wrap.js').wordWrapText; +const View = require('./view.js').View; +const strUtil = require('./string_util.js'); +const ansi = require('./ansi_term.js'); +const colorCodes = require('./color_codes.js'); +const wordWrapText = require('./word_wrap.js').wordWrapText; -var assert = require('assert'); -var _ = require('lodash'); +const assert = require('assert'); +const _ = require('lodash'); // :TODO: Determine CTRL-* keys for various things // See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt @@ -34,7 +33,7 @@ var _ = require('lodash'); // // Editors - BBS // * https://github.com/M-griffin/Enthral/blob/master/src/msg_fse.cpp -// +// // // Editors - Other // * http://joe-editor.sourceforge.net/ @@ -55,12 +54,12 @@ var _ = require('lodash'); // // To-Do -// +// // * Index pos % for emit scroll events // * Some of this shoudl be async'd where there is lots of processing (e.g. word wrap) // * Fix backspace when col=0 (e.g. bs to prev line) // * Add back word delete -// * +// * var SPECIAL_KEY_MAP_DEFAULT = { @@ -114,6 +113,11 @@ function MultiLineEditTextView(options) { this.topVisibleIndex = 0; this.mode = options.mode || 'edit'; // edit | preview | read-only + if ('preview' === this.mode) { + this.autoScroll = options.autoScroll || true; + } else { + this.autoScroll = options.autoScroll || false; + } // // cursorPos represents zero-based row, col positions // within the editor itself @@ -179,7 +183,7 @@ function MultiLineEditTextView(options) { this.eraseRows = function(startRow, endRow) { self.client.term.rawWrite(self.getSGRFor('text') + ansi.hideCursor()); - + var absPos = self.getAbsolutePosition(startRow, 0); var absPosEnd = self.getAbsolutePosition(endRow, 0); var eraseFiller = new Array(self.dimens.width).join(' '); @@ -216,7 +220,7 @@ function MultiLineEditTextView(options) { if(!_.isNumber(index)) { index = self.getTextLinesIndex(); } - return self.textLines[index].text.replace(/\t/g, ' '); + return self.textLines[index].text.replace(/\t/g, ' '); }; this.getText = function(index) { @@ -266,19 +270,19 @@ function MultiLineEditTextView(options) { } return lines; }; - + this.getOutputText = function(startIndex, endIndex, eolMarker) { let lines = self.getTextLines(startIndex, endIndex); let text = ''; var re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g'); - + lines.forEach(line => { text += line.text.replace(re, '\t'); if(eolMarker && line.eol) { text += eolMarker; - } + } }); - + return text; } @@ -302,7 +306,7 @@ function MultiLineEditTextView(options) { /* this.editTextAtPosition = function(editAction, text, index, col) { switch(editAction) { - case 'insert' : + case 'insert' : self.insertCharactersInText(text, index, col); break; @@ -329,7 +333,7 @@ function MultiLineEditTextView(options) { newLines[newLines.length - 1].eol = true; Array.prototype.splice.apply( - self.textLines, + self.textLines, [ index, (nextEolIndex - index) + 1 ].concat(newLines)); return wrapped.firstWrapRange; @@ -337,7 +341,7 @@ function MultiLineEditTextView(options) { this.removeCharactersFromText = function(index, col, operation, count) { if('right' === operation) { - self.textLines[index].text = + self.textLines[index].text = self.textLines[index].text.slice(col, count) + self.textLines[index].text.slice(col + count); @@ -354,11 +358,11 @@ function MultiLineEditTextView(options) { } else if ('backspace' === operation) { // :TODO: method for splicing text self.textLines[index].text = - self.textLines[index].text.slice(0, col - (count - 1)) + + self.textLines[index].text.slice(0, col - (count - 1)) + self.textLines[index].text.slice(col + 1); self.cursorPos.col -= (count - 1); - + self.updateTextWordWrap(index); self.redrawRows(self.cursorPos.row, self.dimens.height); @@ -405,9 +409,9 @@ function MultiLineEditTextView(options) { this.insertCharactersInText = function(c, index, col) { self.textLines[index].text = [ - self.textLines[index].text.slice(0, col), - c, - self.textLines[index].text.slice(col) + self.textLines[index].text.slice(0, col), + c, + self.textLines[index].text.slice(col) ].join(''); //self.cursorPos.col++; @@ -443,13 +447,13 @@ function MultiLineEditTextView(options) { // absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); self.client.term.write( - ansi.hideCursor() + - self.getSGRFor('text') + + ansi.hideCursor() + + self.getSGRFor('text') + self.getRenderText(index).slice(self.cursorPos.col - c.length) + ansi.goto(absPos.row, absPos.col) + ansi.showCursor(), false ); - } + } }; this.getRemainingTabWidth = function(col) { @@ -541,7 +545,7 @@ function MultiLineEditTextView(options) { .split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g); var wrapped; - + for(var i = 0; i < text.length; ++i) { wrapped = self.wordWrapSingleLine( text[i], // input @@ -556,7 +560,7 @@ function MultiLineEditTextView(options) { }; this.getAbsolutePosition = function(row, col) { - return { + return { row : self.position.row + row, col : self.position.col + col, }; @@ -610,7 +614,7 @@ function MultiLineEditTextView(options) { this.keyPressDown = function() { var lastVisibleRow = Math.min( - self.dimens.height, + self.dimens.height, (self.textLines.length - self.topVisibleIndex)) - 1; if(self.cursorPos.row < lastVisibleRow) { @@ -714,7 +718,7 @@ function MultiLineEditTextView(options) { var nextEolIndex = self.getNextEndOfLineIndex(index); var text = self.getContiguousText(index, nextEolIndex); var newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped; - + newLines.unshift( { text : text.slice(0, self.cursorPos.col), eol : true } ); for(var i = 1; i < newLines.length; ++i) { newLines[i] = { text : newLines[i] }; @@ -722,7 +726,7 @@ function MultiLineEditTextView(options) { newLines[newLines.length - 1].eol = true; Array.prototype.splice.apply( - self.textLines, + self.textLines, [ index, (nextEolIndex - index) + 1 ].concat(newLines)); // redraw from current row to end of visible area @@ -844,9 +848,9 @@ function MultiLineEditTextView(options) { self.client.term.rawWrite(ansi.left(move)); break; - case 'up' : + case 'up' : case 'down' : - // + // // Jump to the tabstop nearest the cursor // var newCol = self.tabStops.reduce(function r(prev, curr) { @@ -890,7 +894,7 @@ function MultiLineEditTextView(options) { this.cursorBeginOfNextLine = function() { // e.g. when scrolling right past eol var linesBelow = self.getRemainingLinesBelowRow(); - + if(linesBelow > 0) { var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1; if(self.cursorPos.row < lastVisibleRow) { @@ -1007,9 +1011,9 @@ MultiLineEditTextView.prototype.setText = function(text) { MultiLineEditTextView.prototype.addText = function(text) { this.insertRawText(text); - if(this.isEditMode()) { + if(this.isEditMode() || this.autoScroll) { this.cursorEndOfDocument(); - } else if(this.isPreviewMode()) { + } else { this.cursorStartOfDocument(); } }; @@ -1020,14 +1024,15 @@ MultiLineEditTextView.prototype.getData = function() { MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { switch(propName) { - case 'mode' : this.mode = value; break; + case 'mode' : this.mode = value; break; + case 'autoScroll' : this.autoScroll = value; break; } MultiLineEditTextView.super_.prototype.setPropertyValue.call(this, propName, value); }; var HANDLED_SPECIAL_KEYS = [ - 'up', 'down', 'left', 'right', + 'up', 'down', 'left', 'right', 'home', 'end', 'page up', 'page down', 'line feed', @@ -1045,7 +1050,7 @@ MultiLineEditTextView.prototype.onKeyPress = function(ch, key) { var self = this; var handled; - if(key) { + if(key) { HANDLED_SPECIAL_KEYS.forEach(function aKey(specialKey) { if(self.isKeyMapped(specialKey, key.name)) { @@ -1068,6 +1073,22 @@ MultiLineEditTextView.prototype.onKeyPress = function(ch, key) { } }; +MultiLineEditTextView.prototype.scrollUp = function() { + this.scrollDocumentUp(); +}; + +MultiLineEditTextView.prototype.scrollDown = function() { + this.scrollDocumentDown(); +}; + +MultiLineEditTextView.prototype.deleteLine = function(line) { + this.textLines.splice(line, 1); +}; + +MultiLineEditTextView.prototype.getLineCount = function() { + return this.textLines.length; +}; + MultiLineEditTextView.prototype.getTextEditMode = function() { return this.overtypeMode ? 'overtype' : 'insert'; }; @@ -1075,11 +1096,10 @@ MultiLineEditTextView.prototype.getTextEditMode = function() { MultiLineEditTextView.prototype.getEditPosition = function() { var currentIndex = this.getTextLinesIndex() + 1; - return { - row : this.getTextLinesIndex(this.cursorPos.row), + return { + row : this.getTextLinesIndex(this.cursorPos.row), col : this.cursorPos.col, percent : Math.floor(((currentIndex / this.textLines.length) * 100)), below : this.getRemainingLinesBelowRow(), }; }; - diff --git a/core/standard_menu.js b/core/standard_menu.js index 9848acb5..4d4b8819 100644 --- a/core/standard_menu.js +++ b/core/standard_menu.js @@ -22,8 +22,8 @@ StandardMenuModule.prototype.enter = function() { StandardMenuModule.super_.prototype.enter.call(this); }; -StandardMenuModule.prototype.beforeArt = function() { - StandardMenuModule.super_.prototype.beforeArt.call(this); +StandardMenuModule.prototype.beforeArt = function(cb) { + StandardMenuModule.super_.prototype.beforeArt.call(this, cb); }; StandardMenuModule.prototype.mciReady = function(mciData, cb) { diff --git a/core/system_menu_method.js b/core/system_menu_method.js index 0299c00e..72621122 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -1,25 +1,23 @@ /* jslint node: true */ 'use strict'; -var theme = require('./theme.js'); -var removeClient = require('./client_connections.js').removeClient; -var ansi = require('./ansi_term.js'); -var userDb = require('./database.js').dbs.user; -var sysProp = require('./system_property.js'); -var userLogin = require('./user_login.js').userLogin; +// ENiGMA½ +const removeClient = require('./client_connections.js').removeClient; +const ansiNormal = require('./ansi_term.js').normal; +const userLogin = require('./user_login.js').userLogin; -var async = require('async'); -var _ = require('lodash'); -var iconv = require('iconv-lite'); +// deps +const _ = require('lodash'); +const iconv = require('iconv-lite'); exports.login = login; exports.logoff = logoff; exports.prevMenu = prevMenu; +exports.nextMenu = nextMenu; -function login(callingMenu, formData, extraArgs) { - var client = callingMenu.client; +function login(callingMenu, formData) { - userLogin(callingMenu.client, formData.value.username, formData.value.password, function authResult(err) { + userLogin(callingMenu.client, formData.value.username, formData.value.password, err => { if(err) { // login failure if(err.existingConn && _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) { @@ -36,32 +34,41 @@ function login(callingMenu, formData, extraArgs) { }); } -function logoff(callingMenu, formData, extraArgs) { +function logoff(callingMenu) { // // Simple logoff. Note that recording of @ logoff properties/stats // occurs elsewhere! // - var client = callingMenu.client; + const client = callingMenu.client; - setTimeout(function timeout() { + setTimeout( () => { // // For giggles... // client.term.write( - ansi.normal() + '\n' + + ansiNormal() + '\n' + iconv.decode(require('crypto').randomBytes(Math.floor(Math.random() * 65) + 20), client.term.outputEncoding) + - 'NO CARRIER', null, function written() { + 'NO CARRIER', null, () => { // after data is written, disconnect & remove the client - removeClient(client); - }); + return removeClient(client); + } + ); }, 500); } -function prevMenu(callingMenu, formData, extraArgs) { - callingMenu.prevMenu(function result(err) { +function prevMenu(callingMenu) { + callingMenu.prevMenu( err => { if(err) { callingMenu.client.log.error( { error : err.toString() }, 'Error attempting to fallback!'); } }); } + +function nextMenu(callingMenu) { + callingMenu.nextMenu( err => { + if(err) { + callingMenu.client.log.error( { error : err.toString() }, 'Error attempting to go to next menu!'); + } + }); +} diff --git a/mods/art/ONEADD.ANS b/mods/art/ONEADD.ANS new file mode 100644 index 00000000..046e6855 Binary files /dev/null and b/mods/art/ONEADD.ANS differ diff --git a/mods/art/ONELINER.ANS b/mods/art/ONELINER.ANS new file mode 100644 index 00000000..cdaa3500 Binary files /dev/null and b/mods/art/ONELINER.ANS differ diff --git a/mods/art/erc.ans b/mods/art/erc.ans new file mode 100644 index 00000000..d2f336d2 Binary files /dev/null and b/mods/art/erc.ans differ diff --git a/mods/erc_client.js b/mods/erc_client.js new file mode 100644 index 00000000..cb30f2d0 --- /dev/null +++ b/mods/erc_client.js @@ -0,0 +1,174 @@ +/* jslint node: true */ +'use strict'; + +var MenuModule = require('../core/menu_module.js').MenuModule; + +// deps +const async = require('async'); +const _ = require('lodash'); +const net = require('net'); + +/* + Expected configuration block example: + + config: { + host: 192.168.1.171 + port: 5001 + bbsTag: SOME_TAG + } + +*/ + +exports.getModule = ErcClientModule; + +exports.moduleInfo = { + name : 'ENiGMA Relay Chat Client', + desc : 'Chat with other ENiGMA BBSes', + author : 'Andrew Pamment', +}; + +var MciViewIds = { + ChatDisplay : 1, + InputArea : 3, +}; + +function ErcClientModule(options) { + MenuModule.call(this, options); + + const self = this; + this.config = options.menuConfig.config; + + this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}'; + this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}'; + + this.finishedLoading = function() { + async.waterfall( + [ + function validateConfig(callback) { + if(_.isString(self.config.host) && + _.isNumber(self.config.port) && + _.isString(self.config.bbsTag)) + { + return callback(null); + } else { + return callback(new Error('Configuration is missing required option(s)')); + } + }, + function connectToServer(callback) { + const connectOpts = { + port : self.config.port, + host : self.config.host, + }; + + const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); + + chatMessageView.setText('Connecting to server...'); + chatMessageView.redraw(); + + self.viewControllers.menu.switchFocus(MciViewIds.InputArea); + + // :TODO: Track actual client->enig connection for optional prevMenu @ final CB + self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host); + + self.chatConnection.on('data', data => { + data = data.toString(); + + if(data.startsWith('ERCHANDSHAKE')) { + self.chatConnection.write(`ERCMAGIC|${self.config.bbsTag}|${self.client.user.username}\r\n`); + } else if(data.startsWith('{')) { + try { + data = JSON.parse(data); + } catch(e) { + return self.client.log.warn( { error : e.message }, 'ERC: Error parsing ERC data from server'); + } + + let text; + try { + if(data.userName) { + // user message + text = self.chatEntryFormat.format(data); + } else { + // system message + text = self.systemEntryFormat.format(data); + } + } catch(e) { + return self.client.log.warn( { error : e.message }, 'ERC: chatEntryFormat error'); + } + + chatMessageView.addText(text); + + if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height? + chatMessageView.deleteLine(0); + chatMessageView.scrollDown(); + } + + chatMessageView.redraw(); + self.viewControllers.menu.switchFocus(MciViewIds.InputArea); + } + }); + + self.chatConnection.once('end', () => { + return callback(null); + }); + + self.chatConnection.once('error', err => { + self.client.log.info(`ERC connection error: ${err.message}`); + return callback(new Error('Failed connecting to ERC server!')); + }); + } + ], + err => { + if(err) { + self.client.log.warn( { error : err.message }, 'ERC error'); + } + + self.prevMenu(); + } + ); + }; + + this.scrollHandler = function(keyName) { + const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); + const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); + + if('up arrow' === keyName) { + chatDisplayView.scrollUp(); + } else { + chatDisplayView.scrollDown(); + } + + chatDisplayView.redraw(); + inputAreaView.setFocus(true); + }; + + + this.menuMethods = { + inputAreaSubmit : function() { + const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); + const inputData = inputAreaView.getData(); + + if('/quit' === inputData.toLowerCase()) { + self.chatConnection.end(); + } else { + try { + self.chatConnection.write(`${inputData}\r\n`); + } catch(e) { + self.client.log.warn( { error : e.message }, 'ERC error'); + } + inputAreaView.clearText(); + } + }, + scrollUp : function(formData) { + self.scrollHandler(formData.key.name); + }, + scrollDown : function(formData) { + self.scrollHandler(formData.key.name); + } + }; +} + +require('util').inherits(ErcClientModule, MenuModule); + +ErcClientModule.prototype.mciReady = function(mciData, cb) { + this.standardMCIReadyHandler(mciData, cb); +}; diff --git a/mods/menu.hjson b/mods/menu.hjson index 5618f111..1ed12a98 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -1,4 +1,4 @@ -{ +{ /* ENiGMA½ Menu Configuration @@ -29,7 +29,7 @@ // // Another SSH specialization: If the user logs in with a new user - // name (e.g. "new", "apply", ...) they will be directed to the + // name (e.g. "new", "apply", ...) they will be directed to the // application process. // sshConnectedNewUser: { @@ -157,7 +157,7 @@ } } - logoff: { + logoff: { art: LOGOFF desc: Logging Off next: @systemMethod:logoff @@ -264,7 +264,7 @@ action: @systemMethod:prevMenu } ] - } + } } } @@ -361,7 +361,7 @@ action: @systemMethod:prevMenu } ] - } + } } } @@ -375,10 +375,10 @@ status: Feedback to SysOp module: msg_area_post_fse next: [ - { + { acs: AS2 next: fullLoginSequenceLoginArt - } + } { next: newUserInactiveDone } @@ -510,16 +510,103 @@ module: last_callers art: LASTCALL options: { pause: true } - next: fullLoginSequenceWhosOnline + next: fullLoginSequenceWhosOnline } fullLoginSequenceWhosOnline: { desc: Who's Online module: whos_online art: WHOSON options: { pause: true } - next: fullLoginSequenceNewScanConfirm + next: fullLoginSequenceOnelinerz } + fullLoginSequenceOnelinerz: { + desc: Viewing Onelinerz + module: onelinerz + next: fullLoginSequenceNewScanConfirm + options: { + cls: true + } + config: { + art: { + entries: 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 @@ -644,6 +731,14 @@ value: { command: "K" } action: @menu:mainMenuFeedbackToSysOp } + { + value: { command: "O" } + action: @menu:mainMenuOnelinerz + } + { + value: { command: "CHAT"} + action: @menu:ercClient + } { value: 1 action: @menu:mainMenu @@ -665,7 +760,7 @@ mainMenuUserStats: { desc: User Stats art: STATUS - options: { pause: true } + options: { pause: true } } mainMenuSystemStats: { desc: System Stats @@ -907,6 +1002,144 @@ } } + mainMenuOnelinerz: { + desc: Viewing Onelinerz + module: onelinerz + options: { + cls: true + } + config: { + art: { + entries: 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 + } + ] + } + } + } + + 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 + } + ] + } + } + } + /////////////////////////////////////////////////////////////////////// // Doors Menu /////////////////////////////////////////////////////////////////////// @@ -943,17 +1176,21 @@ value: { command: "DP" } action: @menu:doorParty } + { + value: { command: "HL" } + action: @menu:telnetBridgeHappyLand + } ] } doorPimpWars: { desc: Playing PimpWars - module: abracadabra + module: abracadabra config: { name: PimpWars dropFileType: DORINFO cmd: /home/nuskooler/DOS/scripts/pimpwars.sh - args: [ + args: [ "{node}", "{dropFile}", "{srvPort}", @@ -966,12 +1203,12 @@ doorDarkLands: { desc: Playing Dark Lands - module: abracadabra + module: abracadabra config: { name: DARKLANDS dropFileType: DOOR cmd: /home/nuskooler/dev/enigma-bbs/doors/darklands/start.sh - args: [ + args: [ "{node}", "{dropFile}", "{srvPort}", @@ -981,7 +1218,7 @@ io: socket } } - + doorLORD: { desc: Playing L.O.R.D. module: abracadabra @@ -1020,6 +1257,18 @@ bbsTag: XX } } + + telnetBridgeHappyLand: { + desc: Connected to HappyLand BBS + module: telnet_bridge + config: { + host: andrew.homeunix.org + port: 2023 + //host: agency.bbs.geek.nz + //port: 23 + } + } + /////////////////////////////////////////////////////////////////////// // Message Area Menu /////////////////////////////////////////////////////////////////////// @@ -1056,7 +1305,7 @@ { value: 1 action: @menu:messageArea - } + } ] } @@ -1244,7 +1493,7 @@ { keys: [ "n", "shift + n" ] action: @method:nextMessage - } + } { keys: [ "r", "shift + r" ] action: @method:replyMessage @@ -1259,7 +1508,7 @@ { keys: [ "?" ] action: @method:viewModeMenuHelp - } + } { keys: [ "down arrow", "up arrow", "page up", "page down" ] action: @method:movementKeyPressed @@ -1295,7 +1544,7 @@ validate: @systemMethod:validateNonEmpty } ET3: { - argName: subject + argName: subject maxLength: 72 submit: true validate: @systemMethod:validateNonEmpty @@ -1395,7 +1644,7 @@ width: 79 height: 4 argName: quote - } + } } submit: { @@ -1552,7 +1801,7 @@ "mci" : { "VM1" : { "items" : [ - "Single Line Text Editing Views", + "Single Line Text Editing Views", "Spinner & Toggle Views", "Mask Edit Views", "Multi Line Text Editor", @@ -1735,7 +1984,7 @@ "form" : { "0" : { "BTMT" : { - "mci" : { + "mci" : { "MT1" : { "width" : 70, "height" : 17, @@ -2019,6 +2268,6 @@ } } } - } + } } -} \ No newline at end of file +} diff --git a/mods/onelinerz.js b/mods/onelinerz.js new file mode 100644 index 00000000..80bd301e --- /dev/null +++ b/mods/onelinerz.js @@ -0,0 +1,315 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const MenuModule = require('../core/menu_module.js').MenuModule; +const getModDatabasePath = require('../core/database.js').getModDatabasePath; +const ViewController = require('../core/view_controller.js').ViewController; +const theme = require('../core/theme.js'); +const ansi = require('../core/ansi_term.js'); + +// deps +const sqlite3 = require('sqlite3'); +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); + +exports.moduleInfo = { + name : 'Onelinerz', + desc : 'Standard local onelinerz', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.onelinerz', +}; + +exports.getModule = OnelinerzModule; + +const MciCodeIds = { + ViewForm : { + Entries : 1, + AddPrompt : 2, + }, + AddForm : { + NewEntry : 1, + EntryPreview : 2, + AddPrompt : 3, + } +}; + +const FormIds = { + View : 0, + Add : 1, +}; + +function OnelinerzModule(options) { + MenuModule.call(this, options); + + const self = this; + const config = this.menuConfig.config; + + this.initSequence = function() { + async.series( + [ + function beforeDisplayArt(callback) { + self.beforeArt(callback); + }, + function display(callback) { + self.displayViewScreen(false, callback); + } + ], + err => { + if(err) { + // :TODO: Handle me -- initSequence() should really take a completion callback + } + self.finishedLoading(); + } + ); + }; + + this.displayViewScreen = function(clearScreen, cb) { + async.waterfall( + [ + function clearAndDisplayArt(callback) { + if(self.viewControllers.add) { + self.viewControllers.add.setFocus(false); + } + + if(clearScreen) { + self.client.term.rawWrite(ansi.resetScreen()); + } + + theme.displayThemedAsset( + config.art.entries, + self.client, + { font : self.menuConfig.font, trailingLF : false }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function initOrRedrawViewController(artData, callback) { + if(_.isUndefined(self.viewControllers.add)) { + const vc = self.addViewController( + 'view', + new ViewController( { client : self.client, formId : FormIds.View } ) + ); + + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.View, + }; + + return vc.loadFromMenuConfig(loadOpts, callback); + } else { + self.viewControllers.view.setFocus(true); + self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw(); + return callback(null); + } + }, + function fetchEntries(callback) { + const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries); + const limit = entriesView.dimens.height; + let entries = []; + + self.db.each( + `SELECT * + FROM ( + SELECT * + FROM onelinerz + ORDER BY timestamp DESC + LIMIT ${limit} + ) + ORDER BY timestamp ASC;`, + (err, row) => { + if(!err) { + row.timestamp = moment(row.timestamp); // convert -> moment + entries.push(row); + } + }, + err => { + return callback(err, entriesView, entries); + } + ); + }, + function populateEntries(entriesView, entries, callback) { + const listFormat = config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent + const tsFormat = config.timestampFormat || 'ddd h:mma'; + + entriesView.setItems(entries.map( e => { + return listFormat.format( { + userId : e.user_id, + username : e.user_name, + oneliner : e.oneliner, + ts : e.timestamp.format(tsFormat), + } ); + })); + + entriesView.focusItems = entriesView.items; // :TODO: this is a hack + entriesView.redraw(); + + return callback(null); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + }; + + this.displayAddScreen = function(cb) { + async.waterfall( + [ + function clearAndDisplayArt(callback) { + self.viewControllers.view.setFocus(false); + self.client.term.rawWrite(ansi.resetScreen()); + + theme.displayThemedAsset( + config.art.add, + self.client, + { font : self.menuConfig.font }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function initOrRedrawViewController(artData, callback) { + if(_.isUndefined(self.viewControllers.add)) { + const vc = self.addViewController( + 'add', + new ViewController( { client : self.client, formId : FormIds.Add } ) + ); + + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.Add, + }; + + return vc.loadFromMenuConfig(loadOpts, callback); + } else { + self.viewControllers.add.setFocus(true); + self.viewControllers.add.redrawAll(); + self.viewControllers.add.switchFocus(MciCodeIds.AddForm.NewEntry); + return callback(null); + } + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + }; + + this.clearAddForm = function() { + const newEntryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); + const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); + + newEntryView.setText(''); + + // preview is optional + if(previewView) { + previewView.setText(''); + } + }; + + this.menuMethods = { + viewAddScreen : function() { + self.displayAddScreen(); + }, + + addEntry : function(formData) { + if(_.isString(formData.value.oneliner) && formData.value.oneliner.length > 0) { + const oneliner = formData.value.oneliner.trim(); // remove any trailing ws + + self.storeNewOneliner(oneliner, err => { + if(err) { + self.client.log.warn( { error : err.message }, 'Failed saving oneliner'); + } + + self.clearAddForm(); + self.displayViewScreen(true); // true=cls + }); + + } else { + // empty message - treat as if cancel was hit + self.displayViewScreen(true); // true=cls + } + }, + + cancelAdd : function() { + self.clearAddForm(); + self.displayViewScreen(true); // true=cls + } + }; + + this.initDatabase = function(cb) { + async.series( + [ + function openDatabase(callback) { + self.db = new sqlite3.Database( + getModDatabasePath(exports.moduleInfo), + callback + ); + }, + function createTables(callback) { + self.db.serialize( () => { + self.db.run( + `CREATE TABLE IF NOT EXISTS onelinerz ( + id INTEGER PRIMARY KEY, + user_id INTEGER_NOT NULL, + user_name VARCHAR NOT NULL, + oneliner VARCHAR NOT NULL, + timestamp DATETIME NOT NULL + )` + ); + }); + callback(null); + } + ], + cb + ); + }; + + this.storeNewOneliner = function(oneliner, cb) { + const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + + async.series( + [ + function addRec(callback) { + self.db.run( + `INSERT INTO onelinerz (user_id, user_name, oneliner, timestamp) + VALUES (?, ?, ?, ?);`, + [ self.client.user.userId, self.client.user.username, oneliner, ts ], + callback + ); + }, + function removeOld(callback) { + // keep 25 max most recent items - remove the older ones + self.db.run( + `DELETE FROM onelinerz + WHERE id IN ( + SELECT id + FROM onelinerz + ORDER BY id DESC + LIMIT -1 OFFSET 25 + );`, + callback + ); + } + ], + cb + ); + }; +} + +require('util').inherits(OnelinerzModule, MenuModule); + +OnelinerzModule.prototype.beforeArt = function(cb) { + OnelinerzModule.super_.prototype.beforeArt.call(this, err => { + return err ? cb(err) : this.initDatabase(cb); + }); +}; diff --git a/mods/telnet_bridge.js b/mods/telnet_bridge.js new file mode 100644 index 00000000..6f7f4366 --- /dev/null +++ b/mods/telnet_bridge.js @@ -0,0 +1,115 @@ +/* jslint node: true */ +'use strict'; + +const MenuModule = require('../core/menu_module.js').MenuModule; +const resetScreen = require('../core/ansi_term.js').resetScreen; + +const async = require('async'); +const _ = require('lodash'); +const net = require('net'); + +/* + Expected configuration block: + + { + module: telnet_bridge + ... + config: { + host: somehost.net + port: 23 + } + } +*/ + +// :TODO: BUG: When a client disconnects, it's not handled very well -- the log is spammed with tons of errors +// :TODO: ENH: Support nodeMax and tooManyArt + +exports.getModule = TelnetBridgeModule; + +exports.moduleInfo = { + name : 'Telnet Bridge', + desc : 'Connect to other Telnet Systems', + author : 'Andrew Pamment', +}; + + +function TelnetBridgeModule(options) { + MenuModule.call(this, options); + + const self = this; + this.config = options.menuConfig.config; + + this.initSequence = function() { + let clientTerminated; + + async.series( + [ + function validateConfig(callback) { + if(_.isString(self.config.host) && + _.isNumber(self.config.port)) + { + callback(null); + } else { + callback(new Error('Configuration is missing required option(s)')); + } + }, + function createTelnetBridge(callback) { + const connectOpts = { + port : self.config.port, + host : self.config.host, + }; + + let clientTerminated; + + self.client.term.write(resetScreen()); + self.client.term.write(` Connecting to ${connectOpts.host}, please wait...\n`); + + let bridgeConnection = net.createConnection(connectOpts, () => { + self.client.log.info(connectOpts, 'Telnet bridge connection established'); + + self.client.term.output.pipe(bridgeConnection); + + self.client.once('end', () => { + self.client.log.info('Connection ended. Terminating connection'); + clientTerminated = true; + return bridgeConnection.end(); + }); + }); + + const restorePipe = function() { + self.client.term.output.unpipe(bridgeConnection); + self.client.term.output.resume(); + }; + + bridgeConnection.on('data', data => { + // pass along + // :TODO: just pipe this as well + return self.client.term.rawWrite(data); + }); + + bridgeConnection.once('end', () => { + restorePipe(); + return callback(clientTerminated ? new Error('Client connection terminated') : null); + }); + + bridgeConnection.once('error', err => { + self.client.log.info(`Telnet bridge connection error: ${err.message}`); + restorePipe(); + return callback(err); + }); + } + ], + err => { + if(err) { + self.client.log.warn( { error : err.message }, 'Telnet connection error'); + } + + if(!clientTerminated) { + self.prevMenu(); + } + } + ); + }; +} + +require('util').inherits(TelnetBridgeModule, MenuModule); diff --git a/mods/themes/luciano_blocktronics/MMENU.ANS b/mods/themes/luciano_blocktronics/MMENU.ANS index 35950215..0fc985de 100644 Binary files a/mods/themes/luciano_blocktronics/MMENU.ANS and b/mods/themes/luciano_blocktronics/MMENU.ANS differ diff --git a/mods/themes/luciano_blocktronics/ONELINER.ANS b/mods/themes/luciano_blocktronics/ONELINER.ANS new file mode 100644 index 00000000..b494dc7b Binary files /dev/null and b/mods/themes/luciano_blocktronics/ONELINER.ANS differ diff --git a/mods/themes/luciano_blocktronics/theme.hjson b/mods/themes/luciano_blocktronics/theme.hjson index b6d98b9a..ac359ac8 100644 --- a/mods/themes/luciano_blocktronics/theme.hjson +++ b/mods/themes/luciano_blocktronics/theme.hjson @@ -84,6 +84,29 @@ } } + fullLoginSequenceOnelinerz: { + config: { + listFormat: "|00|11{username:<12}|08: |03{oneliner:<59.58}" + } + 0: { + mci: { + VM1: { height: 10 } + TM2: { + focusTextStyle: first lower + } + } + } + 1: { + mci: { + ET1: { width: 60 } + TL2: { width: 60 } + TM3: { + focusTextStyle: first lower + } + } + } + } + mainMenuUserStats: { mci: { UN1: { width: 17 } @@ -157,6 +180,30 @@ } } + mainMenuOnelinerz: { + // :TODO: Need way to just duplicate entry here & in menu.hjson, e.g. use: someName + must supply next/etc. in menu + config: { + listFormat: "|00|11{username:<12}|08: |03{oneliner:<59.58}" + } + 0: { + mci: { + VM1: { height: 10 } + TM2: { + focusTextStyle: first lower + } + } + } + 1: { + mci: { + ET1: { width: 60 } + TL2: { width: 60 } + TM3: { + focusTextStyle: first lower + } + } + } + } + messageAreaMessageList: { config: { listFormat: "|00|15{msgNum:>4} |03{subj:<29.29} |11{from:<20.20} |03{ts} |01|31{newIndicator}" @@ -336,6 +383,12 @@ } } } + + ercClient: { + config: { + //chatEntryFormat: "|00|08[|03{bbsTag}|08] |10{userName}|08: |02{message}" + } + } } } } \ No newline at end of file