Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs

This commit is contained in:
Bryan Ashby 2016-06-30 23:04:36 -06:00
commit e213fdd696
20 changed files with 1150 additions and 223 deletions

View File

@ -12,9 +12,13 @@
// * http://www.inwap.com/pdp10/ansicode.txt // * http://www.inwap.com/pdp10/ansicode.txt
// //
const assert = require('assert'); // ENiGMA½
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
// deps
const assert = require('assert');
const _ = require('lodash');
exports.getFGColorValue = getFGColorValue; exports.getFGColorValue = getFGColorValue;
exports.getBGColorValue = getBGColorValue; exports.getBGColorValue = getBGColorValue;
exports.sgr = sgr; exports.sgr = sgr;
@ -23,7 +27,6 @@ exports.clearScreen = clearScreen;
exports.resetScreen = resetScreen; exports.resetScreen = resetScreen;
exports.normal = normal; exports.normal = normal;
exports.goHome = goHome; exports.goHome = goHome;
//exports.deleteLine = deleteLine;
exports.disableVT100LineWrapping = disableVT100LineWrapping; exports.disableVT100LineWrapping = disableVT100LineWrapping;
exports.setSyncTERMFont = setSyncTERMFont; exports.setSyncTERMFont = setSyncTERMFont;
exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias; exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias;
@ -35,9 +38,9 @@ exports.setEmulatedBaudRate = setEmulatedBaudRate;
// See also // See also
// https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js // 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', up : 'A',
down : 'B', down : 'B',
@ -124,7 +127,7 @@ var CONTROL = {
// Select Graphics Rendition // Select Graphics Rendition
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt // See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
// //
var SGRValues = { const SGRValues = {
reset : 0, reset : 0,
bold : 1, bold : 1,
dim : 2, dim : 2,
@ -180,7 +183,7 @@ function getBGColorValue(name) {
// //
// See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // 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', 'cp437',
'cp1251', 'cp1251',
'koi8_r', 'koi8_r',
@ -233,7 +236,7 @@ var SYNCTERM_FONT_AND_ENCODING_TABLE = [
// This table contains lowercased entries with any spaces // This table contains lowercased entries with any spaces
// replaced with '_' for lookup purposes. // replaced with '_' for lookup purposes.
// //
var FONT_ALIAS_TO_SYNCTERM_MAP = { const FONT_ALIAS_TO_SYNCTERM_MAP = {
'cp437' : 'cp437', 'cp437' : 'cp437',
'ibm_vga' : 'cp437', 'ibm_vga' : 'cp437',
'ibmpc' : 'cp437', 'ibmpc' : 'cp437',
@ -280,13 +283,13 @@ var FONT_ALIAS_TO_SYNCTERM_MAP = {
}; };
function setSyncTERMFont(name, fontPage) { function setSyncTERMFont(name, fontPage) {
var p1 = miscUtil.valueWithDefault(fontPage, 0); const p1 = miscUtil.valueWithDefault(fontPage, 0);
assert(p1 >= 0 && p1 <= 3); 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) { if(p2 > -1) {
return ESC_CSI + p1 + ';' + p2 + ' D'; return `${ESC_CSI}${p1};${p2} D`;
} }
return ''; return '';
@ -296,7 +299,7 @@ function getSyncTERMFontFromAlias(alias) {
return FONT_ALIAS_TO_SYNCTERM_MAP[alias.toLowerCase().replace(/ /g, '_')]; return FONT_ALIAS_TO_SYNCTERM_MAP[alias.toLowerCase().replace(/ /g, '_')];
} }
var DEC_CURSOR_STYLE = { const DEC_CURSOR_STYLE = {
'blinking block' : 0, 'blinking block' : 0,
'default' : 1, 'default' : 1,
'steady block' : 2, 'steady block' : 2,
@ -307,9 +310,9 @@ var DEC_CURSOR_STYLE = {
}; };
function setCursorStyle(cursorStyle) { function setCursorStyle(cursorStyle) {
var ps = DEC_CURSOR_STYLE[cursorStyle]; const ps = DEC_CURSOR_STYLE[cursorStyle];
if(ps) { if(ps) {
return ESC_CSI + ps + ' q'; return `${ESC_CSI}${ps} q`;
} }
return ''; return '';
@ -317,24 +320,24 @@ function setCursorStyle(cursorStyle) {
// Create methods such as up(), nextLine(),... // Create methods such as up(), nextLine(),...
Object.keys(CONTROL).forEach(function onControlName(name) { Object.keys(CONTROL).forEach(function onControlName(name) {
var code = CONTROL[name]; const code = CONTROL[name];
exports[name] = function() { exports[name] = function() {
var c = code; let c = code;
if(arguments.length > 0) { if(arguments.length > 0) {
// arguments are array like -- we want an array // arguments are array like -- we want an array
c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code; 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(), ... // Create various color methods such as white(), yellowBG(), reset(), ...
Object.keys(SGRValues).forEach(function onSgrName(name) { Object.keys(SGRValues).forEach( name => {
var code = SGRValues[name]; const code = SGRValues[name];
exports[name] = function() { exports[name] = function() {
return ESC_CSI + code + 'm'; return `${ESC_CSI}${code}m`;
}; };
}); });
@ -348,27 +351,19 @@ function sgr() {
return ''; return '';
} }
var result = ''; let result = [];
const args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
// :TODO: this method needs a lot of cleanup! 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);
}
}
var args = Array.isArray(arguments[0]) ? arguments[0] : arguments; return `${ESC_CSI}${result.join(';')}m`;
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];
}
}
return ESC_CSI + result + 'm';
} }
// //
@ -376,10 +371,10 @@ function sgr() {
// to a ANSI SGR sequence. // to a ANSI SGR sequence.
// //
function getSGRFromGraphicRendition(graphicRendition, initialReset) { function getSGRFromGraphicRendition(graphicRendition, initialReset) {
var sgrSeq = []; let sgrSeq = [];
let styleCount = 0;
var styleCount = 0; [ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => {
[ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach(function style(s) {
if(graphicRendition[s]) { if(graphicRendition[s]) {
sgrSeq.push(graphicRendition[s]); sgrSeq.push(graphicRendition[s]);
++styleCount; ++styleCount;
@ -414,7 +409,7 @@ function clearScreen() {
} }
function resetScreen() { function resetScreen() {
return exports.reset() + exports.eraseData(2) + exports.goHome(); return `${exports.reset()}${exports.eraseData(2)}${exports.goHome()}`;
} }
function normal() { function normal() {
@ -426,40 +421,23 @@ function goHome() {
} }
// //
// Delete line(s) // Disable auto line wraping @ termWidth
// This method acts like ESC[ p1 M but should work
// for all terminals via using eraseLine and movement
// //
/* // See:
function deleteLine(count) { // http://stjarnhimlen.se/snippets/vt100.txt
count = count || 1; // https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
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://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() { function disableVT100LineWrapping() {
return ESC_CSI + '7l'; return `${ESC_CSI}?7l`;
} }
function setEmulatedBaudRate(rate) { function setEmulatedBaudRate(rate) {
var speed = { const speed = {
unlimited : 0, unlimited : 0,
off : 0, off : 0,
0 : 0, 0 : 0,

View File

@ -1,10 +1,12 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var Config = require('./config.js').config; // ENiGMA½
const Config = require('./config.js').config;
var _ = require('lodash'); // deps
var assert = require('assert'); const _ = require('lodash');
const assert = require('assert');
exports.parseAsset = parseAsset; exports.parseAsset = parseAsset;
exports.getAssetWithShorthand = getAssetWithShorthand; exports.getAssetWithShorthand = getAssetWithShorthand;
@ -17,19 +19,20 @@ const ALL_ASSETS = [
'art', 'art',
'menu', 'menu',
'method', 'method',
'module',
'systemMethod', 'systemMethod',
'systemModule', 'systemModule',
'prompt', 'prompt',
'config', '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) { function parseAsset(s) {
var m = ASSET_RE.exec(s); const m = ASSET_RE.exec(s);
if(m) { if(m) {
var result = { type : m[1] }; let result = { type : m[1] };
if(m[3]) { if(m[3]) {
result.location = m[2]; result.location = m[2];
@ -48,7 +51,7 @@ function getAssetWithShorthand(spec, defaultType) {
} }
if('@' === spec[0]) { if('@' === spec[0]) {
var asset = parseAsset(spec); const asset = parseAsset(spec);
assert(_.isString(asset.type)); assert(_.isString(asset.type));
return asset; return asset;
@ -56,63 +59,48 @@ function getAssetWithShorthand(spec, defaultType) {
return { return {
type : defaultType, type : defaultType,
asset : spec, 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) { function getArtAsset(spec) {
if(!_.isString(module)) { const asset = getAssetWithShorthand(spec, 'art');
if(!asset) {
return null; return null;
} }
if('@' === module[0]) { assert( ['art', 'method' ].indexOf(asset.type) > -1);
var modAsset = parseAsset(module); return asset;
assert('module' === modAsset.type || 'systemModule' === modAsset.type);
return modAsset;
} else {
return {
type : 'module',
asset : module,
}
}
} }
function resolveConfigAsset(from) { function getModuleAsset(spec) {
var asset = parseAsset(from); 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) { if(asset) {
assert('config' === asset.type); assert('config' === asset.type);
var path = asset.asset.split('.'); const path = asset.asset.split('.');
var conf = Config; let conf = Config;
for(var i = 0; i < path.length; ++i) { for(let i = 0; i < path.length; ++i) {
if(_.isUndefined(conf[path[i]])) { if(_.isUndefined(conf[path[i]])) {
return from; return spec;
} }
conf = conf[path[i]]; conf = conf[path[i]];
} }
return conf; return conf;
} else { } else {
return from; return spec;
} }
} }
@ -122,4 +110,4 @@ function getViewPropertyAsset(src) {
} }
return parseAsset(src); return parseAsset(src);
}; }

View File

@ -180,6 +180,7 @@ function getDefaultConfig() {
themes : paths.join(__dirname, './../mods/themes/'), themes : paths.join(__dirname, './../mods/themes/'),
logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such
db : paths.join(__dirname, './../db/'), db : paths.join(__dirname, './../db/'),
modsDb : paths.join(__dirname, './../db/mods/'),
dropFiles : paths.join(__dirname, './../dropfiles/'), // + "/node<x>/ dropFiles : paths.join(__dirname, './../dropfiles/'), // + "/node<x>/
misc : paths.join(__dirname, './../misc/'), misc : paths.join(__dirname, './../misc/'),
}, },

View File

@ -72,7 +72,7 @@ function ansiQueryTermSizeIfNeeded(client, cb) {
function prepareTerminal(term) { function prepareTerminal(term) {
term.rawWrite(ansi.normal()); term.rawWrite(ansi.normal());
term.rawWrite(ansi.disableVT100LineWrapping()); //term.rawWrite(ansi.disableVT100LineWrapping());
// :TODO: set xterm stuff -- see x84/others // :TODO: set xterm stuff -- see x84/others
} }

View File

@ -7,15 +7,42 @@ var sqlite3 = require('sqlite3');
var paths = require('path'); var paths = require('path');
var async = require('async'); var async = require('async');
const _ = require('lodash');
const assert = require('assert');
// database handles // database handles
var dbs = {}; var dbs = {};
exports.getModDatabasePath = getModDatabasePath;
exports.initializeDatabases = initializeDatabases; exports.initializeDatabases = initializeDatabases;
exports.dbs = dbs; exports.dbs = dbs;
function getDatabasePath(name) { 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) { function initializeDatabases(cb) {

View File

@ -317,7 +317,7 @@ function FullScreenEditorModule(options) {
// in NetRunner: // in NetRunner:
self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3)); 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); callback(null);
}, },
@ -424,8 +424,7 @@ function FullScreenEditorModule(options) {
async.series( async.series(
[ [
function beforeDisplayArt(callback) { function beforeDisplayArt(callback) {
self.beforeArt(); self.beforeArt(callback);
callback(null);
}, },
function displayHeaderAndBodyArt(callback) { function displayHeaderAndBodyArt(callback) {
assert(_.isString(art.header)); assert(_.isString(art.header));

View File

@ -46,8 +46,7 @@ function MenuModule(options) {
async.series( async.series(
[ [
function beforeDisplayArt(callback) { function beforeDisplayArt(callback) {
self.beforeArt(); self.beforeArt(callback);
callback(null);
}, },
function displayMenuArt(callback) { function displayMenuArt(callback) {
if(_.isString(self.menuConfig.art)) { if(_.isString(self.menuConfig.art)) {
@ -247,7 +246,7 @@ MenuModule.prototype.leave = function() {
this.detachViewControllers(); this.detachViewControllers();
}; };
MenuModule.prototype.beforeArt = function() { MenuModule.prototype.beforeArt = function(cb) {
if(this.cls) { if(this.cls) {
this.client.term.write(ansi.resetScreen()); this.client.term.write(ansi.resetScreen());
} }
@ -255,6 +254,8 @@ MenuModule.prototype.beforeArt = function() {
if(_.isNumber(this.menuConfig.options.baudRate)) { if(_.isNumber(this.menuConfig.options.baudRate)) {
this.client.term.write(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate)); this.client.term.write(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate));
} }
return cb(null);
}; };
MenuModule.prototype.mciReady = function(mciData, cb) { MenuModule.prototype.mciReady = function(mciData, cb) {

View File

@ -1,15 +1,14 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var View = require('./view.js').View; const View = require('./view.js').View;
var miscUtil = require('./misc_util.js'); const strUtil = require('./string_util.js');
var strUtil = require('./string_util.js'); const ansi = require('./ansi_term.js');
var ansi = require('./ansi_term.js'); const colorCodes = require('./color_codes.js');
var colorCodes = require('./color_codes.js'); const wordWrapText = require('./word_wrap.js').wordWrapText;
var wordWrapText = require('./word_wrap.js').wordWrapText;
var assert = require('assert'); const assert = require('assert');
var _ = require('lodash'); const _ = require('lodash');
// :TODO: Determine CTRL-* keys for various things // :TODO: Determine CTRL-* keys for various things
// See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt // See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt
@ -114,6 +113,11 @@ function MultiLineEditTextView(options) {
this.topVisibleIndex = 0; this.topVisibleIndex = 0;
this.mode = options.mode || 'edit'; // edit | preview | read-only 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 // cursorPos represents zero-based row, col positions
// within the editor itself // within the editor itself
@ -1007,9 +1011,9 @@ MultiLineEditTextView.prototype.setText = function(text) {
MultiLineEditTextView.prototype.addText = function(text) { MultiLineEditTextView.prototype.addText = function(text) {
this.insertRawText(text); this.insertRawText(text);
if(this.isEditMode()) { if(this.isEditMode() || this.autoScroll) {
this.cursorEndOfDocument(); this.cursorEndOfDocument();
} else if(this.isPreviewMode()) { } else {
this.cursorStartOfDocument(); this.cursorStartOfDocument();
} }
}; };
@ -1021,6 +1025,7 @@ MultiLineEditTextView.prototype.getData = function() {
MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) {
switch(propName) { 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); MultiLineEditTextView.super_.prototype.setPropertyValue.call(this, propName, value);
@ -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() { MultiLineEditTextView.prototype.getTextEditMode = function() {
return this.overtypeMode ? 'overtype' : 'insert'; return this.overtypeMode ? 'overtype' : 'insert';
}; };
@ -1082,4 +1103,3 @@ MultiLineEditTextView.prototype.getEditPosition = function() {
below : this.getRemainingLinesBelowRow(), below : this.getRemainingLinesBelowRow(),
}; };
}; };

View File

@ -22,8 +22,8 @@ StandardMenuModule.prototype.enter = function() {
StandardMenuModule.super_.prototype.enter.call(this); StandardMenuModule.super_.prototype.enter.call(this);
}; };
StandardMenuModule.prototype.beforeArt = function() { StandardMenuModule.prototype.beforeArt = function(cb) {
StandardMenuModule.super_.prototype.beforeArt.call(this); StandardMenuModule.super_.prototype.beforeArt.call(this, cb);
}; };
StandardMenuModule.prototype.mciReady = function(mciData, cb) { StandardMenuModule.prototype.mciReady = function(mciData, cb) {

View File

@ -1,25 +1,23 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var theme = require('./theme.js'); // ENiGMA½
var removeClient = require('./client_connections.js').removeClient; const removeClient = require('./client_connections.js').removeClient;
var ansi = require('./ansi_term.js'); const ansiNormal = require('./ansi_term.js').normal;
var userDb = require('./database.js').dbs.user; const userLogin = require('./user_login.js').userLogin;
var sysProp = require('./system_property.js');
var userLogin = require('./user_login.js').userLogin;
var async = require('async'); // deps
var _ = require('lodash'); const _ = require('lodash');
var iconv = require('iconv-lite'); const iconv = require('iconv-lite');
exports.login = login; exports.login = login;
exports.logoff = logoff; exports.logoff = logoff;
exports.prevMenu = prevMenu; exports.prevMenu = prevMenu;
exports.nextMenu = nextMenu;
function login(callingMenu, formData, extraArgs) { function login(callingMenu, formData) {
var client = callingMenu.client;
userLogin(callingMenu.client, formData.value.username, formData.value.password, function authResult(err) { userLogin(callingMenu.client, formData.value.username, formData.value.password, err => {
if(err) { if(err) {
// login failure // login failure
if(err.existingConn && _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) { 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 // Simple logoff. Note that recording of @ logoff properties/stats
// occurs elsewhere! // occurs elsewhere!
// //
var client = callingMenu.client; const client = callingMenu.client;
setTimeout(function timeout() { setTimeout( () => {
// //
// For giggles... // For giggles...
// //
client.term.write( client.term.write(
ansi.normal() + '\n' + ansiNormal() + '\n' +
iconv.decode(require('crypto').randomBytes(Math.floor(Math.random() * 65) + 20), client.term.outputEncoding) + 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 // after data is written, disconnect & remove the client
removeClient(client); return removeClient(client);
}); }
);
}, 500); }, 500);
} }
function prevMenu(callingMenu, formData, extraArgs) { function prevMenu(callingMenu) {
callingMenu.prevMenu(function result(err) { callingMenu.prevMenu( err => {
if(err) { if(err) {
callingMenu.client.log.error( { error : err.toString() }, 'Error attempting to fallback!'); 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!');
}
});
}

BIN
mods/art/ONEADD.ANS Normal file

Binary file not shown.

BIN
mods/art/ONELINER.ANS Normal file

Binary file not shown.

BIN
mods/art/erc.ans Normal file

Binary file not shown.

174
mods/erc_client.js Normal file
View File

@ -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);
};

View File

@ -517,7 +517,94 @@
module: whos_online module: whos_online
art: WHOSON art: WHOSON
options: { pause: true } options: { pause: true }
next: fullLoginSequenceOnelinerz
}
fullLoginSequenceOnelinerz: {
desc: Viewing Onelinerz
module: onelinerz
next: fullLoginSequenceNewScanConfirm 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: { fullLoginSequenceNewScanConfirm: {
@ -644,6 +731,14 @@
value: { command: "K" } value: { command: "K" }
action: @menu:mainMenuFeedbackToSysOp action: @menu:mainMenuFeedbackToSysOp
} }
{
value: { command: "O" }
action: @menu:mainMenuOnelinerz
}
{
value: { command: "CHAT"}
action: @menu:ercClient
}
{ {
value: 1 value: 1
action: @menu:mainMenu action: @menu:mainMenu
@ -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 // Doors Menu
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
@ -943,6 +1176,10 @@
value: { command: "DP" } value: { command: "DP" }
action: @menu:doorParty action: @menu:doorParty
} }
{
value: { command: "HL" }
action: @menu:telnetBridgeHappyLand
}
] ]
} }
@ -1020,6 +1257,18 @@
bbsTag: XX 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 // Message Area Menu
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////

315
mods/onelinerz.js Normal file
View File

@ -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);
});
};

115
mods/telnet_bridge.js Normal file
View File

@ -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);

Binary file not shown.

View File

@ -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: { mainMenuUserStats: {
mci: { mci: {
UN1: { width: 17 } 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: { messageAreaMessageList: {
config: { config: {
listFormat: "|00|15{msgNum:>4} |03{subj:<29.29} |11{from:<20.20} |03{ts} |01|31{newIndicator}" 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}"
}
}
} }
} }
} }