Merge remote-tracking branch 'upstream/master'

This commit is contained in:
David Stephens 2016-08-28 00:05:46 +01:00
commit 0b8916194e
15 changed files with 559 additions and 98 deletions

View File

@ -11,6 +11,7 @@ const logger = require('./logger.js');
const database = require('./database.js');
const clientConns = require('./client_connections.js');
// deps
const async = require('async');
const util = require('util');
const _ = require('lodash');

View File

@ -1,28 +1,37 @@
/* jslint node: true */
'use strict';
var logger = require('./logger.js');
// ENiGMA½
const logger = require('./logger.js');
var _ = require('lodash');
var moment = require('moment');
// deps
const _ = require('lodash');
const moment = require('moment');
exports.getActiveConnections = getActiveConnections;
exports.getActiveNodeList = getActiveNodeList;
exports.addNewClient = addNewClient;
exports.removeClient = removeClient;
var clientConnections = [];
const clientConnections = [];
exports.clientConnections = clientConnections;
function getActiveConnections() {
return clientConnections;
}
function getActiveConnections() { return clientConnections; }
function getActiveNodeList(authUsersOnly) {
if(!_.isBoolean(authUsersOnly)) {
authUsersOnly = true;
}
function getActiveNodeList() {
const now = moment();
const activeConnections = getActiveConnections().filter(ac => {
return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly);
});
return _.map(getActiveConnections(), ac => {
let entry = {
return _.map(activeConnections, ac => {
const entry = {
node : ac.node,
authenticated : ac.user.isAuthenticated(),
userId : ac.user.userId,
@ -46,13 +55,13 @@ function getActiveNodeList() {
}
function addNewClient(client, clientSock) {
var id = client.session.id = clientConnections.push(client) - 1;
const id = client.session.id = clientConnections.push(client) - 1;
// Create a client specific logger
// Note that this will be updated @ login with additional information
client.log = logger.log.child( { clientId : id } );
var connInfo = {
const connInfo = {
ip : clientSock.remoteAddress,
serverName : client.session.serverName,
isSecure : client.session.isSecure,
@ -71,7 +80,7 @@ function addNewClient(client, clientSock) {
function removeClient(client) {
client.end();
var i = clientConnections.indexOf(client);
const i = clientConnections.indexOf(client);
if(i > -1) {
clientConnections.splice(i, 1);
@ -84,15 +93,3 @@ function removeClient(client) {
);
}
}
/* :TODO: make a public API elsewhere
function getActiveClientInformation() {
var info = {};
clientConnections.forEach(function connEntry(cc) {
});
return info;
}
*/

View File

@ -122,10 +122,10 @@ function prepareTerminal(term) {
function displayBanner(term) {
// note: intentional formatting:
term.pipeWrite(`
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|06Copyright (c) 2014-2016 Bryan Ashby |14- |12http://l33t.codes/
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|00`
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|06Copyright (c) 2014-2016 Bryan Ashby |14- |12http://l33t.codes/
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|00`
);
}

19
core/enig_error.js Normal file
View File

@ -0,0 +1,19 @@
/* jslint node: true */
'use strict';
class EnigError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
this.message = message;
if(typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor);
} else {
this.stack = (new Error(message)).stack;
}
}
}
exports.EnigError = EnigError;

18
core/enigma_assert.js Normal file
View File

@ -0,0 +1,18 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const Config = require('./config.js').config;
const Log = require('./logger.js').log;
// deps
const assert = require('assert');
module.exports = function(condition, message) {
if(Config.debug.assertsEnabled) {
assert.apply(this, arguments);
} else if(!(condition)) {
const stack = new Error().stack;
Log.error( { condition : condition, stack : stack }, message || 'Assertion failed' );
}
};

327
core/string_format.js Normal file
View File

@ -0,0 +1,327 @@
/* jslint node: true */
'use strict';
const EnigError = require('./enig_error.js').EnigError;
const pad = require('./string_util.js').pad;
const stylizeString = require('./string_util.js').stylizeString;
// deps
const _ = require('lodash');
/*
String formatting HEAVILY inspired by David Chambers string-format library
and the mini-language branch specifically which was gratiously released
under the DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE.
We need some extra functionality. Namely, support for RA style pipe codes
and ANSI escape sequences.
*/
class ValueError extends EnigError { }
class KeyError extends EnigError { }
const SpecRegExp = {
FillAlign : /^(.)?([<>=^])/,
Sign : /^[ +-]/,
Width : /^\d*/,
Precision : /^\d+/,
};
function tokenizeFormatSpec(spec) {
const tokens = {
fill : '',
align : '',
sign : '',
'#' : false,
'0' : false,
width : '',
',' : false,
precision : '',
type : '',
};
let index = 0;
let match;
function incIndexByMatch() {
index += match[0].length;
}
match = SpecRegExp.FillAlign.exec(spec);
if(match) {
if(match[1]) {
tokens.fill = match[1];
}
tokens.align = match[2];
incIndexByMatch();
}
match = SpecRegExp.Sign.exec(spec.slice(index));
if(match) {
tokens.sign = match[0];
incIndexByMatch();
}
if('#' === spec.charAt(index)) {
tokens['#'] = true;
++index;
}
if('0' === spec.charAt(index)) {
tokens['0'] = true;
++index;
}
match = SpecRegExp.Width.exec(spec.slice(index));
tokens.width = match[0];
incIndexByMatch();
if(',' === spec.charAt(index)) {
tokens[','] = true;
++index;
}
if('.' === spec.charAt(index)) {
++index;
match = SpecRegExp.Precision.exec(spec.slice(index));
if(!match) {
throw new ValueError('Format specifier missing precision');
}
tokens.precision = match[0];
incIndexByMatch();
}
if(index < spec.length) {
tokens.type = spec.charAt(index);
++index;
}
if(index < spec.length) {
throw new ValueError('Invalid conversion specification');
}
if(tokens[','] && 's' === tokens.type) {
throw new ValueError(`Cannot specify ',' with 's'`); // eslint-disable-line quotes
}
return tokens;
}
function quote(s) {
return `"${s.replace(/"/g, '\\"')}"`;
}
function getPadAlign(align) {
return {
'<' : 'right',
'>' : 'left',
'^' : 'center',
}[align] || '<';
}
function formatString(value, tokens) {
const fill = tokens.fill || (tokens['0'] ? '0' : ' ');
const align = tokens.align || (tokens['0'] ? '=' : '<');
const precision = Number(tokens.precision || value.length); // :TODO: consider pipe/ANSI length
if('' !== tokens.type && 's' !== tokens.type) {
throw new ValueError(`Unknown format code "${tokens.type}" for String object`);
}
if(tokens[',']) {
throw new ValueError(`Cannot specify ',' with 's'`); // eslint-disable-line quotes
}
if(tokens.sign) {
throw new ValueError('Sign not allowed in string format specifier');
}
if(tokens['#']) {
throw new ValueError('Alternate form (#) not allowed in string format specifier');
}
if('=' === align) {
throw new ValueError('"=" alignment not allowed in string format specifier');
}
return pad(value.slice(0, precision), parseInt(tokens.width), fill, getPadAlign(align));
}
const FormatNumRegExp = {
UpperType : /[A-Z]/,
ExponentRep : /e[+-](?=\d$)/,
};
function formatNumberHelper(n, precision, type) {
if(FormatNumRegExp.UpperType.test(type)) {
return formatNumberHelper(n, precision, type.toLowerCase()).toUpperCase();
}
switch(type) {
case 'c' : return String.fromCharCode(n);
case 'd' : return n.toString(10);
case 'b' : return n.toString(2);
case 'o' : return n.toString(8);
case 'x' : return n.toString(16);
case 'e' : return n.toExponential(precision).replace(FormatNumRegExp.ExponentRep, '$&0');
case 'f' : return n.toFixed(precision);
case 'g' : return n.toPrecision(precision || 1);
case '%' : return formatNumberHelper(n * 100, precision, 'f') + '%';
case '' : return formatNumberHelper(n, precision, 'd');
default :
throw new ValueError(`Unknown format code "${type}" for object of type 'float'`);
}
}
function formatNumber(value, tokens) {
const fill = tokens.fill || (tokens['0'] ? '0' : ' ');
const align = tokens.align || (tokens['0'] ? '=' : '>');
const width = Number(tokens.width);
const type = tokens.type || (tokens.precision ? 'g' : '');
if( [ 'c', 'd', 'b', 'o', 'x', 'X' ].indexOf(type) > -1) {
if(0 !== value % 1) {
throw new ValueError(`Cannot format non-integer with format specifier "${type}"`);
}
if('' !== tokens.sign && 'c' !== type) {
throw new ValueError(`Sign not allowed with integer format specifier 'c'`); // eslint-disable-line quotes
}
if(tokens[','] && 'd' !== type) {
throw new ValueError(`Cannot specify ',' with '${type}'`);
}
if('' !== tokens.precision) {
throw new ValueError('Precision not allowed in integer format specifier');
}
} else if( [ 'e', 'E', 'f', 'F', 'g', 'G', '%' ].indexOf(type) > - 1) {
if(tokens['#']) {
throw new ValueError('Alternate form (#) not allowed in float format specifier');
}
}
const s = formatNumberHelper(Math.abs(value), Number(tokens.precision || 6), type);
const sign = value < 0 || 1 / value < 0 ?
'-' :
'-' === tokens.sign ? '' : tokens.sign;
const prefix = tokens['#'] && ( [ 'b', 'o', 'x', 'X' ].indexOf(type) > -1 ) ? '0' + type : '';
if(tokens[',']) {
const match = /^(\d*)(.*)$/.exec(s);
const separated = match[1].replace(/.(?=(...)+$)/g, '$&,') + match[2];
if('=' !== align) {
return pad(sign + separated, width, fill, getPadAlign(align));
}
if('0' === fill) {
const shortfall = Math.max(0, width - sign.length - separated.length);
const digits = /^\d*/.exec(separated)[0].length;
let padding = '';
// :TODO: do this differntly...
for(let n = 0; n < shortfall; n++) {
padding = ((digits + n) % 4 === 3 ? ',' : '0') + padding;
}
return sign + (/^,/.test(padding) ? '0' : '') + padding + separated;
}
return sign + pad(separated, width - sign.length, fill, getPadAlign('>'));
}
if(0 === width) {
return sign + prefix + s;
}
if('=' === align) {
return sign + prefix + pad(s, width - sign.length - prefix.length, fill, getPadAlign('>'));
}
return pad(sign + prefix + s, width, fill, getPadAlign(align));
}
const transformers = {
// String standard
toUpperCase : String.prototype.toUpperCase,
toLowerCase : String.prototype.toLowerCase,
// some super l33b BBS styles!!
styleUpper : (s) => stylizeString(s, 'upper'),
styleLower : (s) => stylizeString(s, 'lower'),
styleTitle : (s) => stylizeString(s, 'title'),
styleFirstLower : (s) => stylizeString(s, 'first lower'),
styleSmallVowels : (s) => stylizeString(s, 'small vowels'),
styleBigVowels : (s) => stylizeString(s, 'big vowels'),
styleSmallI : (s) => stylizeString(s, 'small i'),
styleMixed : (s) => stylizeString(s, 'mixed'),
styleL33t : (s) => stylizeString(s, 'l33t'),
};
function transformValue(transformerName, value) {
if(transformerName in transformers) {
const transformer = transformers[transformerName];
value = transformer.apply(value, [ value ] );
}
return value;
}
// :TODO: Use explicit set of chars for paths & function/transforms such that } is allowed as fill/etc.
const REGEXP_BASIC_FORMAT = /{([^.!:}]+(?:\.[^.!:}]+)*)(?:\!([^:}]+))?(?:\:([^}]+))?}/g;
function getValue(obj, path) {
const value = _.get(obj, path);
if(value) {
return _.isFunction(value) ? value() : value;
}
throw new KeyError(quote(path));
}
module.exports = function format(fmt, obj) {
const re = REGEXP_BASIC_FORMAT;
let match;
let pos;
let out = '';
do {
pos = re.lastIndex;
match = re.exec(fmt);
if(match) {
if(match.index > pos) {
out += fmt.slice(pos, match.index);
}
const objPath = match[1];
const transformer = match[2];
const formatSpec = match[3];
let value = getValue(obj, objPath);
if(transformer) {
value = transformValue(transformer, value);
}
const tokens = tokenizeFormatSpec(formatSpec || '');
if(!isNaN(parseInt(value))) {
out += formatNumber(value, tokens);
} else {
out += formatString(value, tokens);
}
}
} while(0 !== re.lastIndex);
// remainder
if(pos < fmt.length) {
out += fmt.slice(pos);
}
return out;
};

View File

@ -1,18 +1,19 @@
/* jslint node: true */
'use strict';
var events = require('events');
var util = require('util');
var assert = require('assert');
var ansi = require('./ansi_term.js');
var colorCodes = require('./color_codes.js');
// ENiGMA½
const events = require('events');
const util = require('util');
const ansi = require('./ansi_term.js');
const colorCodes = require('./color_codes.js');
const enigAssert = require('./enigma_assert.js');
var _ = require('lodash');
// deps
const _ = require('lodash');
exports.View = View;
exports.VIEW_SPECIAL_KEY_MAP_DEFAULT = VIEW_SPECIAL_KEY_MAP_DEFAULT;
var VIEW_SPECIAL_KEY_MAP_DEFAULT = {
const VIEW_SPECIAL_KEY_MAP_DEFAULT = {
accept : [ 'return' ],
exit : [ 'esc' ],
backspace : [ 'backspace', 'del' ],
@ -27,11 +28,13 @@ var VIEW_SPECIAL_KEY_MAP_DEFAULT = {
clearLine : [ 'ctrl + y' ],
};
exports.VIEW_SPECIAL_KEY_MAP_DEFAULT = VIEW_SPECIAL_KEY_MAP_DEFAULT;
function View(options) {
events.EventEmitter.call(this);
assert(_.isObject(options));
assert(_.isObject(options.client));
enigAssert(_.isObject(options));
enigAssert(_.isObject(options.client));
var self = this;
@ -131,7 +134,7 @@ View.prototype.setPosition = function(pos) {
this.position.col = parseInt(arguments[1], 10);
}
// santaize
// sanatize
this.position.row = Math.max(this.position.row, 1);
this.position.col = Math.max(this.position.col, 1);
this.position.row = Math.min(this.position.row, this.client.term.termHeight);
@ -139,25 +142,23 @@ View.prototype.setPosition = function(pos) {
};
View.prototype.setDimension = function(dimens) {
assert(_.isObject(dimens) && _.isNumber(dimens.height) && _.isNumber(dimens.width));
enigAssert(_.isObject(dimens) && _.isNumber(dimens.height) && _.isNumber(dimens.width));
this.dimens = dimens;
this.autoScale = { height : false, width : false };
};
View.prototype.setHeight = function(height) {
height = parseInt(height, 10);
assert(_.isNumber(height));
// :TODO: assert height is within this.client.term.termHeight
height = parseInt(height) || 1;
height = Math.min(height, this.client.term.termHeight);
this.dimens.height = height;
this.autoScale.height = false;
};
View.prototype.setWidth = function(width) {
width = parseInt(width);
assert(_.isNumber(width));
// :TODO: assert width is appropriate for this.client.term.termWidth
width = parseInt(width) || 1;
width = Math.min(width, this.client.term.termWidth);
this.dimens.width = width;
this.autoScale.width = false;
@ -168,7 +169,7 @@ View.prototype.getSGR = function() {
};
View.prototype.getStyleSGR = function(n) {
assert(_.isNumber(n));
n = parseInt(n) || 0;
return this['styleSGR' + n];
};
@ -241,21 +242,22 @@ View.prototype.redraw = function() {
};
View.prototype.setFocus = function(focused) {
assert(this.acceptsFocus, 'View does not accept focus');
enigAssert(this.acceptsFocus, 'View does not accept focus');
this.hasFocus = focused;
this.restoreCursor();
};
View.prototype.onKeyPress = function(ch, key) {
if(false === this.hasFocus) {
console.log('doh!'); // :TODO: fix me -- assert here?
View.prototype.onKeyPress = function(ch, key) {
enigAssert(this.hasFocus, 'View does not have focus');
enigAssert(this.acceptsInput, 'View does not accept input');
if(!this.hasFocus || !this.acceptsInput) {
return;
}
assert(this.hasFocus, 'View does not have focus');
assert(this.acceptsInput, 'View does not accept input');
if(key) {
assert(this.specialKeyMap, 'No special key map defined');
enigAssert(this.specialKeyMap, 'No special key map defined');
if(this.isKeyMapped('accept', key.name)) {
this.emit('action', 'accept', key);
@ -265,7 +267,7 @@ View.prototype.onKeyPress = function(ch, key) {
}
if(ch) {
assert(1 === ch.length);
enigAssert(1 === ch.length);
}
this.emit('key press', ch, key);

View File

@ -745,6 +745,10 @@
value: { command: "O" }
action: @menu:mainMenuOnelinerz
}
{
value: { command: "R" }
action: @menu:mainMenuRumorz
}
{
value: { command: "CHAT"}
action: @menu:ercClient
@ -1108,6 +1112,92 @@
}
}
mainMenuRumorz: {
desc: Rumorz
module: rumorz
options: {
cls: true
}
config: {
art: {
entries: RUMORS
add: RUMORADD
}
}
form: {
0: {
mci: {
VM1: {
focus: false
height: 10
}
TM2: {
argName: addOrExit
items: [ "yeah!", "nah" ]
"hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 }
submit: true
focus: true
}
}
submit: {
*: [
{
value: { addOrExit: 0 }
action: @method:viewAddScreen
}
{
value: { addOrExit: null }
action: @systemMethod:nextMenu
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:nextMenu
}
]
},
1: {
mci: {
ET1: {
focus: true
maxLength: 70
argName: rumor
}
TL2: {
width: 60
}
TM3: {
argName: addOrCancel
items: [ "add", "cancel" ]
"hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 }
submit: true
}
}
submit: {
*: [
{
value: { addOrCancel: 0 }
action: @method:addEntry
}
{
value: { addOrCancel: 1 }
action: @method:cancelAdd
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @method:cancelAdd
}
]
}
}
}
ercClient: {
art: erc
module: erc_client

View File

@ -7,6 +7,7 @@ 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');
const stringFormat = require('../core/string_format.js');
// deps
const sqlite3 = require('sqlite3');
@ -145,7 +146,7 @@ function OnelinerzModule(options) {
const tsFormat = config.timestampFormat || 'ddd h:mma';
entriesView.setItems(entries.map( e => {
return listFormat.format( {
return stringFormat(listFormat, {
userId : e.user_id,
username : e.user_name,
oneliner : e.oneliner,

Binary file not shown.

Binary file not shown.

View File

@ -271,6 +271,31 @@
}
}
mainMenuRumorz: {
config: {
listFormat: "|00|11 {rumor}"
focusListFormat: "|00|15> |14{rumor}"
}
0: {
mci: {
VM1: { height: 14 }
TM2: {
focusTextStyle: upper
items: [ "yes", "no" ]
}
}
}
1: {
mci: {
ET1: { width: 60 }
TL2: { width: 60 }
TM3: {
focusTextStyle: upper
}
}
}
}
bbsList: {
0: {
mci: {

View File

@ -1,14 +1,14 @@
/* jslint node: true */
'use strict';
var MenuModule = require('../core/menu_module.js').MenuModule;
var ViewController = require('../core/view_controller.js').ViewController;
var getActiveNodeList = require('../core/client_connections.js').getActiveNodeList;
// ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController;
const getActiveNodeList = require('../core/client_connections.js').getActiveNodeList;
var moment = require('moment');
var async = require('async');
var assert = require('assert');
var _ = require('lodash');
// deps
const async = require('async');
const _ = require('lodash');
exports.moduleInfo = {
name : 'Who\'s Online',
@ -17,26 +17,9 @@ exports.moduleInfo = {
packageName : 'codes.l33t.enigma.whosonline'
};
/*
node
userName
userId
action
note
affils
timeOnSec
location
realName
serverName (Telnet, SSH, ...)
default
{node} - {username} - {action} - {timeOnSec}
*/
exports.getModule = WhosOnlineModule;
var MciCodeIds = {
const MciCodeIds = {
OnlineList : 1,
};
@ -47,30 +30,29 @@ function WhosOnlineModule(options) {
require('util').inherits(WhosOnlineModule, MenuModule);
WhosOnlineModule.prototype.mciReady = function(mciData, cb) {
var self = this;
var vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
async.series(
[
function callParentMciReady(callback) {
WhosOnlineModule.super_.prototype.mciReady.call(self, mciData, callback);
return WhosOnlineModule.super_.prototype.mciReady.call(self, mciData, callback);
},
function loadFromConfig(callback) {
var loadOpts = {
const loadOpts = {
callingMenu : self,
mciMap : mciData.menu,
noInput : true,
};
vc.loadFromMenuConfig(loadOpts, callback);
return vc.loadFromMenuConfig(loadOpts, callback);
},
function populateList(callback) {
var onlineListView = vc.getView(MciCodeIds.OnlineList);
const listFormat = self.menuConfig.config.listFormat || '{node} - {userName} - {action} - {timeOn}';
const nonAuthUser = self.menuConfig.config.nonAuthUser || 'Logging In';
const otherUnknown = self.menuConfig.config.otherUnknown || 'N/A';
const onlineList = getActiveNodeList().slice(0, onlineListView.height);
const onlineListView = vc.getView(MciCodeIds.OnlineList);
const listFormat = self.menuConfig.config.listFormat || '{node} - {userName} - {action} - {timeOn}';
const nonAuthUser = self.menuConfig.config.nonAuthUser || 'Logging In';
const otherUnknown = self.menuConfig.config.otherUnknown || 'N/A';
const onlineList = getActiveNodeList(self.menuConfig.config.authUsersOnly).slice(0, onlineListView.height);
onlineListView.setItems(_.map(onlineList, oe => {
if(oe.authenticated) {
@ -85,16 +67,16 @@ WhosOnlineModule.prototype.mciReady = function(mciData, cb) {
}));
onlineListView.focusItems = onlineListView.items;
onlineListView.redraw();
callback(null);
return callback(null);
}
],
function complete(err) {
if(err) {
self.client.log.error( { error : err.toString() }, 'Error loading who\'s online');
self.client.log.error( { error : err.message }, 'Error loading who\'s online');
}
cb(err);
return cb(err);
}
);
};

View File

@ -12,7 +12,6 @@ const resolvePath = require('./core/misc_util.js').resolvePath;
// deps
const _ = require('lodash');
const async = require('async');
const assert = require('assert');
const inq = require('inquirer');
const mkdirsSync = require('fs-extra').mkdirsSync;
const fs = require('fs');