ENiGMA 1/2 WILL USE SPACES FROM THIS POINT ON VS TABS

* Really just to make GitHub formatting happy. Arg.
This commit is contained in:
Bryan Ashby 2018-06-21 23:15:04 -06:00
parent 5ddf04c882
commit e9787cee3e
135 changed files with 27397 additions and 27397 deletions

View File

@ -18,9 +18,9 @@ const mkdirs = require('fs-extra').mkdirs;
const activeDoorNodeInstances = {}; const activeDoorNodeInstances = {};
exports.moduleInfo = { exports.moduleInfo = {
name : 'Abracadabra', name : 'Abracadabra',
desc : 'External BBS Door Module', desc : 'External BBS Door Module',
author : 'NuSkooler', author : 'NuSkooler',
}; };
/* /*
@ -60,138 +60,138 @@ exports.moduleInfo = {
*/ */
exports.getModule = class AbracadabraModule extends MenuModule { exports.getModule = class AbracadabraModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... } // :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
assert(_.isString(this.config.name, 'Config \'name\' is required')); assert(_.isString(this.config.name, 'Config \'name\' is required'));
assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required')); assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required'));
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required')); assert(_.isString(this.config.cmd, 'Config \'cmd\' is required'));
this.config.nodeMax = this.config.nodeMax || 0; this.config.nodeMax = this.config.nodeMax || 0;
this.config.args = this.config.args || []; this.config.args = this.config.args || [];
} }
/* /*
:TODO: :TODO:
* disconnecting wile door is open leaves dosemu * disconnecting wile door is open leaves dosemu
* http://bbslink.net/sysop.php support * http://bbslink.net/sysop.php support
* Font support ala all other menus... or does this just work? * Font support ala all other menus... or does this just work?
*/ */
initSequence() { initSequence() {
const self = this; const self = this;
async.series( async.series(
[ [
function validateNodeCount(callback) { function validateNodeCount(callback) {
if(self.config.nodeMax > 0 && if(self.config.nodeMax > 0 &&
_.isNumber(activeDoorNodeInstances[self.config.name]) && _.isNumber(activeDoorNodeInstances[self.config.name]) &&
activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax) activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax)
{ {
self.client.log.info( self.client.log.info(
{ {
name : self.config.name, name : self.config.name,
activeCount : activeDoorNodeInstances[self.config.name] activeCount : activeDoorNodeInstances[self.config.name]
}, },
'Too many active instances'); 'Too many active instances');
if(_.isString(self.config.tooManyArt)) { if(_.isString(self.config.tooManyArt)) {
theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() { theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() {
self.pausePrompt( () => { self.pausePrompt( () => {
callback(new Error('Too many active instances')); callback(new Error('Too many active instances'));
}); });
}); });
} else { } else {
self.client.term.write('\nToo many active instances. Try again later.\n'); self.client.term.write('\nToo many active instances. Try again later.\n');
// :TODO: Use MenuModule.pausePrompt() // :TODO: Use MenuModule.pausePrompt()
self.pausePrompt( () => { self.pausePrompt( () => {
callback(new Error('Too many active instances')); callback(new Error('Too many active instances'));
}); });
} }
} else { } else {
// :TODO: JS elegant way to do this? // :TODO: JS elegant way to do this?
if(activeDoorNodeInstances[self.config.name]) { if(activeDoorNodeInstances[self.config.name]) {
activeDoorNodeInstances[self.config.name] += 1; activeDoorNodeInstances[self.config.name] += 1;
} else { } else {
activeDoorNodeInstances[self.config.name] = 1; activeDoorNodeInstances[self.config.name] = 1;
} }
callback(null); callback(null);
} }
}, },
function generateDropfile(callback) { function generateDropfile(callback) {
self.dropFile = new DropFile(self.client, self.config.dropFileType); self.dropFile = new DropFile(self.client, self.config.dropFileType);
var fullPath = self.dropFile.fullPath; var fullPath = self.dropFile.fullPath;
mkdirs(paths.dirname(fullPath), function dirCreated(err) { mkdirs(paths.dirname(fullPath), function dirCreated(err) {
if(err) { if(err) {
callback(err); callback(err);
} else { } else {
self.dropFile.createFile(function created(err) { self.dropFile.createFile(function created(err) {
callback(err); callback(err);
}); });
} }
}); });
} }
], ],
function complete(err) { function complete(err) {
if(err) { if(err) {
self.client.log.warn( { error : err.toString() }, 'Could not start door'); self.client.log.warn( { error : err.toString() }, 'Could not start door');
self.lastError = err; self.lastError = err;
self.prevMenu(); self.prevMenu();
} else { } else {
self.finishedLoading(); self.finishedLoading();
} }
} }
); );
} }
runDoor() { runDoor() {
const exeInfo = { const exeInfo = {
cmd : this.config.cmd, cmd : this.config.cmd,
args : this.config.args, args : this.config.args,
io : this.config.io || 'stdio', io : this.config.io || 'stdio',
encoding : this.config.encoding || this.client.term.outputEncoding, encoding : this.config.encoding || this.client.term.outputEncoding,
dropFile : this.dropFile.fileName, dropFile : this.dropFile.fileName,
node : this.client.node, node : this.client.node,
//inhSocket : this.client.output._handle.fd, //inhSocket : this.client.output._handle.fd,
}; };
const doorInstance = new door.Door(this.client, exeInfo); const doorInstance = new door.Door(this.client, exeInfo);
doorInstance.once('finished', () => { doorInstance.once('finished', () => {
// //
// Try to clean up various settings such as scroll regions that may // Try to clean up various settings such as scroll regions that may
// have been set within the door // have been set within the door
// //
this.client.term.rawWrite( this.client.term.rawWrite(
ansi.normal() + ansi.normal() +
ansi.goto(this.client.term.termHeight, this.client.term.termWidth) + ansi.goto(this.client.term.termHeight, this.client.term.termWidth) +
ansi.setScrollRegion() + ansi.setScrollRegion() +
ansi.goto(this.client.term.termHeight, 0) + ansi.goto(this.client.term.termHeight, 0) +
'\r\n\r\n' '\r\n\r\n'
); );
this.prevMenu(); this.prevMenu();
}); });
this.client.term.write(ansi.resetScreen()); this.client.term.write(ansi.resetScreen());
doorInstance.run(); doorInstance.run();
} }
leave() { leave() {
super.leave(); super.leave();
if(!this.lastError) { if(!this.lastError) {
activeDoorNodeInstances[this.config.name] -= 1; activeDoorNodeInstances[this.config.name] -= 1;
} }
} }
finishedLoading() { finishedLoading() {
this.runDoor(); this.runDoor();
} }
}; };

View File

@ -10,81 +10,81 @@ const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
class ACS { class ACS {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
} }
check(acs, scope, defaultAcs) { check(acs, scope, defaultAcs) {
acs = acs ? acs[scope] : defaultAcs; acs = acs ? acs[scope] : defaultAcs;
acs = acs || defaultAcs; acs = acs || defaultAcs;
try { try {
return checkAcs(acs, { client : this.client } ); return checkAcs(acs, { client : this.client } );
} catch(e) { } catch(e) {
Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS'); Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS');
return false; return false;
} }
} }
// //
// Message Conferences & Areas // Message Conferences & Areas
// //
hasMessageConfRead(conf) { hasMessageConfRead(conf) {
return this.check(conf.acs, 'read', ACS.Defaults.MessageConfRead); return this.check(conf.acs, 'read', ACS.Defaults.MessageConfRead);
} }
hasMessageAreaRead(area) { hasMessageAreaRead(area) {
return this.check(area.acs, 'read', ACS.Defaults.MessageAreaRead); return this.check(area.acs, 'read', ACS.Defaults.MessageAreaRead);
} }
// //
// File Base / Areas // File Base / Areas
// //
hasFileAreaRead(area) { hasFileAreaRead(area) {
return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead); return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead);
} }
hasFileAreaWrite(area) { hasFileAreaWrite(area) {
return this.check(area.acs, 'write', ACS.Defaults.FileAreaWrite); return this.check(area.acs, 'write', ACS.Defaults.FileAreaWrite);
} }
hasFileAreaDownload(area) { hasFileAreaDownload(area) {
return this.check(area.acs, 'download', ACS.Defaults.FileAreaDownload); return this.check(area.acs, 'download', ACS.Defaults.FileAreaDownload);
} }
getConditionalValue(condArray, memberName) { getConditionalValue(condArray, memberName) {
if(!Array.isArray(condArray)) { if(!Array.isArray(condArray)) {
// no cond array, just use the value // no cond array, just use the value
return condArray; return condArray;
} }
assert(_.isString(memberName)); assert(_.isString(memberName));
const matchCond = condArray.find( cond => { const matchCond = condArray.find( cond => {
if(_.has(cond, 'acs')) { if(_.has(cond, 'acs')) {
try { try {
return checkAcs(cond.acs, { client : this.client } ); return checkAcs(cond.acs, { client : this.client } );
} catch(e) { } catch(e) {
Log.warn( { exception : e, acs : cond }, 'Exception caught checking ACS'); Log.warn( { exception : e, acs : cond }, 'Exception caught checking ACS');
return false; return false;
} }
} else { } else {
return true; // no acs check req. return true; // no acs check req.
} }
}); });
if(matchCond) { if(matchCond) {
return matchCond[memberName]; return matchCond[memberName];
} }
} }
} }
ACS.Defaults = { ACS.Defaults = {
MessageAreaRead : 'GM[users]', MessageAreaRead : 'GM[users]',
MessageConfRead : 'GM[users]', MessageConfRead : 'GM[users]',
FileAreaRead : 'GM[users]', FileAreaRead : 'GM[users]',
FileAreaWrite : 'GM[sysops]', FileAreaWrite : 'GM[sysops]',
FileAreaDownload : 'GM[users]', FileAreaDownload : 'GM[users]',
}; };
module.exports = ACS; module.exports = ACS;

View File

@ -16,278 +16,278 @@ const CR = 0x0d;
const LF = 0x0a; const LF = 0x0a;
function ANSIEscapeParser(options) { function ANSIEscapeParser(options) {
var self = this; var self = this;
events.EventEmitter.call(this); events.EventEmitter.call(this);
this.column = 1; this.column = 1;
this.row = 1; this.row = 1;
this.scrollBack = 0; this.scrollBack = 0;
this.graphicRendition = {}; this.graphicRendition = {};
this.parseState = { this.parseState = {
re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
}; };
options = miscUtil.valueWithDefault(options, { options = miscUtil.valueWithDefault(options, {
mciReplaceChar : '', mciReplaceChar : '',
termHeight : 25, termHeight : 25,
termWidth : 80, termWidth : 80,
trailingLF : 'default', // default|omit|no|yes, ... trailingLF : 'default', // default|omit|no|yes, ...
}); });
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, ''); this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25); this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80); this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default'); this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
self.moveCursor = function(cols, rows) { self.moveCursor = function(cols, rows) {
self.column += cols; self.column += cols;
self.row += rows; self.row += rows;
self.column = Math.max(self.column, 1); self.column = Math.max(self.column, 1);
self.column = Math.min(self.column, self.termWidth); // can't move past term width self.column = Math.min(self.column, self.termWidth); // can't move past term width
self.row = Math.max(self.row, 1); self.row = Math.max(self.row, 1);
self.positionUpdated(); self.positionUpdated();
}; };
self.saveCursorPosition = function() { self.saveCursorPosition = function() {
self.savedPosition = { self.savedPosition = {
row : self.row, row : self.row,
column : self.column column : self.column
}; };
}; };
self.restoreCursorPosition = function() { self.restoreCursorPosition = function() {
self.row = self.savedPosition.row; self.row = self.savedPosition.row;
self.column = self.savedPosition.column; self.column = self.savedPosition.column;
delete self.savedPosition; delete self.savedPosition;
self.positionUpdated(); self.positionUpdated();
// self.rowUpdated(); // self.rowUpdated();
}; };
self.clearScreen = function() { self.clearScreen = function() {
// :TODO: should be doing something with row/column? // :TODO: should be doing something with row/column?
self.emit('clear screen'); self.emit('clear screen');
}; };
/* /*
self.rowUpdated = function() { self.rowUpdated = function() {
self.emit('row update', self.row + self.scrollBack); self.emit('row update', self.row + self.scrollBack);
};*/ };*/
self.positionUpdated = function() { self.positionUpdated = function() {
self.emit('position update', self.row, self.column); self.emit('position update', self.row, self.column);
}; };
function literal(text) { function literal(text) {
const len = text.length; const len = text.length;
let pos = 0; let pos = 0;
let start = 0; let start = 0;
let charCode; let charCode;
while(pos < len) { while(pos < len) {
charCode = text.charCodeAt(pos) & 0xff; // 8bit clean charCode = text.charCodeAt(pos) & 0xff; // 8bit clean
switch(charCode) { switch(charCode) {
case CR : case CR :
self.emit('literal', text.slice(start, pos)); self.emit('literal', text.slice(start, pos));
start = pos; start = pos;
self.column = 1; self.column = 1;
self.positionUpdated(); self.positionUpdated();
break; break;
case LF : case LF :
self.emit('literal', text.slice(start, pos)); self.emit('literal', text.slice(start, pos));
start = pos; start = pos;
self.row += 1; self.row += 1;
self.positionUpdated(); self.positionUpdated();
break; break;
default : default :
if(self.column === self.termWidth) { if(self.column === self.termWidth) {
self.emit('literal', text.slice(start, pos + 1)); self.emit('literal', text.slice(start, pos + 1));
start = pos + 1; start = pos + 1;
self.column = 1; self.column = 1;
self.row += 1; self.row += 1;
self.positionUpdated(); self.positionUpdated();
} else { } else {
self.column += 1; self.column += 1;
} }
break; break;
} }
++pos; ++pos;
} }
// //
// Finalize this chunk // Finalize this chunk
// //
if(self.column > self.termWidth) { if(self.column > self.termWidth) {
self.column = 1; self.column = 1;
self.row += 1; self.row += 1;
self.positionUpdated(); self.positionUpdated();
} }
const rem = text.slice(start); const rem = text.slice(start);
if(rem) { if(rem) {
self.emit('literal', rem); self.emit('literal', rem);
} }
} }
function parseMCI(buffer) { function parseMCI(buffer) {
// :TODO: move this to "constants" seciton @ top // :TODO: move this to "constants" seciton @ top
var mciRe = /%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g; var mciRe = /%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g;
var pos = 0; var pos = 0;
var match; var match;
var mciCode; var mciCode;
var args; var args;
var id; var id;
do { do {
pos = mciRe.lastIndex; pos = mciRe.lastIndex;
match = mciRe.exec(buffer); match = mciRe.exec(buffer);
if(null !== match) { if(null !== match) {
if(match.index > pos) { if(match.index > pos) {
literal(buffer.slice(pos, match.index)); literal(buffer.slice(pos, match.index));
} }
mciCode = match[1]; mciCode = match[1];
id = match[2] || null; id = match[2] || null;
if(match[3]) { if(match[3]) {
args = match[3].split(','); args = match[3].split(',');
} else { } else {
args = []; args = [];
} }
// if MCI codes are changing, save off the current color // if MCI codes are changing, save off the current color
var fullMciCode = mciCode + (id || ''); var fullMciCode = mciCode + (id || '');
if(self.lastMciCode !== fullMciCode) { if(self.lastMciCode !== fullMciCode) {
self.lastMciCode = fullMciCode; self.lastMciCode = fullMciCode;
self.graphicRenditionForErase = _.clone(self.graphicRendition); self.graphicRenditionForErase = _.clone(self.graphicRendition);
} }
self.emit('mci', { self.emit('mci', {
mci : mciCode, mci : mciCode,
id : id ? parseInt(id, 10) : null, id : id ? parseInt(id, 10) : null,
args : args, args : args,
SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true)
}); });
if(self.mciReplaceChar.length > 0) { if(self.mciReplaceChar.length > 0) {
const sgrCtrl = ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase); const sgrCtrl = ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase);
self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[;m]/).slice(0, 3)); self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[;m]/).slice(0, 3));
literal(new Array(match[0].length + 1).join(self.mciReplaceChar)); literal(new Array(match[0].length + 1).join(self.mciReplaceChar));
} else { } else {
literal(match[0]); literal(match[0]);
} }
} }
} while(0 !== mciRe.lastIndex); } while(0 !== mciRe.lastIndex);
if(pos < buffer.length) { if(pos < buffer.length) {
literal(buffer.slice(pos)); literal(buffer.slice(pos));
} }
} }
self.reset = function(input) { self.reset = function(input) {
self.parseState = { self.parseState = {
// ignore anything past EOF marker, if any // ignore anything past EOF marker, if any
buffer : input.split(String.fromCharCode(0x1a), 1)[0], buffer : input.split(String.fromCharCode(0x1a), 1)[0],
re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
stop : false, stop : false,
}; };
}; };
self.stop = function() { self.stop = function() {
self.parseState.stop = true; self.parseState.stop = true;
}; };
self.parse = function(input) { self.parse = function(input) {
if(input) { if(input) {
self.reset(input); self.reset(input);
} }
// :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc.
var pos; var pos;
var match; var match;
var opCode; var opCode;
var args; var args;
var re = self.parseState.re; var re = self.parseState.re;
var buffer = self.parseState.buffer; var buffer = self.parseState.buffer;
self.parseState.stop = false; self.parseState.stop = false;
do { do {
if(self.parseState.stop) { if(self.parseState.stop) {
return; return;
} }
pos = re.lastIndex; pos = re.lastIndex;
match = re.exec(buffer); match = re.exec(buffer);
if(null !== match) { if(null !== match) {
if(match.index > pos) { if(match.index > pos) {
parseMCI(buffer.slice(pos, match.index)); parseMCI(buffer.slice(pos, match.index));
} }
opCode = match[2]; opCode = match[2];
args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints
escape(opCode, args); escape(opCode, args);
//self.emit('chunk', match[0]); //self.emit('chunk', match[0]);
self.emit('control', match[0], opCode, args); self.emit('control', match[0], opCode, args);
} }
} while(0 !== re.lastIndex); } while(0 !== re.lastIndex);
if(pos < buffer.length) { if(pos < buffer.length) {
var lastBit = buffer.slice(pos); var lastBit = buffer.slice(pos);
// :TODO: check for various ending LF's, not just DOS \r\n // :TODO: check for various ending LF's, not just DOS \r\n
if('\r\n' === lastBit.slice(-2).toString()) { if('\r\n' === lastBit.slice(-2).toString()) {
switch(self.trailingLF) { switch(self.trailingLF) {
case 'default' : case 'default' :
// //
// Default is to *not* omit the trailing LF // Default is to *not* omit the trailing LF
// if we're going to end on termHeight // if we're going to end on termHeight
// //
if(this.termHeight === self.row) { if(this.termHeight === self.row) {
lastBit = lastBit.slice(0, -2); lastBit = lastBit.slice(0, -2);
} }
break; break;
case 'omit' : case 'omit' :
case 'no' : case 'no' :
case false : case false :
lastBit = lastBit.slice(0, -2); lastBit = lastBit.slice(0, -2);
break; break;
} }
} }
parseMCI(lastBit); parseMCI(lastBit);
} }
self.emit('complete'); self.emit('complete');
}; };
/* /*
self.parse = function(buffer, savedRe) { self.parse = function(buffer, savedRe) {
// :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc.
// :TODO: move this to "constants" section @ top // :TODO: move this to "constants" section @ top
@ -329,164 +329,164 @@ function ANSIEscapeParser(options) {
}; };
*/ */
function escape(opCode, args) { function escape(opCode, args) {
let arg; let arg;
switch(opCode) { switch(opCode) {
// cursor up // cursor up
case 'A' : case 'A' :
//arg = args[0] || 1; //arg = args[0] || 1;
arg = isNaN(args[0]) ? 1 : args[0]; arg = isNaN(args[0]) ? 1 : args[0];
self.moveCursor(0, -arg); self.moveCursor(0, -arg);
break; break;
// cursor down // cursor down
case 'B' : case 'B' :
//arg = args[0] || 1; //arg = args[0] || 1;
arg = isNaN(args[0]) ? 1 : args[0]; arg = isNaN(args[0]) ? 1 : args[0];
self.moveCursor(0, arg); self.moveCursor(0, arg);
break; break;
// cursor forward/right // cursor forward/right
case 'C' : case 'C' :
//arg = args[0] || 1; //arg = args[0] || 1;
arg = isNaN(args[0]) ? 1 : args[0]; arg = isNaN(args[0]) ? 1 : args[0];
self.moveCursor(arg, 0); self.moveCursor(arg, 0);
break; break;
// cursor back/left // cursor back/left
case 'D' : case 'D' :
//arg = args[0] || 1; //arg = args[0] || 1;
arg = isNaN(args[0]) ? 1 : args[0]; arg = isNaN(args[0]) ? 1 : args[0];
self.moveCursor(-arg, 0); self.moveCursor(-arg, 0);
break; break;
case 'f' : // horiz & vertical case 'f' : // horiz & vertical
case 'H' : // cursor position case 'H' : // cursor position
//self.row = args[0] || 1; //self.row = args[0] || 1;
//self.column = args[1] || 1; //self.column = args[1] || 1;
self.row = isNaN(args[0]) ? 1 : args[0]; self.row = isNaN(args[0]) ? 1 : args[0];
self.column = isNaN(args[1]) ? 1 : args[1]; self.column = isNaN(args[1]) ? 1 : args[1];
//self.rowUpdated(); //self.rowUpdated();
self.positionUpdated(); self.positionUpdated();
break; break;
// save position // save position
case 's' : case 's' :
self.saveCursorPosition(); self.saveCursorPosition();
break; break;
// restore position // restore position
case 'u' : case 'u' :
self.restoreCursorPosition(); self.restoreCursorPosition();
break; break;
// set graphic rendition // set graphic rendition
case 'm' : case 'm' :
self.graphicRendition.reset = false; self.graphicRendition.reset = false;
for(let i = 0, len = args.length; i < len; ++i) { for(let i = 0, len = args.length; i < len; ++i) {
arg = args[i]; arg = args[i];
if(ANSIEscapeParser.foregroundColors[arg]) { if(ANSIEscapeParser.foregroundColors[arg]) {
self.graphicRendition.fg = arg; self.graphicRendition.fg = arg;
} else if(ANSIEscapeParser.backgroundColors[arg]) { } else if(ANSIEscapeParser.backgroundColors[arg]) {
self.graphicRendition.bg = arg; self.graphicRendition.bg = arg;
} else if(ANSIEscapeParser.styles[arg]) { } else if(ANSIEscapeParser.styles[arg]) {
switch(arg) { switch(arg) {
case 0 : case 0 :
// clear out everything // clear out everything
delete self.graphicRendition.intensity; delete self.graphicRendition.intensity;
delete self.graphicRendition.underline; delete self.graphicRendition.underline;
delete self.graphicRendition.blink; delete self.graphicRendition.blink;
delete self.graphicRendition.negative; delete self.graphicRendition.negative;
delete self.graphicRendition.invisible; delete self.graphicRendition.invisible;
delete self.graphicRendition.fg; delete self.graphicRendition.fg;
delete self.graphicRendition.bg; delete self.graphicRendition.bg;
self.graphicRendition.reset = true; self.graphicRendition.reset = true;
//self.graphicRendition.fg = 39; //self.graphicRendition.fg = 39;
//self.graphicRendition.bg = 49; //self.graphicRendition.bg = 49;
break; break;
case 1 : case 1 :
case 2 : case 2 :
case 22 : case 22 :
self.graphicRendition.intensity = arg; self.graphicRendition.intensity = arg;
break; break;
case 4 : case 4 :
case 24 : case 24 :
self.graphicRendition.underline = arg; self.graphicRendition.underline = arg;
break; break;
case 5 : case 5 :
case 6 : case 6 :
case 25 : case 25 :
self.graphicRendition.blink = arg; self.graphicRendition.blink = arg;
break; break;
case 7 : case 7 :
case 27 : case 27 :
self.graphicRendition.negative = arg; self.graphicRendition.negative = arg;
break; break;
case 8 : case 8 :
case 28 : case 28 :
self.graphicRendition.invisible = arg; self.graphicRendition.invisible = arg;
break; break;
default : default :
Log.trace( { attribute : arg }, 'Unknown attribute while parsing ANSI'); Log.trace( { attribute : arg }, 'Unknown attribute while parsing ANSI');
break; break;
} }
} }
} }
self.emit('sgr update', self.graphicRendition); self.emit('sgr update', self.graphicRendition);
break; // m break; // m
// :TODO: s, u, K // :TODO: s, u, K
// erase display/screen // erase display/screen
case 'J' : case 'J' :
// :TODO: Handle other 'J' types! // :TODO: Handle other 'J' types!
if(2 === args[0]) { if(2 === args[0]) {
self.clearScreen(); self.clearScreen();
} }
break; break;
} }
} }
} }
util.inherits(ANSIEscapeParser, events.EventEmitter); util.inherits(ANSIEscapeParser, events.EventEmitter);
ANSIEscapeParser.foregroundColors = { ANSIEscapeParser.foregroundColors = {
30 : 'black', 30 : 'black',
31 : 'red', 31 : 'red',
32 : 'green', 32 : 'green',
33 : 'yellow', 33 : 'yellow',
34 : 'blue', 34 : 'blue',
35 : 'magenta', 35 : 'magenta',
36 : 'cyan', 36 : 'cyan',
37 : 'white', 37 : 'white',
39 : 'default', // same as white for most implementations 39 : 'default', // same as white for most implementations
90 : 'grey' 90 : 'grey'
}; };
Object.freeze(ANSIEscapeParser.foregroundColors); Object.freeze(ANSIEscapeParser.foregroundColors);
ANSIEscapeParser.backgroundColors = { ANSIEscapeParser.backgroundColors = {
40 : 'black', 40 : 'black',
41 : 'red', 41 : 'red',
42 : 'green', 42 : 'green',
43 : 'yellow', 43 : 'yellow',
44 : 'blue', 44 : 'blue',
45 : 'magenta', 45 : 'magenta',
46 : 'cyan', 46 : 'cyan',
47 : 'white', 47 : 'white',
49 : 'default', // same as black for most implementations 49 : 'default', // same as black for most implementations
}; };
Object.freeze(ANSIEscapeParser.backgroundColors); Object.freeze(ANSIEscapeParser.backgroundColors);
@ -501,24 +501,24 @@ Object.freeze(ANSIEscapeParser.backgroundColors);
// can be grouped by concept here in code. // can be grouped by concept here in code.
// //
ANSIEscapeParser.styles = { ANSIEscapeParser.styles = {
0 : 'default', // Everything disabled 0 : 'default', // Everything disabled
1 : 'intensityBright', // aka bold 1 : 'intensityBright', // aka bold
2 : 'intensityDim', 2 : 'intensityDim',
22 : 'intensityNormal', 22 : 'intensityNormal',
4 : 'underlineOn', // Not supported by most BBS-like terminals 4 : 'underlineOn', // Not supported by most BBS-like terminals
24 : 'underlineOff', // Not supported by most BBS-like terminals 24 : 'underlineOff', // Not supported by most BBS-like terminals
5 : 'blinkSlow', // blinkSlow & blinkFast are generally treated the same 5 : 'blinkSlow', // blinkSlow & blinkFast are generally treated the same
6 : 'blinkFast', // blinkSlow & blinkFast are generally treated the same 6 : 'blinkFast', // blinkSlow & blinkFast are generally treated the same
25 : 'blinkOff', 25 : 'blinkOff',
7 : 'negativeImageOn', // Generally not supported or treated as "reverse FG & BG" 7 : 'negativeImageOn', // Generally not supported or treated as "reverse FG & BG"
27 : 'negativeImageOff', // Generally not supported or treated as "reverse FG & BG" 27 : 'negativeImageOff', // Generally not supported or treated as "reverse FG & BG"
8 : 'invisibleOn', // FG set to BG 8 : 'invisibleOn', // FG set to BG
28 : 'invisibleOff', // Not supported by most BBS-like terminals 28 : 'invisibleOff', // Not supported by most BBS-like terminals
}; };
Object.freeze(ANSIEscapeParser.styles); Object.freeze(ANSIEscapeParser.styles);

View File

@ -5,216 +5,216 @@
const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser;
const ANSI = require('./ansi_term.js'); const ANSI = require('./ansi_term.js');
const { const {
splitTextAtTerms, splitTextAtTerms,
renderStringLength renderStringLength
} = require('./string_util.js'); } = require('./string_util.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
module.exports = function ansiPrep(input, options, cb) { module.exports = function ansiPrep(input, options, cb) {
if(!input) { if(!input) {
return cb(null, ''); return cb(null, '');
} }
options.termWidth = options.termWidth || 80; options.termWidth = options.termWidth || 80;
options.termHeight = options.termHeight || 25; options.termHeight = options.termHeight || 25;
options.cols = options.cols || options.termWidth || 80; options.cols = options.cols || options.termWidth || 80;
options.rows = options.rows || options.termHeight || 'auto'; options.rows = options.rows || options.termHeight || 'auto';
options.startCol = options.startCol || 1; options.startCol = options.startCol || 1;
options.exportMode = options.exportMode || false; options.exportMode = options.exportMode || false;
options.fillLines = _.get(options, 'fillLines', true); options.fillLines = _.get(options, 'fillLines', true);
options.indent = options.indent || 0; options.indent = options.indent || 0;
// in auto we start out at 25 rows, but can always expand for more // in auto we start out at 25 rows, but can always expand for more
const canvas = Array.from( { length : 'auto' === options.rows ? 25 : options.rows }, () => Array.from( { length : options.cols}, () => new Object() ) ); const canvas = Array.from( { length : 'auto' === options.rows ? 25 : options.rows }, () => Array.from( { length : options.cols}, () => new Object() ) );
const parser = new ANSIEscapeParser( { termHeight : options.termHeight, termWidth : options.termWidth } ); const parser = new ANSIEscapeParser( { termHeight : options.termHeight, termWidth : options.termWidth } );
const state = { const state = {
row : 0, row : 0,
col : 0, col : 0,
}; };
let lastRow = 0; let lastRow = 0;
function ensureRow(row) { function ensureRow(row) {
if(canvas[row]) { if(canvas[row]) {
return; return;
} }
canvas[row] = Array.from( { length : options.cols}, () => new Object() ); canvas[row] = Array.from( { length : options.cols}, () => new Object() );
} }
parser.on('position update', (row, col) => { parser.on('position update', (row, col) => {
state.row = row - 1; state.row = row - 1;
state.col = col - 1; state.col = col - 1;
if(0 === state.col) { if(0 === state.col) {
state.initialSgr = state.lastSgr; state.initialSgr = state.lastSgr;
} }
lastRow = Math.max(state.row, lastRow); lastRow = Math.max(state.row, lastRow);
}); });
parser.on('literal', literal => { parser.on('literal', literal => {
// //
// CR/LF are handled for 'position update'; we don't need the chars themselves // CR/LF are handled for 'position update'; we don't need the chars themselves
// //
literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, ''); literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, '');
for(let c of literal) { for(let c of literal) {
if(state.col < options.cols && ('auto' === options.rows || state.row < options.rows)) { if(state.col < options.cols && ('auto' === options.rows || state.row < options.rows)) {
ensureRow(state.row); ensureRow(state.row);
if(0 === state.col) { if(0 === state.col) {
canvas[state.row][state.col].initialSgr = state.initialSgr; canvas[state.row][state.col].initialSgr = state.initialSgr;
} }
canvas[state.row][state.col].char = c; canvas[state.row][state.col].char = c;
if(state.sgr) { if(state.sgr) {
canvas[state.row][state.col].sgr = _.clone(state.sgr); canvas[state.row][state.col].sgr = _.clone(state.sgr);
state.lastSgr = canvas[state.row][state.col].sgr; state.lastSgr = canvas[state.row][state.col].sgr;
state.sgr = null; state.sgr = null;
} }
} }
state.col += 1; state.col += 1;
} }
}); });
parser.on('sgr update', sgr => { parser.on('sgr update', sgr => {
ensureRow(state.row); ensureRow(state.row);
if(state.col < options.cols) { if(state.col < options.cols) {
canvas[state.row][state.col].sgr = _.clone(sgr); canvas[state.row][state.col].sgr = _.clone(sgr);
state.lastSgr = canvas[state.row][state.col].sgr; state.lastSgr = canvas[state.row][state.col].sgr;
} else { } else {
state.sgr = sgr; state.sgr = sgr;
} }
}); });
function getLastPopulatedColumn(row) { function getLastPopulatedColumn(row) {
let col = row.length; let col = row.length;
while(--col > 0) { while(--col > 0) {
if(row[col].char || row[col].sgr) { if(row[col].char || row[col].sgr) {
break; break;
} }
} }
return col; return col;
} }
parser.on('complete', () => { parser.on('complete', () => {
let output = ''; let output = '';
let line; let line;
let sgr; let sgr;
canvas.slice(0, lastRow + 1).forEach(row => { canvas.slice(0, lastRow + 1).forEach(row => {
const lastCol = getLastPopulatedColumn(row) + 1; const lastCol = getLastPopulatedColumn(row) + 1;
let i; let i;
line = options.indent ? line = options.indent ?
output.length > 0 ? ' '.repeat(options.indent) : '' : output.length > 0 ? ' '.repeat(options.indent) : '' :
''; '';
for(i = 0; i < lastCol; ++i) { for(i = 0; i < lastCol; ++i) {
const col = row[i]; const col = row[i];
sgr = !options.asciiMode && 0 === i ? sgr = !options.asciiMode && 0 === i ?
col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' : col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' :
''; '';
if(!options.asciiMode && col.sgr) { if(!options.asciiMode && col.sgr) {
sgr += ANSI.getSGRFromGraphicRendition(col.sgr); sgr += ANSI.getSGRFromGraphicRendition(col.sgr);
} }
line += `${sgr}${col.char || ' '}`; line += `${sgr}${col.char || ' '}`;
} }
output += line; output += line;
if(i < row.length) { if(i < row.length) {
output += `${options.asciiMode ? '' : ANSI.blackBG()}`; output += `${options.asciiMode ? '' : ANSI.blackBG()}`;
if(options.fillLines) { if(options.fillLines) {
output += `${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`; output += `${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`;
} }
} }
if(options.startCol + i < options.termWidth || options.forceLineTerm) { if(options.startCol + i < options.termWidth || options.forceLineTerm) {
output += '\r\n'; output += '\r\n';
} }
}); });
if(options.exportMode) { if(options.exportMode) {
// //
// If we're in export mode, we do some additional hackery: // If we're in export mode, we do some additional hackery:
// //
// * Hard wrap ALL lines at <= 79 *characters* (not visible columns) // * Hard wrap ALL lines at <= 79 *characters* (not visible columns)
// if a line must wrap early, we'll place a ESC[A ESC[<N>C where <N> // if a line must wrap early, we'll place a ESC[A ESC[<N>C where <N>
// represents chars to get back to the position we were previously at // represents chars to get back to the position we were previously at
// //
// * Replace contig spaces with ESC[<N>C as well to save... space. // * Replace contig spaces with ESC[<N>C as well to save... space.
// //
// :TODO: this would be better to do as part of the processing above, but this will do for now // :TODO: this would be better to do as part of the processing above, but this will do for now
const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with
let exportOutput = ''; let exportOutput = '';
let m; let m;
let afterSeq; let afterSeq;
let wantMore; let wantMore;
let renderStart; let renderStart;
splitTextAtTerms(output).forEach(fullLine => { splitTextAtTerms(output).forEach(fullLine => {
renderStart = 0; renderStart = 0;
while(fullLine.length > 0) { while(fullLine.length > 0) {
let splitAt; let splitAt;
const ANSI_REGEXP = ANSI.getFullMatchRegExp(); const ANSI_REGEXP = ANSI.getFullMatchRegExp();
wantMore = true; wantMore = true;
while((m = ANSI_REGEXP.exec(fullLine))) { while((m = ANSI_REGEXP.exec(fullLine))) {
afterSeq = m.index + m[0].length; afterSeq = m.index + m[0].length;
if(afterSeq < MAX_CHARS) { if(afterSeq < MAX_CHARS) {
// after current seq // after current seq
splitAt = afterSeq; splitAt = afterSeq;
} else { } else {
if(m.index < MAX_CHARS) { if(m.index < MAX_CHARS) {
// before last found seq // before last found seq
splitAt = m.index; splitAt = m.index;
wantMore = false; // can't eat up any more wantMore = false; // can't eat up any more
} }
break; // seq's beyond this point are >= MAX_CHARS break; // seq's beyond this point are >= MAX_CHARS
} }
} }
if(splitAt) { if(splitAt) {
if(wantMore) { if(wantMore) {
splitAt = Math.min(fullLine.length, MAX_CHARS - 1); splitAt = Math.min(fullLine.length, MAX_CHARS - 1);
} }
} else { } else {
splitAt = Math.min(fullLine.length, MAX_CHARS - 1); splitAt = Math.min(fullLine.length, MAX_CHARS - 1);
} }
const part = fullLine.slice(0, splitAt); const part = fullLine.slice(0, splitAt);
fullLine = fullLine.slice(splitAt); fullLine = fullLine.slice(splitAt);
renderStart += renderStringLength(part); renderStart += renderStringLength(part);
exportOutput += `${part}\r\n`; exportOutput += `${part}\r\n`;
if(fullLine.length > 0) { // more to go for this line? if(fullLine.length > 0) { // more to go for this line?
exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`; exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`;
} else { } else {
exportOutput += ANSI.up(); exportOutput += ANSI.up();
} }
} }
}); });
return cb(null, exportOutput); return cb(null, exportOutput);
} }
return cb(null, output); return cb(null, output);
}); });
parser.parse(input); parser.parse(input);
}; };

View File

@ -65,86 +65,86 @@ exports.vtxHyperlink = vtxHyperlink;
const ESC_CSI = '\u001b['; const ESC_CSI = '\u001b[';
const CONTROL = { const CONTROL = {
up : 'A', up : 'A',
down : 'B', down : 'B',
forward : 'C', forward : 'C',
right : 'C', right : 'C',
back : 'D', back : 'D',
left : 'D', left : 'D',
nextLine : 'E', nextLine : 'E',
prevLine : 'F', prevLine : 'F',
horizAbsolute : 'G', horizAbsolute : 'G',
// //
// CSI [ p1 ] J // CSI [ p1 ] J
// Erase in Page / Erase Data // Erase in Page / Erase Data
// Defaults: p1 = 0 // Defaults: p1 = 0
// Erases from the current screen according to the value of p1 // Erases from the current screen according to the value of p1
// 0 - Erase from the current position to the end of the screen. // 0 - Erase from the current position to the end of the screen.
// 1 - Erase from the current position to the start of the screen. // 1 - Erase from the current position to the start of the screen.
// 2 - Erase entire screen. As a violation of ECMA-048, also moves // 2 - Erase entire screen. As a violation of ECMA-048, also moves
// the cursor to position 1/1 as a number of BBS programs assume // the cursor to position 1/1 as a number of BBS programs assume
// this behaviour. // this behaviour.
// Erased characters are set to the current attribute. // Erased characters are set to the current attribute.
// //
// Support: // Support:
// * SyncTERM: Works as expected // * SyncTERM: Works as expected
// * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1 // * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1
// and screen remainder // and screen remainder
// //
eraseData : 'J', eraseData : 'J',
eraseLine : 'K', eraseLine : 'K',
insertLine : 'L', insertLine : 'L',
// //
// CSI [ p1 ] M // CSI [ p1 ] M
// Delete Line(s) / "ANSI" Music // Delete Line(s) / "ANSI" Music
// Defaults: p1 = 1 // Defaults: p1 = 1
// Deletes the current line and the p1 - 1 lines after it scrolling the // Deletes the current line and the p1 - 1 lines after it scrolling the
// first non-deleted line up to the current line and filling the newly // first non-deleted line up to the current line and filling the newly
// empty lines at the end of the screen with the current attribute. // empty lines at the end of the screen with the current attribute.
// If "ANSI" Music is fully enabled (CSI = 2 M), performs "ANSI" music // If "ANSI" Music is fully enabled (CSI = 2 M), performs "ANSI" music
// instead. // instead.
// See "ANSI" MUSIC section for more details. // See "ANSI" MUSIC section for more details.
// //
// Support: // Support:
// * SyncTERM: Works as expected // * SyncTERM: Works as expected
// * NetRunner: // * NetRunner:
// //
// General Notes: // General Notes:
// See also notes in bansi.txt and cterm.txt about the various // See also notes in bansi.txt and cterm.txt about the various
// incompatibilities & oddities around this sequence. ANSI-BBS // incompatibilities & oddities around this sequence. ANSI-BBS
// states that it *should* work with any value of p1. // states that it *should* work with any value of p1.
// //
deleteLine : 'M', deleteLine : 'M',
ansiMusic : 'M', ansiMusic : 'M',
scrollUp : 'S', scrollUp : 'S',
scrollDown : 'T', scrollDown : 'T',
setScrollRegion : 'r', setScrollRegion : 'r',
savePos : 's', savePos : 's',
restorePos : 'u', restorePos : 'u',
queryPos : '6n', queryPos : '6n',
queryScreenSize : '255n', // See bansi.txt queryScreenSize : '255n', // See bansi.txt
goto : 'H', // row Pr, column Pc -- same as f goto : 'H', // row Pr, column Pc -- same as f
gotoAlt : 'f', // same as H gotoAlt : 'f', // same as H
blinkToBrightIntensity : '?33h', blinkToBrightIntensity : '?33h',
blinkNormal : '?33l', blinkNormal : '?33l',
emulationSpeed : '*r', // Set output emulation speed. See cterm.txt emulationSpeed : '*r', // Set output emulation speed. See cterm.txt
hideCursor : '?25l', // Nonstandard - cterm.txt hideCursor : '?25l', // Nonstandard - cterm.txt
showCursor : '?25h', // Nonstandard - cterm.txt showCursor : '?25h', // Nonstandard - cterm.txt
queryDeviceAttributes : 'c', // Nonstandard - cterm.txt queryDeviceAttributes : 'c', // Nonstandard - cterm.txt
// :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes // :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes
// apparently some terms can report screen size and text area via 18t and 19t // apparently some terms can report screen size and text area via 18t and 19t
}; };
// //
@ -152,49 +152,49 @@ const CONTROL = {
// 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
// //
const SGRValues = { const SGRValues = {
reset : 0, reset : 0,
bold : 1, bold : 1,
dim : 2, dim : 2,
blink : 5, blink : 5,
fastBlink : 6, fastBlink : 6,
negative : 7, negative : 7,
hidden : 8, hidden : 8,
normal : 22, // normal : 22, //
steady : 25, steady : 25,
positive : 27, positive : 27,
black : 30, black : 30,
red : 31, red : 31,
green : 32, green : 32,
yellow : 33, yellow : 33,
blue : 34, blue : 34,
magenta : 35, magenta : 35,
cyan : 36, cyan : 36,
white : 37, white : 37,
blackBG : 40, blackBG : 40,
redBG : 41, redBG : 41,
greenBG : 42, greenBG : 42,
yellowBG : 43, yellowBG : 43,
blueBG : 44, blueBG : 44,
magentaBG : 45, magentaBG : 45,
cyanBG : 46, cyanBG : 46,
whiteBG : 47, whiteBG : 47,
}; };
function getFullMatchRegExp(flags = 'g') { function getFullMatchRegExp(flags = 'g') {
// :TODO: expand this a bit - see strip-ansi/etc. // :TODO: expand this a bit - see strip-ansi/etc.
// :TODO: \u009b ? // :TODO: \u009b ?
return new RegExp(/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/, flags); // eslint-disable-line no-control-regex return new RegExp(/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/, flags); // eslint-disable-line no-control-regex
} }
function getFGColorValue(name) { function getFGColorValue(name) {
return SGRValues[name]; return SGRValues[name];
} }
function getBGColorValue(name) { function getBGColorValue(name) {
return SGRValues[name + 'BG']; return SGRValues[name + 'BG'];
} }
@ -214,49 +214,49 @@ 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
// //
const SYNCTERM_FONT_AND_ENCODING_TABLE = [ const SYNCTERM_FONT_AND_ENCODING_TABLE = [
'cp437', 'cp437',
'cp1251', 'cp1251',
'koi8_r', 'koi8_r',
'iso8859_2', 'iso8859_2',
'iso8859_4', 'iso8859_4',
'cp866', 'cp866',
'iso8859_9', 'iso8859_9',
'haik8', 'haik8',
'iso8859_8', 'iso8859_8',
'koi8_u', 'koi8_u',
'iso8859_15', 'iso8859_15',
'iso8859_4', 'iso8859_4',
'koi8_r_b', 'koi8_r_b',
'iso8859_4', 'iso8859_4',
'iso8859_5', 'iso8859_5',
'ARMSCII_8', 'ARMSCII_8',
'iso8859_15', 'iso8859_15',
'cp850', 'cp850',
'cp850', 'cp850',
'cp885', 'cp885',
'cp1251', 'cp1251',
'iso8859_7', 'iso8859_7',
'koi8-r_c', 'koi8-r_c',
'iso8859_4', 'iso8859_4',
'iso8859_1', 'iso8859_1',
'cp866', 'cp866',
'cp437', 'cp437',
'cp866', 'cp866',
'cp885', 'cp885',
'cp866_u', 'cp866_u',
'iso8859_1', 'iso8859_1',
'cp1131', 'cp1131',
'c64_upper', 'c64_upper',
'c64_lower', 'c64_lower',
'c128_upper', 'c128_upper',
'c128_lower', 'c128_lower',
'atari', 'atari',
'pot_noodle', 'pot_noodle',
'mo_soul', 'mo_soul',
'microknight_plus', 'microknight_plus',
'topaz_plus', 'topaz_plus',
'microknight', 'microknight',
'topaz', 'topaz',
]; ];
// //
@ -267,137 +267,137 @@ const SYNCTERM_FONT_AND_ENCODING_TABLE = [
// replaced with '_' for lookup purposes. // replaced with '_' for lookup purposes.
// //
const 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',
'ibm_pc' : 'cp437', 'ibm_pc' : 'cp437',
'pc' : 'cp437', 'pc' : 'cp437',
'cp437_art' : 'cp437', 'cp437_art' : 'cp437',
'ibmpcart' : 'cp437', 'ibmpcart' : 'cp437',
'ibmpc_art' : 'cp437', 'ibmpc_art' : 'cp437',
'ibm_pc_art' : 'cp437', 'ibm_pc_art' : 'cp437',
'msdos_art' : 'cp437', 'msdos_art' : 'cp437',
'msdosart' : 'cp437', 'msdosart' : 'cp437',
'pc_art' : 'cp437', 'pc_art' : 'cp437',
'pcart' : 'cp437', 'pcart' : 'cp437',
'ibm_vga50' : 'cp437', 'ibm_vga50' : 'cp437',
'ibm_vga25g' : 'cp437', 'ibm_vga25g' : 'cp437',
'ibm_ega' : 'cp437', 'ibm_ega' : 'cp437',
'ibm_ega43' : 'cp437', 'ibm_ega43' : 'cp437',
'topaz' : 'topaz', 'topaz' : 'topaz',
'amiga_topaz_1' : 'topaz', 'amiga_topaz_1' : 'topaz',
'amiga_topaz_1+' : 'topaz_plus', 'amiga_topaz_1+' : 'topaz_plus',
'topazplus' : 'topaz_plus', 'topazplus' : 'topaz_plus',
'topaz_plus' : 'topaz_plus', 'topaz_plus' : 'topaz_plus',
'amiga_topaz_2' : 'topaz', 'amiga_topaz_2' : 'topaz',
'amiga_topaz_2+' : 'topaz_plus', 'amiga_topaz_2+' : 'topaz_plus',
'topaz2plus' : 'topaz_plus', 'topaz2plus' : 'topaz_plus',
'pot_noodle' : 'pot_noodle', 'pot_noodle' : 'pot_noodle',
'p0tnoodle' : 'pot_noodle', 'p0tnoodle' : 'pot_noodle',
'amiga_p0t-noodle' : 'pot_noodle', 'amiga_p0t-noodle' : 'pot_noodle',
'mo_soul' : 'mo_soul', 'mo_soul' : 'mo_soul',
'mosoul' : 'mo_soul', 'mosoul' : 'mo_soul',
'mO\'sOul' : 'mo_soul', 'mO\'sOul' : 'mo_soul',
'amiga_microknight' : 'microknight', 'amiga_microknight' : 'microknight',
'amiga_microknight+' : 'microknight_plus', 'amiga_microknight+' : 'microknight_plus',
'atari' : 'atari', 'atari' : 'atari',
'atarist' : 'atari', 'atarist' : 'atari',
}; };
function setSyncTERMFont(name, fontPage) { function setSyncTERMFont(name, fontPage) {
const p1 = miscUtil.valueWithDefault(fontPage, 0); const p1 = miscUtil.valueWithDefault(fontPage, 0);
assert(p1 >= 0 && p1 <= 3); assert(p1 >= 0 && p1 <= 3);
const 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 '';
} }
function getSyncTERMFontFromAlias(alias) { function getSyncTERMFontFromAlias(alias) {
return FONT_ALIAS_TO_SYNCTERM_MAP[alias.toLowerCase().replace(/ /g, '_')]; return FONT_ALIAS_TO_SYNCTERM_MAP[alias.toLowerCase().replace(/ /g, '_')];
} }
function setSyncTermFontWithAlias(nameOrAlias) { function setSyncTermFontWithAlias(nameOrAlias) {
nameOrAlias = getSyncTERMFontFromAlias(nameOrAlias) || nameOrAlias; nameOrAlias = getSyncTERMFontFromAlias(nameOrAlias) || nameOrAlias;
return setSyncTERMFont(nameOrAlias); return setSyncTERMFont(nameOrAlias);
} }
const DEC_CURSOR_STYLE = { const DEC_CURSOR_STYLE = {
'blinking block' : 0, 'blinking block' : 0,
'default' : 1, 'default' : 1,
'steady block' : 2, 'steady block' : 2,
'blinking underline' : 3, 'blinking underline' : 3,
'steady underline' : 4, 'steady underline' : 4,
'blinking bar' : 5, 'blinking bar' : 5,
'steady bar' : 6, 'steady bar' : 6,
}; };
function setCursorStyle(cursorStyle) { function setCursorStyle(cursorStyle) {
const 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 '';
} }
// 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) {
const code = CONTROL[name]; const code = CONTROL[name];
exports[name] = function() { exports[name] = function() {
let 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( name => { Object.keys(SGRValues).forEach( name => {
const code = SGRValues[name]; const code = SGRValues[name];
exports[name] = function() { exports[name] = function() {
return `${ESC_CSI}${code}m`; return `${ESC_CSI}${code}m`;
}; };
}); });
function sgr() { function sgr() {
// //
// - Allow an single array or variable number of arguments // - Allow an single array or variable number of arguments
// - Each element can be either a integer or string found in SGRValues // - Each element can be either a integer or string found in SGRValues
// which in turn maps to a integer // which in turn maps to a integer
// //
if(arguments.length <= 0) { if(arguments.length <= 0) {
return ''; return '';
} }
let result = []; let result = [];
const args = Array.isArray(arguments[0]) ? arguments[0] : arguments; const args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
for(let i = 0; i < args.length; ++i) { for(let i = 0; i < args.length; ++i) {
const arg = args[i]; const arg = args[i];
if(_.isString(arg) && arg in SGRValues) { if(_.isString(arg) && arg in SGRValues) {
result.push(SGRValues[arg]); result.push(SGRValues[arg]);
} else if(_.isNumber(arg)) { } else if(_.isNumber(arg)) {
result.push(arg); result.push(arg);
} }
} }
return `${ESC_CSI}${result.join(';')}m`; return `${ESC_CSI}${result.join(';')}m`;
} }
// //
@ -405,29 +405,29 @@ function sgr() {
// to a ANSI SGR sequence. // to a ANSI SGR sequence.
// //
function getSGRFromGraphicRendition(graphicRendition, initialReset) { function getSGRFromGraphicRendition(graphicRendition, initialReset) {
let sgrSeq = []; let sgrSeq = [];
let styleCount = 0; let styleCount = 0;
[ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => { [ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => {
if(graphicRendition[s]) { if(graphicRendition[s]) {
sgrSeq.push(graphicRendition[s]); sgrSeq.push(graphicRendition[s]);
++styleCount; ++styleCount;
} }
}); });
if(graphicRendition.fg) { if(graphicRendition.fg) {
sgrSeq.push(graphicRendition.fg); sgrSeq.push(graphicRendition.fg);
} }
if(graphicRendition.bg) { if(graphicRendition.bg) {
sgrSeq.push(graphicRendition.bg); sgrSeq.push(graphicRendition.bg);
} }
if(0 === styleCount || initialReset) { if(0 === styleCount || initialReset) {
sgrSeq.unshift(0); sgrSeq.unshift(0);
} }
return sgr(sgrSeq); return sgr(sgrSeq);
} }
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -435,19 +435,19 @@ function getSGRFromGraphicRendition(graphicRendition, initialReset) {
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
function clearScreen() { function clearScreen() {
return exports.eraseData(2); return exports.eraseData(2);
} }
function resetScreen() { function resetScreen() {
return `${exports.reset()}${exports.eraseData(2)}${exports.goHome()}`; return `${exports.reset()}${exports.eraseData(2)}${exports.goHome()}`;
} }
function normal() { function normal() {
return sgr( [ 'normal', 'reset' ] ); return sgr( [ 'normal', 'reset' ] );
} }
function goHome() { function goHome() {
return exports.goto(); // no params = home = 1,1 return exports.goto(); // no params = home = 1,1
} }
// //
@ -463,36 +463,36 @@ function goHome() {
// and use term width -- generally 80 columns -- will display garbled! // 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) {
const speed = { const speed = {
unlimited : 0, unlimited : 0,
off : 0, off : 0,
0 : 0, 0 : 0,
300 : 1, 300 : 1,
600 : 2, 600 : 2,
1200 : 3, 1200 : 3,
2400 : 4, 2400 : 4,
4800 : 5, 4800 : 5,
9600 : 6, 9600 : 6,
19200 : 7, 19200 : 7,
38400 : 8, 38400 : 8,
57600 : 9, 57600 : 9,
76800 : 10, 76800 : 10,
115200 : 11, 115200 : 11,
}[rate] || 0; }[rate] || 0;
return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed); return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed);
} }
function vtxHyperlink(client, url, len) { function vtxHyperlink(client, url, len) {
if(!client.terminalSupports('vtx_hyperlink')) { if(!client.terminalSupports('vtx_hyperlink')) {
return ''; return '';
} }
len = len || url.length; len = len || url.length;
url = url.split('').map(c => c.charCodeAt(0)).join(';'); url = url.split('').map(c => c.charCodeAt(0)).join(';');
return `${ESC_CSI}1;${len};1;1;${url}\\`; return `${ESC_CSI}1;${len};1;1;${url}\\`;
} }

View File

@ -16,314 +16,314 @@ const paths = require('path');
let archiveUtil; let archiveUtil;
class Archiver { class Archiver {
constructor(config) { constructor(config) {
this.compress = config.compress; this.compress = config.compress;
this.decompress = config.decompress; this.decompress = config.decompress;
this.list = config.list; this.list = config.list;
this.extract = config.extract; this.extract = config.extract;
} }
ok() { ok() {
return this.canCompress() && this.canDecompress(); return this.canCompress() && this.canDecompress();
} }
can(what) { can(what) {
if(!_.has(this, [ what, 'cmd' ]) || !_.has(this, [ what, 'args' ])) { if(!_.has(this, [ what, 'cmd' ]) || !_.has(this, [ what, 'args' ])) {
return false; return false;
} }
return _.isString(this[what].cmd) && Array.isArray(this[what].args) && this[what].args.length > 0; return _.isString(this[what].cmd) && Array.isArray(this[what].args) && this[what].args.length > 0;
} }
canCompress() { return this.can('compress'); } canCompress() { return this.can('compress'); }
canDecompress() { return this.can('decompress'); } canDecompress() { return this.can('decompress'); }
canList() { return this.can('list'); } // :TODO: validate entryMatch canList() { return this.can('list'); } // :TODO: validate entryMatch
canExtract() { return this.can('extract'); } canExtract() { return this.can('extract'); }
} }
module.exports = class ArchiveUtil { module.exports = class ArchiveUtil {
constructor() { constructor() {
this.archivers = {}; this.archivers = {};
this.longestSignature = 0; this.longestSignature = 0;
} }
// singleton access // singleton access
static getInstance() { static getInstance() {
if(!archiveUtil) { if(!archiveUtil) {
archiveUtil = new ArchiveUtil(); archiveUtil = new ArchiveUtil();
archiveUtil.init(); archiveUtil.init();
} }
return archiveUtil; return archiveUtil;
} }
init() { init() {
// //
// Load configuration // Load configuration
// //
const config = Config(); const config = Config();
if(_.has(config, 'archives.archivers')) { if(_.has(config, 'archives.archivers')) {
Object.keys(config.archives.archivers).forEach(archKey => { Object.keys(config.archives.archivers).forEach(archKey => {
const archConfig = config.archives.archivers[archKey]; const archConfig = config.archives.archivers[archKey];
const archiver = new Archiver(archConfig); const archiver = new Archiver(archConfig);
if(!archiver.ok()) { if(!archiver.ok()) {
// :TODO: Log warning - bad archiver/config // :TODO: Log warning - bad archiver/config
} }
this.archivers[archKey] = archiver; this.archivers[archKey] = archiver;
}); });
} }
if(_.isObject(config.fileTypes)) { if(_.isObject(config.fileTypes)) {
const updateSig = (ft) => { const updateSig = (ft) => {
ft.sig = Buffer.from(ft.sig, 'hex'); ft.sig = Buffer.from(ft.sig, 'hex');
ft.offset = ft.offset || 0; ft.offset = ft.offset || 0;
// :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well
const sigLen = ft.offset + ft.sig.length; const sigLen = ft.offset + ft.sig.length;
if(sigLen > this.longestSignature) { if(sigLen > this.longestSignature) {
this.longestSignature = sigLen; this.longestSignature = sigLen;
} }
}; };
Object.keys(config.fileTypes).forEach(mimeType => { Object.keys(config.fileTypes).forEach(mimeType => {
const fileType = config.fileTypes[mimeType]; const fileType = config.fileTypes[mimeType];
if(Array.isArray(fileType)) { if(Array.isArray(fileType)) {
fileType.forEach(ft => { fileType.forEach(ft => {
if(ft.sig) { if(ft.sig) {
updateSig(ft); updateSig(ft);
} }
}); });
} else if(fileType.sig) { } else if(fileType.sig) {
updateSig(fileType); updateSig(fileType);
} }
}); });
} }
} }
getArchiver(mimeTypeOrExtension, justExtention) { getArchiver(mimeTypeOrExtension, justExtention) {
const mimeType = resolveMimeType(mimeTypeOrExtension); const mimeType = resolveMimeType(mimeTypeOrExtension);
if(!mimeType) { // lookup returns false on failure if(!mimeType) { // lookup returns false on failure
return; return;
} }
const config = Config(); const config = Config();
let fileType = _.get(config, [ 'fileTypes', mimeType ] ); let fileType = _.get(config, [ 'fileTypes', mimeType ] );
if(Array.isArray(fileType)) { if(Array.isArray(fileType)) {
if(!justExtention) { if(!justExtention) {
// need extention for lookup; ambiguous as-is :( // need extention for lookup; ambiguous as-is :(
return; return;
} }
// further refine by extention // further refine by extention
fileType = fileType.find(ft => justExtention === ft.ext); fileType = fileType.find(ft => justExtention === ft.ext);
} }
if(!_.isObject(fileType)) { if(!_.isObject(fileType)) {
return; return;
} }
if(fileType.archiveHandler) { if(fileType.archiveHandler) {
return _.get( config, [ 'archives', 'archivers', fileType.archiveHandler ] ); return _.get( config, [ 'archives', 'archivers', fileType.archiveHandler ] );
} }
} }
haveArchiver(archType) { haveArchiver(archType) {
return this.getArchiver(archType) ? true : false; return this.getArchiver(archType) ? true : false;
} }
// :TODO: implement me: // :TODO: implement me:
/* /*
detectTypeWithBuf(buf, cb) { detectTypeWithBuf(buf, cb) {
} }
*/ */
detectType(path, cb) { detectType(path, cb) {
fs.open(path, 'r', (err, fd) => { fs.open(path, 'r', (err, fd) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const buf = Buffer.alloc(this.longestSignature); const buf = Buffer.alloc(this.longestSignature);
fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => { fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const archFormat = _.findKey(Config().fileTypes, fileTypeInfo => { const archFormat = _.findKey(Config().fileTypes, fileTypeInfo => {
const fileTypeInfos = Array.isArray(fileTypeInfo) ? fileTypeInfo : [ fileTypeInfo ]; const fileTypeInfos = Array.isArray(fileTypeInfo) ? fileTypeInfo : [ fileTypeInfo ];
return fileTypeInfos.find(fti => { return fileTypeInfos.find(fti => {
if(!fti.sig || !fti.archiveHandler) { if(!fti.sig || !fti.archiveHandler) {
return false; return false;
} }
const lenNeeded = fti.offset + fti.sig.length; const lenNeeded = fti.offset + fti.sig.length;
if(bytesRead < lenNeeded) { if(bytesRead < lenNeeded) {
return false; return false;
} }
const comp = buf.slice(fti.offset, fti.offset + fti.sig.length); const comp = buf.slice(fti.offset, fti.offset + fti.sig.length);
return (fti.sig.equals(comp)); return (fti.sig.equals(comp));
}); });
}); });
return cb(archFormat ? null : Errors.General('Unknown type'), archFormat); return cb(archFormat ? null : Errors.General('Unknown type'), archFormat);
}); });
}); });
} }
spawnHandler(proc, action, cb) { spawnHandler(proc, action, cb) {
// pty.js doesn't currently give us a error when things fail, // pty.js doesn't currently give us a error when things fail,
// so we have this horrible, horrible hack: // so we have this horrible, horrible hack:
let err; let err;
proc.once('data', d => { proc.once('data', d => {
if(_.isString(d) && d.startsWith('execvp(3) failed.')) { if(_.isString(d) && d.startsWith('execvp(3) failed.')) {
err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`); err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`);
} }
}); });
proc.once('exit', exitCode => { proc.once('exit', exitCode => {
return cb(exitCode ? Errors.ExternalProcess(`${action} failed with exit code: ${exitCode}`) : err); return cb(exitCode ? Errors.ExternalProcess(`${action} failed with exit code: ${exitCode}`) : err);
}); });
} }
compressTo(archType, archivePath, files, cb) { compressTo(archType, archivePath, files, cb) {
const archiver = this.getArchiver(archType, paths.extname(archivePath)); const archiver = this.getArchiver(archType, paths.extname(archivePath));
if(!archiver) { if(!archiver) {
return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
} }
const fmtObj = { const fmtObj = {
archivePath : archivePath, archivePath : archivePath,
fileList : files.join(' '), // :TODO: probably need same hack as extractTo here! fileList : files.join(' '), // :TODO: probably need same hack as extractTo here!
}; };
const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) ); const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) );
let proc; let proc;
try { try {
proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts()); proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts());
} catch(e) { } catch(e) {
return cb(e); return cb(e);
} }
return this.spawnHandler(proc, 'Compression', cb); return this.spawnHandler(proc, 'Compression', cb);
} }
extractTo(archivePath, extractPath, archType, fileList, cb) { extractTo(archivePath, extractPath, archType, fileList, cb) {
let haveFileList; let haveFileList;
if(!cb && _.isFunction(fileList)) { if(!cb && _.isFunction(fileList)) {
cb = fileList; cb = fileList;
fileList = []; fileList = [];
haveFileList = false; haveFileList = false;
} else { } else {
haveFileList = true; haveFileList = true;
} }
const archiver = this.getArchiver(archType, paths.extname(archivePath)); const archiver = this.getArchiver(archType, paths.extname(archivePath));
if(!archiver) { if(!archiver) {
return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
} }
const fmtObj = { const fmtObj = {
archivePath : archivePath, archivePath : archivePath,
extractPath : extractPath, extractPath : extractPath,
}; };
let action = haveFileList ? 'extract' : 'decompress'; let action = haveFileList ? 'extract' : 'decompress';
if('extract' === action && !_.isObject(archiver[action])) { if('extract' === action && !_.isObject(archiver[action])) {
// we're forced to do a full decompress // we're forced to do a full decompress
action = 'decompress'; action = 'decompress';
haveFileList = false; haveFileList = false;
} }
// we need to treat {fileList} special in that it should be broken up to 0:n args // we need to treat {fileList} special in that it should be broken up to 0:n args
const args = archiver[action].args.map( arg => { const args = archiver[action].args.map( arg => {
return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj); return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj);
}); });
const fileListPos = args.indexOf('{fileList}'); const fileListPos = args.indexOf('{fileList}');
if(fileListPos > -1) { if(fileListPos > -1) {
// replace {fileList} with 0:n sep file list arguments // replace {fileList} with 0:n sep file list arguments
args.splice.apply(args, [fileListPos, 1].concat(fileList)); args.splice.apply(args, [fileListPos, 1].concat(fileList));
} }
let proc; let proc;
try { try {
proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath)); proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath));
} catch(e) { } catch(e) {
return cb(e); return cb(e);
} }
return this.spawnHandler(proc, (haveFileList ? 'Extraction' : 'Decompression'), cb); return this.spawnHandler(proc, (haveFileList ? 'Extraction' : 'Decompression'), cb);
} }
listEntries(archivePath, archType, cb) { listEntries(archivePath, archType, cb) {
const archiver = this.getArchiver(archType, paths.extname(archivePath)); const archiver = this.getArchiver(archType, paths.extname(archivePath));
if(!archiver) { if(!archiver) {
return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
} }
const fmtObj = { const fmtObj = {
archivePath : archivePath, archivePath : archivePath,
}; };
const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) ); const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) );
let proc; let proc;
try { try {
proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts()); proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts());
} catch(e) { } catch(e) {
return cb(e); return cb(e);
} }
let output = ''; let output = '';
proc.on('data', data => { proc.on('data', data => {
// :TODO: hack for: execvp(3) failed.: No such file or directory // :TODO: hack for: execvp(3) failed.: No such file or directory
output += data; output += data;
}); });
proc.once('exit', exitCode => { proc.once('exit', exitCode => {
if(exitCode) { if(exitCode) {
return cb(Errors.ExternalProcess(`List failed with exit code: ${exitCode}`)); return cb(Errors.ExternalProcess(`List failed with exit code: ${exitCode}`));
} }
const entryGroupOrder = archiver.list.entryGroupOrder || { byteSize : 1, fileName : 2 }; const entryGroupOrder = archiver.list.entryGroupOrder || { byteSize : 1, fileName : 2 };
const entries = []; const entries = [];
const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm'); const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm');
let m; let m;
while((m = entryMatchRe.exec(output))) { while((m = entryMatchRe.exec(output))) {
entries.push({ entries.push({
byteSize : parseInt(m[entryGroupOrder.byteSize]), byteSize : parseInt(m[entryGroupOrder.byteSize]),
fileName : m[entryGroupOrder.fileName].trim(), fileName : m[entryGroupOrder.fileName].trim(),
}); });
} }
return cb(null, entries); return cb(null, entries);
}); });
} }
getPtyOpts(extractPath) { getPtyOpts(extractPath) {
const opts = { const opts = {
name : 'enigma-archiver', name : 'enigma-archiver',
cols : 80, cols : 80,
rows : 24, rows : 24,
env : process.env, env : process.env,
}; };
if(extractPath) { if(extractPath) {
opts.cwd = extractPath; opts.cwd = extractPath;
} }
// :TODO: set cwd to supplied temp path if not sepcific extract // :TODO: set cwd to supplied temp path if not sepcific extract
return opts; return opts;
} }
}; };

View File

@ -26,87 +26,87 @@ exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
// :TODO: return font + font mapped information from SAUCE // :TODO: return font + font mapped information from SAUCE
const SUPPORTED_ART_TYPES = { const SUPPORTED_ART_TYPES = {
// :TODO: the defualt encoding are really useless if they are all the same ... // :TODO: the defualt encoding are really useless if they are all the same ...
// perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf // perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf
'.ans' : { name : 'ANSI', defaultEncoding : 'cp437', eof : 0x1a }, '.ans' : { name : 'ANSI', defaultEncoding : 'cp437', eof : 0x1a },
'.asc' : { name : 'ASCII', defaultEncoding : 'cp437', eof : 0x1a }, '.asc' : { name : 'ASCII', defaultEncoding : 'cp437', eof : 0x1a },
'.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a }, '.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a },
'.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a }, '.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a },
'.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a }, '.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a },
'.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a }, '.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a },
// :TODO: extentions for wwiv, renegade, celerity, syncronet, ... // :TODO: extentions for wwiv, renegade, celerity, syncronet, ...
// :TODO: extension for atari // :TODO: extension for atari
// :TODO: extension for topaz ansi/ascii. // :TODO: extension for topaz ansi/ascii.
}; };
function getFontNameFromSAUCE(sauce) { function getFontNameFromSAUCE(sauce) {
if(sauce.Character) { if(sauce.Character) {
return sauce.Character.fontName; return sauce.Character.fontName;
} }
} }
function sliceAtEOF(data, eofMarker) { function sliceAtEOF(data, eofMarker) {
let eof = data.length; let eof = data.length;
const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE)
for(let i = eof - 1; i > stopPos; i--) { for(let i = eof - 1; i > stopPos; i--) {
if(eofMarker === data[i]) { if(eofMarker === data[i]) {
eof = i; eof = i;
break; break;
} }
} }
return data.slice(0, eof); return data.slice(0, eof);
} }
function getArtFromPath(path, options, cb) { function getArtFromPath(path, options, cb) {
fs.readFile(path, (err, data) => { fs.readFile(path, (err, data) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
// //
// Convert from encodedAs -> j // Convert from encodedAs -> j
// //
const ext = paths.extname(path).toLowerCase(); const ext = paths.extname(path).toLowerCase();
const encoding = options.encodedAs || defaultEncodingFromExtension(ext); const encoding = options.encodedAs || defaultEncodingFromExtension(ext);
// :TODO: how are BOM's currently handled if present? Are they removed? Do we need to? // :TODO: how are BOM's currently handled if present? Are they removed? Do we need to?
function sliceOfData() { function sliceOfData() {
if(options.fullFile === true) { if(options.fullFile === true) {
return iconv.decode(data, encoding); return iconv.decode(data, encoding);
} else { } else {
const eofMarker = defaultEofFromExtension(ext); const eofMarker = defaultEofFromExtension(ext);
return iconv.decode(eofMarker ? sliceAtEOF(data, eofMarker) : data, encoding); return iconv.decode(eofMarker ? sliceAtEOF(data, eofMarker) : data, encoding);
} }
} }
function getResult(sauce) { function getResult(sauce) {
const result = { const result = {
data : sliceOfData(), data : sliceOfData(),
fromPath : path, fromPath : path,
}; };
if(sauce) { if(sauce) {
result.sauce = sauce; result.sauce = sauce;
} }
return result; return result;
} }
if(options.readSauce === true) { if(options.readSauce === true) {
sauce.readSAUCE(data, (err, sauce) => { sauce.readSAUCE(data, (err, sauce) => {
if(err) { if(err) {
return cb(null, getResult()); return cb(null, getResult());
} }
// //
// If a encoding was not provided & we have a mapping from // If a encoding was not provided & we have a mapping from
// the information provided by SAUCE, use that. // the information provided by SAUCE, use that.
// //
if(!options.encodedAs) { if(!options.encodedAs) {
/* /*
if(sauce.Character && sauce.Character.fontName) { if(sauce.Character && sauce.Character.fontName) {
var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName]; var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName];
if(enc) { if(enc) {
@ -114,115 +114,115 @@ function getArtFromPath(path, options, cb) {
} }
} }
*/ */
} }
return cb(null, getResult(sauce)); return cb(null, getResult(sauce));
}); });
} else { } else {
return cb(null, getResult()); return cb(null, getResult());
} }
}); });
} }
function getArt(name, options, cb) { function getArt(name, options, cb) {
const ext = paths.extname(name); const ext = paths.extname(name);
options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art); options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art);
options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true); options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true);
// :TODO: make use of asAnsi option and convert from supported -> ansi // :TODO: make use of asAnsi option and convert from supported -> ansi
if('' !== ext) { if('' !== ext) {
options.types = [ ext.toLowerCase() ]; options.types = [ ext.toLowerCase() ];
} else { } else {
if(_.isUndefined(options.types)) { if(_.isUndefined(options.types)) {
options.types = Object.keys(SUPPORTED_ART_TYPES); options.types = Object.keys(SUPPORTED_ART_TYPES);
} else if(_.isString(options.types)) { } else if(_.isString(options.types)) {
options.types = [ options.types.toLowerCase() ]; options.types = [ options.types.toLowerCase() ];
} }
} }
// If an extension is provided, just read the file now // If an extension is provided, just read the file now
if('' !== ext) { if('' !== ext) {
const directPath = paths.join(options.basePath, name); const directPath = paths.join(options.basePath, name);
return getArtFromPath(directPath, options, cb); return getArtFromPath(directPath, options, cb);
} }
fs.readdir(options.basePath, (err, files) => { fs.readdir(options.basePath, (err, files) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const filtered = files.filter( file => { const filtered = files.filter( file => {
// //
// Ignore anything not allowed in |options.types| // Ignore anything not allowed in |options.types|
// //
const fext = paths.extname(file); const fext = paths.extname(file);
if(!options.types.includes(fext.toLowerCase())) { if(!options.types.includes(fext.toLowerCase())) {
return false; return false;
} }
const bn = paths.basename(file, fext).toLowerCase(); const bn = paths.basename(file, fext).toLowerCase();
if(options.random) { if(options.random) {
const suppliedBn = paths.basename(name, fext).toLowerCase(); const suppliedBn = paths.basename(name, fext).toLowerCase();
// //
// Random selection enabled. We'll allow for // Random selection enabled. We'll allow for
// basename1.ext, basename2.ext, ... // basename1.ext, basename2.ext, ...
// //
if(!bn.startsWith(suppliedBn)) { if(!bn.startsWith(suppliedBn)) {
return false; return false;
} }
const num = bn.substr(suppliedBn.length); const num = bn.substr(suppliedBn.length);
if(num.length > 0) { if(num.length > 0) {
if(isNaN(parseInt(num, 10))) { if(isNaN(parseInt(num, 10))) {
return false; return false;
} }
} }
} else { } else {
// //
// We've already validated the extension (above). Must be an exact // We've already validated the extension (above). Must be an exact
// match to basename here // match to basename here
// //
if(bn != paths.basename(name, fext).toLowerCase()) { if(bn != paths.basename(name, fext).toLowerCase()) {
return false; return false;
} }
} }
return true; return true;
}); });
if(filtered.length > 0) { if(filtered.length > 0) {
// //
// We should now have: // We should now have:
// - Exactly (1) item in |filtered| if non-random // - Exactly (1) item in |filtered| if non-random
// - 1:n items in |filtered| to choose from if random // - 1:n items in |filtered| to choose from if random
// //
let readPath; let readPath;
if(options.random) { if(options.random) {
readPath = paths.join(options.basePath, filtered[Math.floor(Math.random() * filtered.length)]); readPath = paths.join(options.basePath, filtered[Math.floor(Math.random() * filtered.length)]);
} else { } else {
assert(1 === filtered.length); assert(1 === filtered.length);
readPath = paths.join(options.basePath, filtered[0]); readPath = paths.join(options.basePath, filtered[0]);
} }
return getArtFromPath(readPath, options, cb); return getArtFromPath(readPath, options, cb);
} }
return cb(new Error(`No matching art for supplied criteria: ${name}`)); return cb(new Error(`No matching art for supplied criteria: ${name}`));
}); });
} }
function defaultEncodingFromExtension(ext) { function defaultEncodingFromExtension(ext) {
const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()]; const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()];
return artType ? artType.defaultEncoding : 'utf8'; return artType ? artType.defaultEncoding : 'utf8';
} }
function defaultEofFromExtension(ext) { function defaultEofFromExtension(ext) {
const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()]; const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()];
if(artType) { if(artType) {
return artType.eof; return artType.eof;
} }
} }
// :TODO: Implement the following // :TODO: Implement the following
@ -230,161 +230,161 @@ function defaultEofFromExtension(ext) {
// * Cancel (disabled | <keys> ) // * Cancel (disabled | <keys> )
// * Resume from pause -> continous (disabled | <keys>) // * Resume from pause -> continous (disabled | <keys>)
function display(client, art, options, cb) { function display(client, art, options, cb) {
if(_.isFunction(options) && !cb) { if(_.isFunction(options) && !cb) {
cb = options; cb = options;
options = {}; options = {};
} }
if(!art || !art.length) { if(!art || !art.length) {
return cb(new Error('Empty art')); return cb(new Error('Empty art'));
} }
options.mciReplaceChar = options.mciReplaceChar || ' '; options.mciReplaceChar = options.mciReplaceChar || ' ';
options.disableMciCache = options.disableMciCache || false; options.disableMciCache = options.disableMciCache || false;
// :TODO: this is going to be broken into two approaches controlled via options: // :TODO: this is going to be broken into two approaches controlled via options:
// 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc. // 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc.
// 2) CPR driven // 2) CPR driven
if(!_.isBoolean(options.iceColors)) { if(!_.isBoolean(options.iceColors)) {
// try to detect from SAUCE // try to detect from SAUCE
if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) { if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) {
options.iceColors = true; options.iceColors = true;
} }
} }
const ansiParser = new aep.ANSIEscapeParser({ const ansiParser = new aep.ANSIEscapeParser({
mciReplaceChar : options.mciReplaceChar, mciReplaceChar : options.mciReplaceChar,
termHeight : client.term.termHeight, termHeight : client.term.termHeight,
termWidth : client.term.termWidth, termWidth : client.term.termWidth,
trailingLF : options.trailingLF, trailingLF : options.trailingLF,
}); });
let parseComplete = false; let parseComplete = false;
let cprListener; let cprListener;
let mciMap; let mciMap;
const mciCprQueue = []; const mciCprQueue = [];
let artHash; let artHash;
let mciMapFromCache; let mciMapFromCache;
function completed() { function completed() {
if(cprListener) { if(cprListener) {
client.removeListener('cursor position report', cprListener); client.removeListener('cursor position report', cprListener);
} }
if(!options.disableMciCache && !mciMapFromCache) { if(!options.disableMciCache && !mciMapFromCache) {
// cache our MCI findings... // cache our MCI findings...
client.mciCache[artHash] = mciMap; client.mciCache[artHash] = mciMap;
client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Added MCI map to cache'); client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Added MCI map to cache');
} }
ansiParser.removeAllListeners(); // :TODO: Necessary??? ansiParser.removeAllListeners(); // :TODO: Necessary???
const extraInfo = { const extraInfo = {
height : ansiParser.row - 1, height : ansiParser.row - 1,
}; };
return cb(null, mciMap, extraInfo); return cb(null, mciMap, extraInfo);
} }
if(!options.disableMciCache) { if(!options.disableMciCache) {
artHash = xxhash.hash(Buffer.from(art), 0xCAFEBABE); artHash = xxhash.hash(Buffer.from(art), 0xCAFEBABE);
// see if we have a mciMap cached for this art // see if we have a mciMap cached for this art
if(client.mciCache) { if(client.mciCache) {
mciMap = client.mciCache[artHash]; mciMap = client.mciCache[artHash];
} }
} }
if(mciMap) { if(mciMap) {
mciMapFromCache = true; mciMapFromCache = true;
client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Loaded MCI map from cache'); client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Loaded MCI map from cache');
} else { } else {
// no cached MCI info // no cached MCI info
mciMap = {}; mciMap = {};
cprListener = function(pos) { cprListener = function(pos) {
if(mciCprQueue.length > 0) { if(mciCprQueue.length > 0) {
mciMap[mciCprQueue.shift()].position = pos; mciMap[mciCprQueue.shift()].position = pos;
if(parseComplete && 0 === mciCprQueue.length) { if(parseComplete && 0 === mciCprQueue.length) {
return completed(); return completed();
} }
} }
}; };
client.on('cursor position report', cprListener); client.on('cursor position report', cprListener);
let generatedId = 100; let generatedId = 100;
ansiParser.on('mci', mciInfo => { ansiParser.on('mci', mciInfo => {
// :TODO: ensure generatedId's do not conflict with any existing |id| // :TODO: ensure generatedId's do not conflict with any existing |id|
const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId; const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId;
const mapKey = `${mciInfo.mci}${id}`; const mapKey = `${mciInfo.mci}${id}`;
const mapEntry = mciMap[mapKey]; const mapEntry = mciMap[mapKey];
if(mapEntry) { if(mapEntry) {
mapEntry.focusSGR = mciInfo.SGR; mapEntry.focusSGR = mciInfo.SGR;
mapEntry.focusArgs = mciInfo.args; mapEntry.focusArgs = mciInfo.args;
} else { } else {
mciMap[mapKey] = { mciMap[mapKey] = {
args : mciInfo.args, args : mciInfo.args,
SGR : mciInfo.SGR, SGR : mciInfo.SGR,
code : mciInfo.mci, code : mciInfo.mci,
id : id, id : id,
}; };
if(!mciInfo.id) { if(!mciInfo.id) {
++generatedId; ++generatedId;
} }
mciCprQueue.push(mapKey); mciCprQueue.push(mapKey);
client.term.rawWrite(ansi.queryPos()); client.term.rawWrite(ansi.queryPos());
} }
}); });
} }
ansiParser.on('literal', literal => client.term.write(literal, false) ); ansiParser.on('literal', literal => client.term.write(literal, false) );
ansiParser.on('control', control => client.term.rawWrite(control) ); ansiParser.on('control', control => client.term.rawWrite(control) );
ansiParser.on('complete', () => { ansiParser.on('complete', () => {
parseComplete = true; parseComplete = true;
if(0 === mciCprQueue.length) { if(0 === mciCprQueue.length) {
return completed(); return completed();
} }
}); });
let initSeq = ''; let initSeq = '';
if(options.font) { if(options.font) {
initSeq = ansi.setSyncTermFontWithAlias(options.font); initSeq = ansi.setSyncTermFontWithAlias(options.font);
} else if(options.sauce) { } else if(options.sauce) {
let fontName = getFontNameFromSAUCE(options.sauce); let fontName = getFontNameFromSAUCE(options.sauce);
if(fontName) { if(fontName) {
fontName = ansi.getSyncTERMFontFromAlias(fontName); fontName = ansi.getSyncTERMFontFromAlias(fontName);
} }
// //
// Set SyncTERM font if we're switching only. Most terminals // Set SyncTERM font if we're switching only. Most terminals
// that support this ESC sequence can only show *one* font // that support this ESC sequence can only show *one* font
// at a time. This applies to detection only (e.g. SAUCE). // at a time. This applies to detection only (e.g. SAUCE).
// If explicit, we'll set it no matter what (above) // If explicit, we'll set it no matter what (above)
// //
if(fontName && client.term.currentSyncFont != fontName) { if(fontName && client.term.currentSyncFont != fontName) {
client.term.currentSyncFont = fontName; client.term.currentSyncFont = fontName;
initSeq = ansi.setSyncTERMFont(fontName); initSeq = ansi.setSyncTERMFont(fontName);
} }
} }
if(options.iceColors) { if(options.iceColors) {
initSeq += ansi.blinkToBrightIntensity(); initSeq += ansi.blinkToBrightIntensity();
} }
if(initSeq) { if(initSeq) {
client.term.rawWrite(initSeq); client.term.rawWrite(initSeq);
} }
ansiParser.reset(art); ansiParser.reset(art);
return ansiParser.parse(); return ansiParser.parse();
} }

View File

@ -18,111 +18,111 @@ exports.resolveSystemStatAsset = resolveSystemStatAsset;
exports.getViewPropertyAsset = getViewPropertyAsset; exports.getViewPropertyAsset = getViewPropertyAsset;
const ALL_ASSETS = [ const ALL_ASSETS = [
'art', 'art',
'menu', 'menu',
'method', 'method',
'userModule', 'userModule',
'systemMethod', 'systemMethod',
'systemModule', 'systemModule',
'prompt', 'prompt',
'config', 'config',
'sysStat', 'sysStat',
]; ];
const 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) {
const m = ASSET_RE.exec(s); const m = ASSET_RE.exec(s);
if(m) { if(m) {
let result = { type : m[1] }; let result = { type : m[1] };
if(m[3]) { if(m[3]) {
result.location = m[2]; result.location = m[2];
result.asset = m[3]; result.asset = m[3];
} else { } else {
result.asset = m[2]; result.asset = m[2];
} }
return result; return result;
} }
} }
function getAssetWithShorthand(spec, defaultType) { function getAssetWithShorthand(spec, defaultType) {
if(!_.isString(spec)) { if(!_.isString(spec)) {
return null; return null;
} }
if('@' === spec[0]) { if('@' === spec[0]) {
const asset = parseAsset(spec); const asset = parseAsset(spec);
assert(_.isString(asset.type)); assert(_.isString(asset.type));
return asset; return asset;
} }
return { return {
type : defaultType, type : defaultType,
asset : spec, asset : spec,
}; };
} }
function getArtAsset(spec) { function getArtAsset(spec) {
const asset = getAssetWithShorthand(spec, 'art'); const asset = getAssetWithShorthand(spec, 'art');
if(!asset) { if(!asset) {
return null; return null;
} }
assert( ['art', 'method' ].indexOf(asset.type) > -1); assert( ['art', 'method' ].indexOf(asset.type) > -1);
return asset; return asset;
} }
function getModuleAsset(spec) { function getModuleAsset(spec) {
const asset = getAssetWithShorthand(spec, 'systemModule'); const asset = getAssetWithShorthand(spec, 'systemModule');
if(!asset) { if(!asset) {
return null; return null;
} }
assert( ['userModule', 'systemModule' ].includes(asset.type) ); assert( ['userModule', 'systemModule' ].includes(asset.type) );
return asset; return asset;
} }
function resolveConfigAsset(spec) { function resolveConfigAsset(spec) {
const asset = parseAsset(spec); const asset = parseAsset(spec);
if(asset) { if(asset) {
assert('config' === asset.type); assert('config' === asset.type);
const path = asset.asset.split('.'); const path = asset.asset.split('.');
let conf = Config(); let conf = Config();
for(let 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 spec; return spec;
} }
conf = conf[path[i]]; conf = conf[path[i]];
} }
return conf; return conf;
} else { } else {
return spec; return spec;
} }
} }
function resolveSystemStatAsset(spec) { function resolveSystemStatAsset(spec) {
const asset = parseAsset(spec); const asset = parseAsset(spec);
if(!asset) { if(!asset) {
return spec; return spec;
} }
assert('sysStat' === asset.type); assert('sysStat' === asset.type);
return StatLog.getSystemStat(asset.asset) || spec; return StatLog.getSystemStat(asset.asset) || spec;
} }
function getViewPropertyAsset(src) { function getViewPropertyAsset(src) {
if(!_.isString(src) || '@' !== src.charAt(0)) { if(!_.isString(src) || '@' !== src.charAt(0)) {
return null; return null;
} }
return parseAsset(src); return parseAsset(src);
} }

View File

@ -41,253 +41,253 @@ valid args:
`; `;
function printHelpAndExit() { function printHelpAndExit() {
console.info(HELP); console.info(HELP);
process.exit(); process.exit();
} }
function main() { function main() {
async.waterfall( async.waterfall(
[ [
function processArgs(callback) { function processArgs(callback) {
const argv = require('minimist')(process.argv.slice(2)); const argv = require('minimist')(process.argv.slice(2));
if(argv.help) { if(argv.help) {
printHelpAndExit(); printHelpAndExit();
} }
const configOverridePath = argv.config; const configOverridePath = argv.config;
return callback(null, configOverridePath || conf.getDefaultPath(), _.isString(configOverridePath)); return callback(null, configOverridePath || conf.getDefaultPath(), _.isString(configOverridePath));
}, },
function initConfig(configPath, configPathSupplied, callback) { function initConfig(configPath, configPathSupplied, callback) {
const configFile = configPath + 'config.hjson'; const configFile = configPath + 'config.hjson';
conf.init(resolvePath(configFile), function configInit(err) { conf.init(resolvePath(configFile), function configInit(err) {
// //
// If the user supplied a path and we can't read/parse it // If the user supplied a path and we can't read/parse it
// then it's a fatal error // then it's a fatal error
// //
if(err) { if(err) {
if('ENOENT' === err.code) { if('ENOENT' === err.code) {
if(configPathSupplied) { if(configPathSupplied) {
console.error('Configuration file does not exist: ' + configFile); console.error('Configuration file does not exist: ' + configFile);
} else { } else {
configPathSupplied = null; // make non-fatal; we'll go with defaults configPathSupplied = null; // make non-fatal; we'll go with defaults
} }
} else { } else {
console.error(err.toString()); console.error(err.toString());
} }
} }
callback(err); callback(err);
}); });
}, },
function initSystem(callback) { function initSystem(callback) {
initialize(function init(err) { initialize(function init(err) {
if(err) { if(err) {
console.error('Error initializing: ' + util.inspect(err)); console.error('Error initializing: ' + util.inspect(err));
} }
return callback(err); return callback(err);
}); });
} }
], ],
function complete(err) { function complete(err) {
// note this is escaped: // note this is escaped:
fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => { fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => {
console.info(FULL_COPYRIGHT); console.info(FULL_COPYRIGHT);
if(!err) { if(!err) {
console.info(banner); console.info(banner);
} }
console.info('System started!'); console.info('System started!');
}); });
if(err) { if(err) {
console.error('Error initializing: ' + util.inspect(err)); console.error('Error initializing: ' + util.inspect(err));
} }
} }
); );
} }
function shutdownSystem() { function shutdownSystem() {
const msg = 'Process interrupted. Shutting down...'; const msg = 'Process interrupted. Shutting down...';
console.info(msg); console.info(msg);
logger.log.info(msg); logger.log.info(msg);
async.series( async.series(
[ [
function closeConnections(callback) { function closeConnections(callback) {
const ClientConns = require('./client_connections.js'); const ClientConns = require('./client_connections.js');
const activeConnections = ClientConns.getActiveConnections(); const activeConnections = ClientConns.getActiveConnections();
let i = activeConnections.length; let i = activeConnections.length;
while(i--) { while(i--) {
const activeTerm = activeConnections[i].term; const activeTerm = activeConnections[i].term;
if(activeTerm) { if(activeTerm) {
activeTerm.write('\n\nServer is shutting down NOW! Disconnecting...\n\n'); activeTerm.write('\n\nServer is shutting down NOW! Disconnecting...\n\n');
} }
ClientConns.removeClient(activeConnections[i]); ClientConns.removeClient(activeConnections[i]);
} }
callback(null); callback(null);
}, },
function stopListeningServers(callback) { function stopListeningServers(callback) {
return require('./listening_server.js').shutdown( () => { return require('./listening_server.js').shutdown( () => {
return callback(null); // ignore err return callback(null); // ignore err
}); });
}, },
function stopEventScheduler(callback) { function stopEventScheduler(callback) {
if(initServices.eventScheduler) { if(initServices.eventScheduler) {
return initServices.eventScheduler.shutdown( () => { return initServices.eventScheduler.shutdown( () => {
return callback(null); // ignore err return callback(null); // ignore err
}); });
} else { } else {
return callback(null); return callback(null);
} }
}, },
function stopFileAreaWeb(callback) { function stopFileAreaWeb(callback) {
require('./file_area_web.js').startup( () => { require('./file_area_web.js').startup( () => {
return callback(null); // ignore err return callback(null); // ignore err
}); });
}, },
function stopMsgNetwork(callback) { function stopMsgNetwork(callback) {
require('./msg_network.js').shutdown(callback); require('./msg_network.js').shutdown(callback);
} }
], ],
() => { () => {
console.info('Goodbye!'); console.info('Goodbye!');
return process.exit(); return process.exit();
} }
); );
} }
function initialize(cb) { function initialize(cb) {
async.series( async.series(
[ [
function createMissingDirectories(callback) { function createMissingDirectories(callback) {
async.each(Object.keys(conf.config.paths), function entry(pathKey, next) { async.each(Object.keys(conf.config.paths), function entry(pathKey, next) {
mkdirs(conf.config.paths[pathKey], function dirCreated(err) { mkdirs(conf.config.paths[pathKey], function dirCreated(err) {
if(err) { if(err) {
console.error('Could not create path: ' + conf.config.paths[pathKey] + ': ' + err.toString()); console.error('Could not create path: ' + conf.config.paths[pathKey] + ': ' + err.toString());
} }
return next(err); return next(err);
}); });
}, function dirCreationComplete(err) { }, function dirCreationComplete(err) {
return callback(err); return callback(err);
}); });
}, },
function basicInit(callback) { function basicInit(callback) {
logger.init(); logger.init();
logger.log.info( logger.log.info(
{ version : require('../package.json').version }, { version : require('../package.json').version },
'**** ENiGMA½ Bulletin Board System Starting Up! ****'); '**** ENiGMA½ Bulletin Board System Starting Up! ****');
process.on('SIGINT', shutdownSystem); process.on('SIGINT', shutdownSystem);
require('later').date.localTime(); // use local times for later.js/scheduling require('later').date.localTime(); // use local times for later.js/scheduling
return callback(null); return callback(null);
}, },
function initDatabases(callback) { function initDatabases(callback) {
return database.initializeDatabases(callback); return database.initializeDatabases(callback);
}, },
function initMimeTypes(callback) { function initMimeTypes(callback) {
return require('./mime_util.js').startup(callback); return require('./mime_util.js').startup(callback);
}, },
function initStatLog(callback) { function initStatLog(callback) {
return require('./stat_log.js').init(callback); return require('./stat_log.js').init(callback);
}, },
function initConfigs(callback) { function initConfigs(callback) {
return require('./config_util.js').init(callback); return require('./config_util.js').init(callback);
}, },
function initThemes(callback) { function initThemes(callback) {
// Have to pull in here so it's after Config init // Have to pull in here so it's after Config init
require('./theme.js').initAvailableThemes( (err, themeCount) => { require('./theme.js').initAvailableThemes( (err, themeCount) => {
logger.log.info({ themeCount }, 'Themes initialized'); logger.log.info({ themeCount }, 'Themes initialized');
return callback(err); return callback(err);
}); });
}, },
function loadSysOpInformation(callback) { function loadSysOpInformation(callback) {
// //
// Copy over some +op information from the user DB -> system propertys. // Copy over some +op information from the user DB -> system propertys.
// * Makes this accessible for MCI codes, easy non-blocking access, etc. // * Makes this accessible for MCI codes, easy non-blocking access, etc.
// * We do this every time as the op is free to change this information just // * We do this every time as the op is free to change this information just
// like any other user // like any other user
// //
const User = require('./user.js'); const User = require('./user.js');
async.waterfall( async.waterfall(
[ [
function getOpUserName(next) { function getOpUserName(next) {
return User.getUserName(1, next); return User.getUserName(1, next);
}, },
function getOpProps(opUserName, next) { function getOpProps(opUserName, next) {
const propLoadOpts = { const propLoadOpts = {
names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ], names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ],
}; };
User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => { User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => {
return next(err, opUserName, opProps); return next(err, opUserName, opProps);
}); });
} }
], ],
(err, opUserName, opProps) => { (err, opUserName, opProps) => {
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
if(err) { if(err) {
[ 'username', 'real_name', 'sex', 'email_address', 'location', 'affiliation' ].forEach(v => { [ 'username', 'real_name', 'sex', 'email_address', 'location', 'affiliation' ].forEach(v => {
StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A'); StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A');
}); });
} else { } else {
opProps.username = opUserName; opProps.username = opUserName;
_.each(opProps, (v, k) => { _.each(opProps, (v, k) => {
StatLog.setNonPeristentSystemStat(`sysop_${k}`, v); StatLog.setNonPeristentSystemStat(`sysop_${k}`, v);
}); });
} }
return callback(null); return callback(null);
} }
); );
}, },
function initFileAreaStats(callback) { function initFileAreaStats(callback) {
const getAreaStats = require('./file_base_area.js').getAreaStats; const getAreaStats = require('./file_base_area.js').getAreaStats;
getAreaStats( (err, stats) => { getAreaStats( (err, stats) => {
if(!err) { if(!err) {
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); StatLog.setNonPeristentSystemStat('file_base_area_stats', stats);
} }
return callback(null); return callback(null);
}); });
}, },
function initMCI(callback) { function initMCI(callback) {
return require('./predefined_mci.js').init(callback); return require('./predefined_mci.js').init(callback);
}, },
function readyMessageNetworkSupport(callback) { function readyMessageNetworkSupport(callback) {
return require('./msg_network.js').startup(callback); return require('./msg_network.js').startup(callback);
}, },
function readyEvents(callback) { function readyEvents(callback) {
return require('./events.js').startup(callback); return require('./events.js').startup(callback);
}, },
function listenConnections(callback) { function listenConnections(callback) {
return require('./listening_server.js').startup(callback); return require('./listening_server.js').startup(callback);
}, },
function readyFileBaseArea(callback) { function readyFileBaseArea(callback) {
return require('./file_base_area.js').startup(callback); return require('./file_base_area.js').startup(callback);
}, },
function readyFileAreaWeb(callback) { function readyFileAreaWeb(callback) {
return require('./file_area_web.js').startup(callback); return require('./file_area_web.js').startup(callback);
}, },
function readyPasswordReset(callback) { function readyPasswordReset(callback) {
const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset; const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset;
return WebPasswordReset.startup(callback); return WebPasswordReset.startup(callback);
}, },
function readyEventScheduler(callback) { function readyEventScheduler(callback) {
const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule; const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule;
EventSchedulerModule.loadAndStart( (err, modInst) => { EventSchedulerModule.loadAndStart( (err, modInst) => {
initServices.eventScheduler = modInst; initServices.eventScheduler = modInst;
return callback(err); return callback(err);
}); });
} }
], ],
function onComplete(err) { function onComplete(err) {
return cb(err); return cb(err);
} }
); );
} }

View File

@ -37,171 +37,171 @@ const packageJson = require('../package.json');
// :TODO: ENH: Support nodeMax and tooManyArt // :TODO: ENH: Support nodeMax and tooManyArt
exports.moduleInfo = { exports.moduleInfo = {
name : 'BBSLink', name : 'BBSLink',
desc : 'BBSLink Access Module', desc : 'BBSLink Access Module',
author : 'NuSkooler', author : 'NuSkooler',
}; };
exports.getModule = class BBSLinkModule extends MenuModule { exports.getModule = class BBSLinkModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
this.config.host = this.config.host || 'games.bbslink.net'; this.config.host = this.config.host || 'games.bbslink.net';
this.config.port = this.config.port || 23; this.config.port = this.config.port || 23;
} }
initSequence() { initSequence() {
let token; let token;
let randomKey; let randomKey;
let clientTerminated; let clientTerminated;
const self = this; const self = this;
async.series( async.series(
[ [
function validateConfig(callback) { function validateConfig(callback) {
if(_.isString(self.config.sysCode) && if(_.isString(self.config.sysCode) &&
_.isString(self.config.authCode) && _.isString(self.config.authCode) &&
_.isString(self.config.schemeCode) && _.isString(self.config.schemeCode) &&
_.isString(self.config.door)) _.isString(self.config.door))
{ {
callback(null); callback(null);
} else { } else {
callback(new Error('Configuration is missing option(s)')); callback(new Error('Configuration is missing option(s)'));
} }
}, },
function acquireToken(callback) { function acquireToken(callback) {
// //
// Acquire an authentication token // Acquire an authentication token
// //
crypto.randomBytes(16, function rand(ex, buf) { crypto.randomBytes(16, function rand(ex, buf) {
if(ex) { if(ex) {
callback(ex); callback(ex);
} else { } else {
randomKey = buf.toString('base64').substr(0, 6); randomKey = buf.toString('base64').substr(0, 6);
self.simpleHttpRequest('/token.php?key=' + randomKey, null, function resp(err, body) { self.simpleHttpRequest('/token.php?key=' + randomKey, null, function resp(err, body) {
if(err) { if(err) {
callback(err); callback(err);
} else { } else {
token = body.trim(); token = body.trim();
self.client.log.trace( { token : token }, 'BBSLink token'); self.client.log.trace( { token : token }, 'BBSLink token');
callback(null); callback(null);
} }
}); });
} }
}); });
}, },
function authenticateToken(callback) { function authenticateToken(callback) {
// //
// Authenticate the token we acquired previously // Authenticate the token we acquired previously
// //
var headers = { var headers = {
'X-User' : self.client.user.userId.toString(), 'X-User' : self.client.user.userId.toString(),
'X-System' : self.config.sysCode, 'X-System' : self.config.sysCode,
'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'), 'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'),
'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'), 'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'),
'X-Rows' : self.client.term.termHeight.toString(), 'X-Rows' : self.client.term.termHeight.toString(),
'X-Key' : randomKey, 'X-Key' : randomKey,
'X-Door' : self.config.door, 'X-Door' : self.config.door,
'X-Token' : token, 'X-Token' : token,
'X-Type' : 'enigma-bbs', 'X-Type' : 'enigma-bbs',
'X-Version' : packageJson.version, 'X-Version' : packageJson.version,
}; };
self.simpleHttpRequest('/auth.php?key=' + randomKey, headers, function resp(err, body) { self.simpleHttpRequest('/auth.php?key=' + randomKey, headers, function resp(err, body) {
var status = body.trim(); var status = body.trim();
if('complete' === status) { if('complete' === status) {
callback(null); callback(null);
} else { } else {
callback(new Error('Bad authentication status: ' + status)); callback(new Error('Bad authentication status: ' + status));
} }
}); });
}, },
function createTelnetBridge(callback) { function createTelnetBridge(callback) {
// //
// Authentication with BBSLink successful. Now, we need to create a telnet // Authentication with BBSLink successful. Now, we need to create a telnet
// bridge from us to them // bridge from us to them
// //
var connectOpts = { var connectOpts = {
port : self.config.port, port : self.config.port,
host : self.config.host, host : self.config.host,
}; };
var clientTerminated; var clientTerminated;
self.client.term.write(resetScreen()); self.client.term.write(resetScreen());
self.client.term.write(' Connecting to BBSLink.net, please wait...\n'); self.client.term.write(' Connecting to BBSLink.net, please wait...\n');
var bridgeConnection = net.createConnection(connectOpts, function connected() { var bridgeConnection = net.createConnection(connectOpts, function connected() {
self.client.log.info(connectOpts, 'BBSLink bridge connection established'); self.client.log.info(connectOpts, 'BBSLink bridge connection established');
self.client.term.output.pipe(bridgeConnection); self.client.term.output.pipe(bridgeConnection);
self.client.once('end', function clientEnd() { self.client.once('end', function clientEnd() {
self.client.log.info('Connection ended. Terminating BBSLink connection'); self.client.log.info('Connection ended. Terminating BBSLink connection');
clientTerminated = true; clientTerminated = true;
bridgeConnection.end(); bridgeConnection.end();
}); });
}); });
var restorePipe = function() { var restorePipe = function() {
self.client.term.output.unpipe(bridgeConnection); self.client.term.output.unpipe(bridgeConnection);
self.client.term.output.resume(); self.client.term.output.resume();
}; };
bridgeConnection.on('data', function incomingData(data) { bridgeConnection.on('data', function incomingData(data) {
// pass along // pass along
// :TODO: just pipe this as well // :TODO: just pipe this as well
self.client.term.rawWrite(data); self.client.term.rawWrite(data);
}); });
bridgeConnection.on('end', function connectionEnd() { bridgeConnection.on('end', function connectionEnd() {
restorePipe(); restorePipe();
callback(clientTerminated ? new Error('Client connection terminated') : null); callback(clientTerminated ? new Error('Client connection terminated') : null);
}); });
bridgeConnection.on('error', function error(err) { bridgeConnection.on('error', function error(err) {
self.client.log.info('BBSLink bridge connection error: ' + err.message); self.client.log.info('BBSLink bridge connection error: ' + err.message);
restorePipe(); restorePipe();
callback(err); callback(err);
}); });
} }
], ],
function complete(err) { function complete(err) {
if(err) { if(err) {
self.client.log.warn( { error : err.toString() }, 'BBSLink connection error'); self.client.log.warn( { error : err.toString() }, 'BBSLink connection error');
} }
if(!clientTerminated) { if(!clientTerminated) {
self.prevMenu(); self.prevMenu();
} }
} }
); );
} }
simpleHttpRequest(path, headers, cb) { simpleHttpRequest(path, headers, cb) {
const getOpts = { const getOpts = {
host : this.config.host, host : this.config.host,
path : path, path : path,
headers : headers, headers : headers,
}; };
const req = http.get(getOpts, function response(resp) { const req = http.get(getOpts, function response(resp) {
let data = ''; let data = '';
resp.on('data', function chunk(c) { resp.on('data', function chunk(c) {
data += c; data += c;
}); });
resp.on('end', function respEnd() { resp.on('end', function respEnd() {
cb(null, data); cb(null, data);
req.end(); req.end();
}); });
}); });
req.on('error', function reqErr(err) { req.on('error', function reqErr(err) {
cb(err); cb(err);
}); });
} }
}; };

View File

@ -5,8 +5,8 @@
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const { const {
getModDatabasePath, getModDatabasePath,
getTransactionDatabase getTransactionDatabase
} = require('./database.js'); } = require('./database.js');
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
@ -23,397 +23,397 @@ const _ = require('lodash');
// :TODO: add notes field // :TODO: add notes field
const moduleInfo = exports.moduleInfo = { const moduleInfo = exports.moduleInfo = {
name : 'BBS List', name : 'BBS List',
desc : 'List of other BBSes', desc : 'List of other BBSes',
author : 'Andrew Pamment', author : 'Andrew Pamment',
packageName : 'com.magickabbs.enigma.bbslist' packageName : 'com.magickabbs.enigma.bbslist'
}; };
const MciViewIds = { const MciViewIds = {
view : { view : {
BBSList : 1, BBSList : 1,
SelectedBBSName : 2, SelectedBBSName : 2,
SelectedBBSSysOp : 3, SelectedBBSSysOp : 3,
SelectedBBSTelnet : 4, SelectedBBSTelnet : 4,
SelectedBBSWww : 5, SelectedBBSWww : 5,
SelectedBBSLoc : 6, SelectedBBSLoc : 6,
SelectedBBSSoftware : 7, SelectedBBSSoftware : 7,
SelectedBBSNotes : 8, SelectedBBSNotes : 8,
SelectedBBSSubmitter : 9, SelectedBBSSubmitter : 9,
}, },
add : { add : {
BBSName : 1, BBSName : 1,
Sysop : 2, Sysop : 2,
Telnet : 3, Telnet : 3,
Www : 4, Www : 4,
Location : 5, Location : 5,
Software : 6, Software : 6,
Notes : 7, Notes : 7,
Error : 8, Error : 8,
} }
}; };
const FormIds = { const FormIds = {
View : 0, View : 0,
Add : 1, Add : 1,
}; };
const SELECTED_MCI_NAME_TO_ENTRY = { const SELECTED_MCI_NAME_TO_ENTRY = {
SelectedBBSName : 'bbsName', SelectedBBSName : 'bbsName',
SelectedBBSSysOp : 'sysOp', SelectedBBSSysOp : 'sysOp',
SelectedBBSTelnet : 'telnet', SelectedBBSTelnet : 'telnet',
SelectedBBSWww : 'www', SelectedBBSWww : 'www',
SelectedBBSLoc : 'location', SelectedBBSLoc : 'location',
SelectedBBSSoftware : 'software', SelectedBBSSoftware : 'software',
SelectedBBSSubmitter : 'submitter', SelectedBBSSubmitter : 'submitter',
SelectedBBSSubmitterId : 'submitterUserId', SelectedBBSSubmitterId : 'submitterUserId',
SelectedBBSNotes : 'notes', SelectedBBSNotes : 'notes',
}; };
exports.getModule = class BBSListModule extends MenuModule { exports.getModule = class BBSListModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
const self = this; const self = this;
this.menuMethods = { this.menuMethods = {
// //
// Validators // Validators
// //
viewValidationListener : function(err, cb) { viewValidationListener : function(err, cb) {
const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error); const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error);
if(errMsgView) { if(errMsgView) {
if(err) { if(err) {
errMsgView.setText(err.message); errMsgView.setText(err.message);
} else { } else {
errMsgView.clearText(); errMsgView.clearText();
} }
} }
return cb(null); return cb(null);
}, },
// //
// Key & submit handlers // Key & submit handlers
// //
addBBS : function(formData, extraArgs, cb) { addBBS : function(formData, extraArgs, cb) {
self.displayAddScreen(cb); self.displayAddScreen(cb);
}, },
deleteBBS : function(formData, extraArgs, cb) { deleteBBS : function(formData, extraArgs, cb) {
if(!_.isNumber(self.selectedBBS) || 0 === self.entries.length) { if(!_.isNumber(self.selectedBBS) || 0 === self.entries.length) {
return cb(null); return cb(null);
} }
const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList);
if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) { if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) {
// must be owner or +op // must be owner or +op
return cb(null); return cb(null);
} }
const entry = self.entries[self.selectedBBS]; const entry = self.entries[self.selectedBBS];
if(!entry) { if(!entry) {
return cb(null); return cb(null);
} }
self.database.run( self.database.run(
`DELETE FROM bbs_list `DELETE FROM bbs_list
WHERE id=?;`, WHERE id=?;`,
[ entry.id ], [ entry.id ],
err => { err => {
if (err) { if (err) {
self.client.log.error( { err : err }, 'Error deleting from BBS list'); self.client.log.error( { err : err }, 'Error deleting from BBS list');
} else { } else {
self.entries.splice(self.selectedBBS, 1); self.entries.splice(self.selectedBBS, 1);
self.setEntries(entriesView); self.setEntries(entriesView);
if(self.entries.length > 0) { if(self.entries.length > 0) {
entriesView.focusPrevious(); entriesView.focusPrevious();
} }
self.viewControllers.view.redrawAll(); self.viewControllers.view.redrawAll();
} }
return cb(null); return cb(null);
} }
); );
}, },
submitBBS : function(formData, extraArgs, cb) { submitBBS : function(formData, extraArgs, cb) {
let ok = true; let ok = true;
[ 'BBSName', 'Sysop', 'Telnet' ].forEach( mciName => { [ 'BBSName', 'Sysop', 'Telnet' ].forEach( mciName => {
if('' === self.viewControllers.add.getView(MciViewIds.add[mciName]).getData()) { if('' === self.viewControllers.add.getView(MciViewIds.add[mciName]).getData()) {
ok = false; ok = false;
} }
}); });
if(!ok) { if(!ok) {
// validators should prevent this! // validators should prevent this!
return cb(null); return cb(null);
} }
self.database.run( self.database.run(
`INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes) `INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes)
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
[ [
formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www,
formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes
], ],
err => { err => {
if(err) { if(err) {
self.client.log.error( { err : err }, 'Error adding to BBS list'); self.client.log.error( { err : err }, 'Error adding to BBS list');
} }
self.clearAddForm(); self.clearAddForm();
self.displayBBSList(true, cb); self.displayBBSList(true, cb);
} }
); );
}, },
cancelSubmit : function(formData, extraArgs, cb) { cancelSubmit : function(formData, extraArgs, cb) {
self.clearAddForm(); self.clearAddForm();
self.displayBBSList(true, cb); self.displayBBSList(true, cb);
} }
}; };
} }
initSequence() { initSequence() {
const self = this; const self = this;
async.series( async.series(
[ [
function beforeDisplayArt(callback) { function beforeDisplayArt(callback) {
self.beforeArt(callback); self.beforeArt(callback);
}, },
function display(callback) { function display(callback) {
self.displayBBSList(false, callback); self.displayBBSList(false, callback);
} }
], ],
err => { err => {
if(err) { if(err) {
// :TODO: Handle me -- initSequence() should really take a completion callback // :TODO: Handle me -- initSequence() should really take a completion callback
} }
self.finishedLoading(); self.finishedLoading();
} }
); );
} }
drawSelectedEntry(entry) { drawSelectedEntry(entry) {
if(!entry) { if(!entry) {
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
this.setViewText('view', MciViewIds.view[mciName], ''); this.setViewText('view', MciViewIds.view[mciName], '');
}); });
} else { } else {
const youSubmittedFormat = this.menuConfig.youSubmittedFormat || '{submitter} (You!)'; const youSubmittedFormat = this.menuConfig.youSubmittedFormat || '{submitter} (You!)';
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]]; const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]];
if(MciViewIds.view[mciName]) { if(MciViewIds.view[mciName]) {
if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == this.client.user.userId) { if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == this.client.user.userId) {
this.setViewText('view',MciViewIds.view.SelectedBBSSubmitter, stringFormat(youSubmittedFormat, entry)); this.setViewText('view',MciViewIds.view.SelectedBBSSubmitter, stringFormat(youSubmittedFormat, entry));
} else { } else {
this.setViewText('view',MciViewIds.view[mciName], t); this.setViewText('view',MciViewIds.view[mciName], t);
} }
} }
}); });
} }
} }
setEntries(entriesView) { setEntries(entriesView) {
const config = this.menuConfig.config; const config = this.menuConfig.config;
const listFormat = config.listFormat || '{bbsName}'; const listFormat = config.listFormat || '{bbsName}';
const focusListFormat = config.focusListFormat || '{bbsName}'; const focusListFormat = config.focusListFormat || '{bbsName}';
entriesView.setItems(this.entries.map( e => stringFormat(listFormat, e) ) ); entriesView.setItems(this.entries.map( e => stringFormat(listFormat, e) ) );
entriesView.setFocusItems(this.entries.map( e => stringFormat(focusListFormat, e) ) ); entriesView.setFocusItems(this.entries.map( e => stringFormat(focusListFormat, e) ) );
} }
displayBBSList(clearScreen, cb) { displayBBSList(clearScreen, cb) {
const self = this; const self = this;
async.waterfall( async.waterfall(
[ [
function clearAndDisplayArt(callback) { function clearAndDisplayArt(callback) {
if(self.viewControllers.add) { if(self.viewControllers.add) {
self.viewControllers.add.setFocus(false); self.viewControllers.add.setFocus(false);
} }
if (clearScreen) { if (clearScreen) {
self.client.term.rawWrite(ansi.resetScreen()); self.client.term.rawWrite(ansi.resetScreen());
} }
theme.displayThemedAsset( theme.displayThemedAsset(
self.menuConfig.config.art.entries, self.menuConfig.config.art.entries,
self.client, self.client,
{ font : self.menuConfig.font, trailingLF : false }, { font : self.menuConfig.font, trailingLF : false },
(err, artData) => { (err, artData) => {
return callback(err, artData); return callback(err, artData);
} }
); );
}, },
function initOrRedrawViewController(artData, callback) { function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) { if(_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController( const vc = self.addViewController(
'view', 'view',
new ViewController( { client : self.client, formId : FormIds.View } ) new ViewController( { client : self.client, formId : FormIds.View } )
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds.View, formId : FormIds.View,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
} else { } else {
self.viewControllers.view.setFocus(true); self.viewControllers.view.setFocus(true);
self.viewControllers.view.getView(MciViewIds.view.BBSList).redraw(); self.viewControllers.view.getView(MciViewIds.view.BBSList).redraw();
return callback(null); return callback(null);
} }
}, },
function fetchEntries(callback) { function fetchEntries(callback) {
const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList);
self.entries = []; self.entries = [];
self.database.each( self.database.each(
`SELECT id, bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes `SELECT id, bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes
FROM bbs_list;`, FROM bbs_list;`,
(err, row) => { (err, row) => {
if (!err) { if (!err) {
self.entries.push({ self.entries.push({
id : row.id, id : row.id,
bbsName : row.bbs_name, bbsName : row.bbs_name,
sysOp : row.sysop, sysOp : row.sysop,
telnet : row.telnet, telnet : row.telnet,
www : row.www, www : row.www,
location : row.location, location : row.location,
software : row.software, software : row.software,
submitterUserId : row.submitter_user_id, submitterUserId : row.submitter_user_id,
notes : row.notes, notes : row.notes,
}); });
} }
}, },
err => { err => {
return callback(err, entriesView); return callback(err, entriesView);
} }
); );
}, },
function getUserNames(entriesView, callback) { function getUserNames(entriesView, callback) {
async.each(self.entries, (entry, next) => { async.each(self.entries, (entry, next) => {
User.getUserName(entry.submitterUserId, (err, username) => { User.getUserName(entry.submitterUserId, (err, username) => {
if(username) { if(username) {
entry.submitter = username; entry.submitter = username;
} else { } else {
entry.submitter = 'N/A'; entry.submitter = 'N/A';
} }
return next(); return next();
}); });
}, () => { }, () => {
return callback(null, entriesView); return callback(null, entriesView);
}); });
}, },
function populateEntries(entriesView, callback) { function populateEntries(entriesView, callback) {
self.setEntries(entriesView); self.setEntries(entriesView);
entriesView.on('index update', idx => { entriesView.on('index update', idx => {
const entry = self.entries[idx]; const entry = self.entries[idx];
self.drawSelectedEntry(entry); self.drawSelectedEntry(entry);
if(!entry) { if(!entry) {
self.selectedBBS = -1; self.selectedBBS = -1;
} else { } else {
self.selectedBBS = idx; self.selectedBBS = idx;
} }
}); });
if (self.selectedBBS >= 0) { if (self.selectedBBS >= 0) {
entriesView.setFocusItemIndex(self.selectedBBS); entriesView.setFocusItemIndex(self.selectedBBS);
self.drawSelectedEntry(self.entries[self.selectedBBS]); self.drawSelectedEntry(self.entries[self.selectedBBS]);
} else if (self.entries.length > 0) { } else if (self.entries.length > 0) {
self.selectedBBS = 0; self.selectedBBS = 0;
entriesView.setFocusItemIndex(0); entriesView.setFocusItemIndex(0);
self.drawSelectedEntry(self.entries[0]); self.drawSelectedEntry(self.entries[0]);
} }
entriesView.redraw(); entriesView.redraw();
return callback(null); return callback(null);
} }
], ],
err => { err => {
if(cb) { if(cb) {
return cb(err); return cb(err);
} }
} }
); );
} }
displayAddScreen(cb) { displayAddScreen(cb) {
const self = this; const self = this;
async.waterfall( async.waterfall(
[ [
function clearAndDisplayArt(callback) { function clearAndDisplayArt(callback) {
self.viewControllers.view.setFocus(false); self.viewControllers.view.setFocus(false);
self.client.term.rawWrite(ansi.resetScreen()); self.client.term.rawWrite(ansi.resetScreen());
theme.displayThemedAsset( theme.displayThemedAsset(
self.menuConfig.config.art.add, self.menuConfig.config.art.add,
self.client, self.client,
{ font : self.menuConfig.font }, { font : self.menuConfig.font },
(err, artData) => { (err, artData) => {
return callback(err, artData); return callback(err, artData);
} }
); );
}, },
function initOrRedrawViewController(artData, callback) { function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) { if(_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController( const vc = self.addViewController(
'add', 'add',
new ViewController( { client : self.client, formId : FormIds.Add } ) new ViewController( { client : self.client, formId : FormIds.Add } )
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds.Add, formId : FormIds.Add,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
} else { } else {
self.viewControllers.add.setFocus(true); self.viewControllers.add.setFocus(true);
self.viewControllers.add.redrawAll(); self.viewControllers.add.redrawAll();
self.viewControllers.add.switchFocus(MciViewIds.add.BBSName); self.viewControllers.add.switchFocus(MciViewIds.add.BBSName);
return callback(null); return callback(null);
} }
} }
], ],
err => { err => {
if(cb) { if(cb) {
return cb(err); return cb(err);
} }
} }
); );
} }
clearAddForm() { clearAddForm() {
[ 'BBSName', 'Sysop', 'Telnet', 'Www', 'Location', 'Software', 'Error', 'Notes' ].forEach( mciName => { [ 'BBSName', 'Sysop', 'Telnet', 'Www', 'Location', 'Software', 'Error', 'Notes' ].forEach( mciName => {
this.setViewText('add', MciViewIds.add[mciName], ''); this.setViewText('add', MciViewIds.add[mciName], '');
}); });
} }
initDatabase(cb) { initDatabase(cb) {
const self = this; const self = this;
async.series( async.series(
[ [
function openDatabase(callback) { function openDatabase(callback) {
self.database = getTransactionDatabase(new sqlite3.Database( self.database = getTransactionDatabase(new sqlite3.Database(
getModDatabasePath(moduleInfo), getModDatabasePath(moduleInfo),
callback callback
)); ));
}, },
function createTables(callback) { function createTables(callback) {
self.database.serialize( () => { self.database.serialize( () => {
self.database.run( self.database.run(
`CREATE TABLE IF NOT EXISTS bbs_list ( `CREATE TABLE IF NOT EXISTS bbs_list (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
bbs_name VARCHAR NOT NULL, bbs_name VARCHAR NOT NULL,
sysop VARCHAR NOT NULL, sysop VARCHAR NOT NULL,
@ -424,20 +424,20 @@ exports.getModule = class BBSListModule extends MenuModule {
submitter_user_id INTEGER NOT NULL, submitter_user_id INTEGER NOT NULL,
notes VARCHAR notes VARCHAR
);` );`
); );
}); });
callback(null); callback(null);
} }
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
} }
beforeArt(cb) { beforeArt(cb) {
super.beforeArt(err => { super.beforeArt(err => {
return err ? cb(err) : this.initDatabase(cb); return err ? cb(err) : this.initDatabase(cb);
}); });
} }
}; };

View File

@ -8,24 +8,24 @@ const util = require('util');
exports.ButtonView = ButtonView; exports.ButtonView = ButtonView;
function ButtonView(options) { function ButtonView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.justify = miscUtil.valueWithDefault(options.justify, 'center'); options.justify = miscUtil.valueWithDefault(options.justify, 'center');
options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide'); options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide');
TextView.call(this, options); TextView.call(this, options);
} }
util.inherits(ButtonView, TextView); util.inherits(ButtonView, TextView);
ButtonView.prototype.onKeyPress = function(ch, key) { ButtonView.prototype.onKeyPress = function(ch, key) {
if(this.isKeyMapped('accept', key.name) || ' ' === ch) { if(this.isKeyMapped('accept', key.name) || ' ' === ch) {
this.submitData = 'accept'; this.submitData = 'accept';
this.emit('action', 'accept'); this.emit('action', 'accept');
delete this.submitData; delete this.submitData;
} else { } else {
ButtonView.super_.prototype.onKeyPress.call(this, ch, key); ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
} }
}; };
/* /*
ButtonView.prototype.onKeyPress = function(ch, key) { ButtonView.prototype.onKeyPress = function(ch, key) {
@ -39,5 +39,5 @@ ButtonView.prototype.onKeyPress = function(ch, key) {
*/ */
ButtonView.prototype.getData = function() { ButtonView.prototype.getData = function() {
return this.submitData || null; return this.submitData || null;
}; };

View File

@ -58,442 +58,442 @@ const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/;
const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/; const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/;
const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$'); const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$');
const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [ const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [
'(\\d+)(?:;(\\d+))?([~^$])', '(\\d+)(?:;(\\d+))?([~^$])',
'(?:M([@ #!a`])(.)(.))', // mouse stuff '(?:M([@ #!a`])(.)(.))', // mouse stuff
'(?:1;)?(\\d+)?([a-zA-Z@])' '(?:1;)?(\\d+)?([a-zA-Z@])'
].join('|') + ')'); ].join('|') + ')');
const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source); const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source);
const RE_ESC_CODE_ANYWHERE = new RegExp( [ const RE_ESC_CODE_ANYWHERE = new RegExp( [
RE_FUNCTION_KEYCODE_ANYWHERE.source, RE_FUNCTION_KEYCODE_ANYWHERE.source,
RE_META_KEYCODE_ANYWHERE.source, RE_META_KEYCODE_ANYWHERE.source,
RE_DSR_RESPONSE_ANYWHERE.source, RE_DSR_RESPONSE_ANYWHERE.source,
RE_DEV_ATTR_RESPONSE_ANYWHERE.source, RE_DEV_ATTR_RESPONSE_ANYWHERE.source,
/\u001b./.source /\u001b./.source
].join('|')); ].join('|'));
function Client(/*input, output*/) { function Client(/*input, output*/) {
stream.call(this); stream.call(this);
const self = this; const self = this;
this.user = new User(); this.user = new User();
this.currentTheme = { info : { name : 'N/A', description : 'None' } }; this.currentTheme = { info : { name : 'N/A', description : 'None' } };
this.lastKeyPressMs = Date.now(); this.lastKeyPressMs = Date.now();
this.menuStack = new MenuStack(this); this.menuStack = new MenuStack(this);
this.acs = new ACS(this); this.acs = new ACS(this);
this.mciCache = {}; this.mciCache = {};
this.clearMciCache = function() { this.clearMciCache = function() {
this.mciCache = {}; this.mciCache = {};
}; };
Object.defineProperty(this, 'node', { Object.defineProperty(this, 'node', {
get : function() { get : function() {
return self.session.id + 1; return self.session.id + 1;
} }
}); });
Object.defineProperty(this, 'currentMenuModule', { Object.defineProperty(this, 'currentMenuModule', {
get : function() { get : function() {
return self.menuStack.currentModule; return self.menuStack.currentModule;
} }
}); });
this.setTemporaryDirectDataHandler = function(handler) { this.setTemporaryDirectDataHandler = function(handler) {
this.input.removeAllListeners('data'); this.input.removeAllListeners('data');
this.input.on('data', handler); this.input.on('data', handler);
}; };
this.restoreDataHandler = function() { this.restoreDataHandler = function() {
this.input.removeAllListeners('data'); this.input.removeAllListeners('data');
this.input.on('data', this.dataHandler); this.input.on('data', this.dataHandler);
}; };
Events.on(Events.getSystemEvents().ThemeChanged, ( { themeId } ) => { Events.on(Events.getSystemEvents().ThemeChanged, ( { themeId } ) => {
if(_.get(this.currentTheme, 'info.themeId') === themeId) { if(_.get(this.currentTheme, 'info.themeId') === themeId) {
this.currentTheme = require('./theme.js').getAvailableThemes().get(themeId); this.currentTheme = require('./theme.js').getAvailableThemes().get(themeId);
} }
}); });
// //
// Peek at incoming |data| and emit events for any special // Peek at incoming |data| and emit events for any special
// handling that may include: // handling that may include:
// * Keyboard input // * Keyboard input
// * ANSI CSR's and the like // * ANSI CSR's and the like
// //
// References: // References:
// * http://www.ansi-bbs.org/ansi-bbs-core-server.html // * http://www.ansi-bbs.org/ansi-bbs-core-server.html
// * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/ // * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/
// //
this.getTermClient = function(deviceAttr) { this.getTermClient = function(deviceAttr) {
let termClient = { let termClient = {
// //
// See http://www.fbl.cz/arctel/download/techman.pdf // See http://www.fbl.cz/arctel/download/techman.pdf
// //
// Known clients: // Known clients:
// * Irssi ConnectBot (Android) // * Irssi ConnectBot (Android)
// //
'63;1;2' : 'arctel', '63;1;2' : 'arctel',
'50;86;84;88' : 'vtx', '50;86;84;88' : 'vtx',
}[deviceAttr]; }[deviceAttr];
if(!termClient) { if(!termClient) {
if(_.startsWith(deviceAttr, '67;84;101;114;109')) { if(_.startsWith(deviceAttr, '67;84;101;114;109')) {
// //
// See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
// //
// Known clients: // Known clients:
// * SyncTERM // * SyncTERM
// //
termClient = 'cterm'; termClient = 'cterm';
} }
} }
return termClient; return termClient;
}; };
this.isMouseInput = function(data) { this.isMouseInput = function(data) {
return /\x1b\[M/.test(data) || // eslint-disable-line no-control-regex return /\x1b\[M/.test(data) || // eslint-disable-line no-control-regex
/\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || // eslint-disable-line no-control-regex /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || // eslint-disable-line no-control-regex
/\u001b\[(\d+;\d+;\d+)M/.test(data) || /\u001b\[(\d+;\d+;\d+)M/.test(data) ||
/\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) || /\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) ||
/\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) ||
/\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) || /\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) ||
/\u001b\[(O|I)/.test(data); /\u001b\[(O|I)/.test(data);
}; };
this.getKeyComponentsFromCode = function(code) { this.getKeyComponentsFromCode = function(code) {
return { return {
// xterm/gnome // xterm/gnome
'OP' : { name : 'f1' }, 'OP' : { name : 'f1' },
'OQ' : { name : 'f2' }, 'OQ' : { name : 'f2' },
'OR' : { name : 'f3' }, 'OR' : { name : 'f3' },
'OS' : { name : 'f4' }, 'OS' : { name : 'f4' },
'OA' : { name : 'up arrow' }, 'OA' : { name : 'up arrow' },
'OB' : { name : 'down arrow' }, 'OB' : { name : 'down arrow' },
'OC' : { name : 'right arrow' }, 'OC' : { name : 'right arrow' },
'OD' : { name : 'left arrow' }, 'OD' : { name : 'left arrow' },
'OE' : { name : 'clear' }, 'OE' : { name : 'clear' },
'OF' : { name : 'end' }, 'OF' : { name : 'end' },
'OH' : { name : 'home' }, 'OH' : { name : 'home' },
// xterm/rxvt // xterm/rxvt
'[11~' : { name : 'f1' }, '[11~' : { name : 'f1' },
'[12~' : { name : 'f2' }, '[12~' : { name : 'f2' },
'[13~' : { name : 'f3' }, '[13~' : { name : 'f3' },
'[14~' : { name : 'f4' }, '[14~' : { name : 'f4' },
'[1~' : { name : 'home' }, '[1~' : { name : 'home' },
'[2~' : { name : 'insert' }, '[2~' : { name : 'insert' },
'[3~' : { name : 'delete' }, '[3~' : { name : 'delete' },
'[4~' : { name : 'end' }, '[4~' : { name : 'end' },
'[5~' : { name : 'page up' }, '[5~' : { name : 'page up' },
'[6~' : { name : 'page down' }, '[6~' : { name : 'page down' },
// Cygwin & libuv // Cygwin & libuv
'[[A' : { name : 'f1' }, '[[A' : { name : 'f1' },
'[[B' : { name : 'f2' }, '[[B' : { name : 'f2' },
'[[C' : { name : 'f3' }, '[[C' : { name : 'f3' },
'[[D' : { name : 'f4' }, '[[D' : { name : 'f4' },
'[[E' : { name : 'f5' }, '[[E' : { name : 'f5' },
// Common impls // Common impls
'[15~' : { name : 'f5' }, '[15~' : { name : 'f5' },
'[17~' : { name : 'f6' }, '[17~' : { name : 'f6' },
'[18~' : { name : 'f7' }, '[18~' : { name : 'f7' },
'[19~' : { name : 'f8' }, '[19~' : { name : 'f8' },
'[20~' : { name : 'f9' }, '[20~' : { name : 'f9' },
'[21~' : { name : 'f10' }, '[21~' : { name : 'f10' },
'[23~' : { name : 'f11' }, '[23~' : { name : 'f11' },
'[24~' : { name : 'f12' }, '[24~' : { name : 'f12' },
// xterm // xterm
'[A' : { name : 'up arrow' }, '[A' : { name : 'up arrow' },
'[B' : { name : 'down arrow' }, '[B' : { name : 'down arrow' },
'[C' : { name : 'right arrow' }, '[C' : { name : 'right arrow' },
'[D' : { name : 'left arrow' }, '[D' : { name : 'left arrow' },
'[E' : { name : 'clear' }, '[E' : { name : 'clear' },
'[F' : { name : 'end' }, '[F' : { name : 'end' },
'[H' : { name : 'home' }, '[H' : { name : 'home' },
// PuTTY // PuTTY
'[[5~' : { name : 'page up' }, '[[5~' : { name : 'page up' },
'[[6~' : { name : 'page down' }, '[[6~' : { name : 'page down' },
// rvxt // rvxt
'[7~' : { name : 'home' }, '[7~' : { name : 'home' },
'[8~' : { name : 'end' }, '[8~' : { name : 'end' },
// rxvt with modifiers // rxvt with modifiers
'[a' : { name : 'up arrow', shift : true }, '[a' : { name : 'up arrow', shift : true },
'[b' : { name : 'down arrow', shift : true }, '[b' : { name : 'down arrow', shift : true },
'[c' : { name : 'right arrow', shift : true }, '[c' : { name : 'right arrow', shift : true },
'[d' : { name : 'left arrow', shift : true }, '[d' : { name : 'left arrow', shift : true },
'[e' : { name : 'clear', shift : true }, '[e' : { name : 'clear', shift : true },
'[2$' : { name : 'insert', shift : true }, '[2$' : { name : 'insert', shift : true },
'[3$' : { name : 'delete', shift : true }, '[3$' : { name : 'delete', shift : true },
'[5$' : { name : 'page up', shift : true }, '[5$' : { name : 'page up', shift : true },
'[6$' : { name : 'page down', shift : true }, '[6$' : { name : 'page down', shift : true },
'[7$' : { name : 'home', shift : true }, '[7$' : { name : 'home', shift : true },
'[8$' : { name : 'end', shift : true }, '[8$' : { name : 'end', shift : true },
'Oa' : { name : 'up arrow', ctrl : true }, 'Oa' : { name : 'up arrow', ctrl : true },
'Ob' : { name : 'down arrow', ctrl : true }, 'Ob' : { name : 'down arrow', ctrl : true },
'Oc' : { name : 'right arrow', ctrl : true }, 'Oc' : { name : 'right arrow', ctrl : true },
'Od' : { name : 'left arrow', ctrl : true }, 'Od' : { name : 'left arrow', ctrl : true },
'Oe' : { name : 'clear', ctrl : true }, 'Oe' : { name : 'clear', ctrl : true },
'[2^' : { name : 'insert', ctrl : true }, '[2^' : { name : 'insert', ctrl : true },
'[3^' : { name : 'delete', ctrl : true }, '[3^' : { name : 'delete', ctrl : true },
'[5^' : { name : 'page up', ctrl : true }, '[5^' : { name : 'page up', ctrl : true },
'[6^' : { name : 'page down', ctrl : true }, '[6^' : { name : 'page down', ctrl : true },
'[7^' : { name : 'home', ctrl : true }, '[7^' : { name : 'home', ctrl : true },
'[8^' : { name : 'end', ctrl : true }, '[8^' : { name : 'end', ctrl : true },
// SyncTERM / EtherTerm // SyncTERM / EtherTerm
'[K' : { name : 'end' }, '[K' : { name : 'end' },
'[@' : { name : 'insert' }, '[@' : { name : 'insert' },
'[V' : { name : 'page up' }, '[V' : { name : 'page up' },
'[U' : { name : 'page down' }, '[U' : { name : 'page down' },
// other // other
'[Z' : { name : 'tab', shift : true }, '[Z' : { name : 'tab', shift : true },
}[code]; }[code];
}; };
this.on('data', function clientData(data) { this.on('data', function clientData(data) {
// create a uniform format that can be parsed below // create a uniform format that can be parsed below
if(data[0] > 127 && undefined === data[1]) { if(data[0] > 127 && undefined === data[1]) {
data[0] -= 128; data[0] -= 128;
data = '\u001b' + data.toString('utf-8'); data = '\u001b' + data.toString('utf-8');
} else { } else {
data = data.toString('utf-8'); data = data.toString('utf-8');
} }
if(self.isMouseInput(data)) { if(self.isMouseInput(data)) {
return; return;
} }
var buf = []; var buf = [];
var m; var m;
while((m = RE_ESC_CODE_ANYWHERE.exec(data))) { while((m = RE_ESC_CODE_ANYWHERE.exec(data))) {
buf = buf.concat(data.slice(0, m.index).split('')); buf = buf.concat(data.slice(0, m.index).split(''));
buf.push(m[0]); buf.push(m[0]);
data = data.slice(m.index + m[0].length); data = data.slice(m.index + m[0].length);
} }
buf = buf.concat(data.split('')); // remainder buf = buf.concat(data.split('')); // remainder
buf.forEach(function bufPart(s) { buf.forEach(function bufPart(s) {
var key = { var key = {
seq : s, seq : s,
name : undefined, name : undefined,
ctrl : false, ctrl : false,
meta : false, meta : false,
shift : false, shift : false,
}; };
var parts; var parts;
if((parts = RE_DSR_RESPONSE_ANYWHERE.exec(s))) { if((parts = RE_DSR_RESPONSE_ANYWHERE.exec(s))) {
if('R' === parts[2]) { if('R' === parts[2]) {
const cprArgs = parts[1].split(';').map(v => (parseInt(v, 10) || 0) ); const cprArgs = parts[1].split(';').map(v => (parseInt(v, 10) || 0) );
if(2 === cprArgs.length) { if(2 === cprArgs.length) {
if(self.cprOffset) { if(self.cprOffset) {
cprArgs[0] = cprArgs[0] + self.cprOffset; cprArgs[0] = cprArgs[0] + self.cprOffset;
cprArgs[1] = cprArgs[1] + self.cprOffset; cprArgs[1] = cprArgs[1] + self.cprOffset;
} }
self.emit('cursor position report', cprArgs); self.emit('cursor position report', cprArgs);
} }
} }
} else if((parts = RE_DEV_ATTR_RESPONSE_ANYWHERE.exec(s))) { } else if((parts = RE_DEV_ATTR_RESPONSE_ANYWHERE.exec(s))) {
assert('c' === parts[2]); assert('c' === parts[2]);
var termClient = self.getTermClient(parts[1]); var termClient = self.getTermClient(parts[1]);
if(termClient) { if(termClient) {
self.term.termClient = termClient; self.term.termClient = termClient;
} }
} else if('\r' === s) { } else if('\r' === s) {
key.name = 'return'; key.name = 'return';
} else if('\n' === s) { } else if('\n' === s) {
key.name = 'line feed'; key.name = 'line feed';
} else if('\t' === s) { } else if('\t' === s) {
key.name = 'tab'; key.name = 'tab';
} else if('\x7f' === s) { } else if('\x7f' === s) {
// //
// Backspace vs delete is a crazy thing, especially in *nix. // Backspace vs delete is a crazy thing, especially in *nix.
// - ANSI-BBS uses 0x7f for DEL // - ANSI-BBS uses 0x7f for DEL
// - xterm et. al clients send 0x7f for backspace... ugg. // - xterm et. al clients send 0x7f for backspace... ugg.
// //
// See http://www.hypexr.org/linux_ruboff.php // See http://www.hypexr.org/linux_ruboff.php
// And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html // And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html
// //
if(self.term.isNixTerm()) { if(self.term.isNixTerm()) {
key.name = 'backspace'; key.name = 'backspace';
} else { } else {
key.name = 'delete'; key.name = 'delete';
} }
} else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) { } else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) {
// backspace, CTRL-H // backspace, CTRL-H
key.name = 'backspace'; key.name = 'backspace';
key.meta = ('\x1b' === s.charAt(0)); key.meta = ('\x1b' === s.charAt(0));
} else if('\x1b' === s || '\x1b\x1b' === s) { } else if('\x1b' === s || '\x1b\x1b' === s) {
key.name = 'escape'; key.name = 'escape';
key.meta = (2 === s.length); key.meta = (2 === s.length);
} else if (' ' === s || '\x1b ' === s) { } else if (' ' === s || '\x1b ' === s) {
// rather annoying that space can come in other than just " " // rather annoying that space can come in other than just " "
key.name = 'space'; key.name = 'space';
key.meta = (2 === s.length); key.meta = (2 === s.length);
} else if(1 === s.length && s <= '\x1a') { } else if(1 === s.length && s <= '\x1a') {
// CTRL-<letter> // CTRL-<letter>
key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
key.ctrl = true; key.ctrl = true;
} else if(1 === s.length && s >= 'a' && s <= 'z') { } else if(1 === s.length && s >= 'a' && s <= 'z') {
// normal, lowercased letter // normal, lowercased letter
key.name = s; key.name = s;
} else if(1 === s.length && s >= 'A' && s <= 'Z') { } else if(1 === s.length && s >= 'A' && s <= 'Z') {
key.name = s.toLowerCase(); key.name = s.toLowerCase();
key.shift = true; key.shift = true;
} else if ((parts = RE_META_KEYCODE.exec(s))) { } else if ((parts = RE_META_KEYCODE.exec(s))) {
// meta with character key // meta with character key
key.name = parts[1].toLowerCase(); key.name = parts[1].toLowerCase();
key.meta = true; key.meta = true;
key.shift = /^[A-Z]$/.test(parts[1]); key.shift = /^[A-Z]$/.test(parts[1]);
} else if((parts = RE_FUNCTION_KEYCODE.exec(s))) { } else if((parts = RE_FUNCTION_KEYCODE.exec(s))) {
var code = var code =
(parts[1] || '') + (parts[2] || '') + (parts[1] || '') + (parts[2] || '') +
(parts[4] || '') + (parts[9] || ''); (parts[4] || '') + (parts[9] || '');
var modifier = (parts[3] || parts[8] || 1) - 1; var modifier = (parts[3] || parts[8] || 1) - 1;
key.ctrl = !!(modifier & 4); key.ctrl = !!(modifier & 4);
key.meta = !!(modifier & 10); key.meta = !!(modifier & 10);
key.shift = !!(modifier & 1); key.shift = !!(modifier & 1);
key.code = code; key.code = code;
_.assign(key, self.getKeyComponentsFromCode(code)); _.assign(key, self.getKeyComponentsFromCode(code));
} }
var ch; var ch;
if(1 === s.length) { if(1 === s.length) {
ch = s; ch = s;
} else if('space' === key.name) { } else if('space' === key.name) {
// stupid hack to always get space as a regular char // stupid hack to always get space as a regular char
ch = ' '; ch = ' ';
} }
if(_.isUndefined(key.name)) { if(_.isUndefined(key.name)) {
key = undefined; key = undefined;
} else { } else {
// //
// Adjust name for CTRL/Shift/Meta modifiers // Adjust name for CTRL/Shift/Meta modifiers
// //
key.name = key.name =
(key.ctrl ? 'ctrl + ' : '') + (key.ctrl ? 'ctrl + ' : '') +
(key.meta ? 'meta + ' : '') + (key.meta ? 'meta + ' : '') +
(key.shift ? 'shift + ' : '') + (key.shift ? 'shift + ' : '') +
key.name; key.name;
} }
if(key || ch) { if(key || ch) {
if(Config().logging.traceUserKeyboardInput) { if(Config().logging.traceUserKeyboardInput) {
self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line
} }
self.lastKeyPressMs = Date.now(); self.lastKeyPressMs = Date.now();
if(!self.ignoreInput) { if(!self.ignoreInput) {
self.emit('key press', ch, key); self.emit('key press', ch, key);
} }
} }
}); });
}); });
} }
require('util').inherits(Client, stream); require('util').inherits(Client, stream);
Client.prototype.setInputOutput = function(input, output) { Client.prototype.setInputOutput = function(input, output) {
this.input = input; this.input = input;
this.output = output; this.output = output;
this.term = new term.ClientTerminal(this.output); this.term = new term.ClientTerminal(this.output);
}; };
Client.prototype.setTermType = function(termType) { Client.prototype.setTermType = function(termType) {
this.term.env.TERM = termType; this.term.env.TERM = termType;
this.term.termType = termType; this.term.termType = termType;
this.log.debug( { termType : termType }, 'Set terminal type'); this.log.debug( { termType : termType }, 'Set terminal type');
}; };
Client.prototype.startIdleMonitor = function() { Client.prototype.startIdleMonitor = function() {
this.lastKeyPressMs = Date.now(); this.lastKeyPressMs = Date.now();
// //
// Every 1m, check for idle. // Every 1m, check for idle.
// //
this.idleCheck = setInterval( () => { this.idleCheck = setInterval( () => {
const nowMs = Date.now(); const nowMs = Date.now();
const idleLogoutSeconds = this.user.isAuthenticated() ? const idleLogoutSeconds = this.user.isAuthenticated() ?
Config().misc.idleLogoutSeconds : Config().misc.idleLogoutSeconds :
Config().misc.preAuthIdleLogoutSeconds; Config().misc.preAuthIdleLogoutSeconds;
if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) {
this.emit('idle timeout'); this.emit('idle timeout');
} }
}, 1000 * 60); }, 1000 * 60);
}; };
Client.prototype.stopIdleMonitor = function() { Client.prototype.stopIdleMonitor = function() {
clearInterval(this.idleCheck); clearInterval(this.idleCheck);
}; };
Client.prototype.end = function () { Client.prototype.end = function () {
if(this.term) { if(this.term) {
this.term.disconnect(); this.term.disconnect();
} }
var currentModule = this.menuStack.getCurrentModule; var currentModule = this.menuStack.getCurrentModule;
if(currentModule) { if(currentModule) {
currentModule.leave(); currentModule.leave();
} }
this.stopIdleMonitor(); this.stopIdleMonitor();
try { try {
// //
// We can end up calling 'end' before TTY/etc. is established, e.g. with SSH // We can end up calling 'end' before TTY/etc. is established, e.g. with SSH
// //
// :TODO: is this OK? // :TODO: is this OK?
return this.output.end.apply(this.output, arguments); return this.output.end.apply(this.output, arguments);
} catch(e) { } catch(e) {
// TypeError // TypeError
} }
}; };
Client.prototype.destroy = function () { Client.prototype.destroy = function () {
return this.output.destroy.apply(this.output, arguments); return this.output.destroy.apply(this.output, arguments);
}; };
Client.prototype.destroySoon = function () { Client.prototype.destroySoon = function () {
return this.output.destroySoon.apply(this.output, arguments); return this.output.destroySoon.apply(this.output, arguments);
}; };
Client.prototype.waitForKeyPress = function(cb) { Client.prototype.waitForKeyPress = function(cb) {
this.once('key press', function kp(ch, key) { this.once('key press', function kp(ch, key) {
cb(ch, key); cb(ch, key);
}); });
}; };
Client.prototype.isLocal = function() { Client.prototype.isLocal = function() {
// :TODO: Handle ipv6 better // :TODO: Handle ipv6 better
return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress); return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress);
}; };
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -502,41 +502,41 @@ Client.prototype.isLocal = function() {
// :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something // :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something
Client.prototype.defaultHandlerMissingMod = function() { Client.prototype.defaultHandlerMissingMod = function() {
var self = this; var self = this;
function handler(err) { function handler(err) {
self.log.error(err); self.log.error(err);
self.term.write(ansi.resetScreen()); self.term.write(ansi.resetScreen());
self.term.write('An unrecoverable error has been encountered!\n'); self.term.write('An unrecoverable error has been encountered!\n');
self.term.write('This has been logged for your SysOp to review.\n'); self.term.write('This has been logged for your SysOp to review.\n');
self.term.write('\nGoodbye!\n'); self.term.write('\nGoodbye!\n');
//self.term.write(err); //self.term.write(err);
//if(miscUtil.isDevelopment() && err.stack) { //if(miscUtil.isDevelopment() && err.stack) {
// self.term.write('\n' + err.stack + '\n'); // self.term.write('\n' + err.stack + '\n');
//} //}
self.end(); self.end();
} }
return handler; return handler;
}; };
Client.prototype.terminalSupports = function(query) { Client.prototype.terminalSupports = function(query) {
const termClient = this.term.termClient; const termClient = this.term.termClient;
switch(query) { switch(query) {
case 'vtx_audio' : case 'vtx_audio' :
// https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
return 'vtx' === termClient; return 'vtx' === termClient;
case 'vtx_hyperlink' : case 'vtx_hyperlink' :
return 'vtx' === termClient; return 'vtx' === termClient;
default : default :
return false; return false;
} }
}; };

View File

@ -23,98 +23,98 @@ function getActiveConnections() { return clientConnections; }
function getActiveNodeList(authUsersOnly) { function getActiveNodeList(authUsersOnly) {
if(!_.isBoolean(authUsersOnly)) { if(!_.isBoolean(authUsersOnly)) {
authUsersOnly = true; authUsersOnly = true;
} }
const now = moment(); const now = moment();
const activeConnections = getActiveConnections().filter(ac => { const activeConnections = getActiveConnections().filter(ac => {
return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly); return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly);
}); });
return _.map(activeConnections, ac => { return _.map(activeConnections, ac => {
const entry = { const entry = {
node : ac.node, node : ac.node,
authenticated : ac.user.isAuthenticated(), authenticated : ac.user.isAuthenticated(),
userId : ac.user.userId, userId : ac.user.userId,
action : _.has(ac, 'currentMenuModule.menuConfig.desc') ? ac.currentMenuModule.menuConfig.desc : 'Unknown', action : _.has(ac, 'currentMenuModule.menuConfig.desc') ? ac.currentMenuModule.menuConfig.desc : 'Unknown',
}; };
// //
// There may be a connection, but not a logged in user as of yet // There may be a connection, but not a logged in user as of yet
// //
if(ac.user.isAuthenticated()) { if(ac.user.isAuthenticated()) {
entry.userName = ac.user.username; entry.userName = ac.user.username;
entry.realName = ac.user.properties.real_name; entry.realName = ac.user.properties.real_name;
entry.location = ac.user.properties.location; entry.location = ac.user.properties.location;
entry.affils = ac.user.properties.affiliation; entry.affils = ac.user.properties.affiliation;
const diff = now.diff(moment(ac.user.properties.last_login_timestamp), 'minutes'); const diff = now.diff(moment(ac.user.properties.last_login_timestamp), 'minutes');
entry.timeOn = moment.duration(diff, 'minutes'); entry.timeOn = moment.duration(diff, 'minutes');
} }
return entry; return entry;
}); });
} }
function addNewClient(client, clientSock) { function addNewClient(client, clientSock) {
const id = client.session.id = clientConnections.push(client) - 1; const id = client.session.id = clientConnections.push(client) - 1;
const remoteAddress = client.remoteAddress = clientSock.remoteAddress; const remoteAddress = client.remoteAddress = clientSock.remoteAddress;
// create a uniqe identifier one-time ID for this session // create a uniqe identifier one-time ID for this session
client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]); client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]);
// Create a client specific logger // Create a client specific logger
// Note that this will be updated @ login with additional information // Note that this will be updated @ login with additional information
client.log = logger.log.child( { clientId : id, sessionId : client.session.uniqueId } ); client.log = logger.log.child( { clientId : id, sessionId : client.session.uniqueId } );
const connInfo = { const connInfo = {
remoteAddress : remoteAddress, remoteAddress : remoteAddress,
serverName : client.session.serverName, serverName : client.session.serverName,
isSecure : client.session.isSecure, isSecure : client.session.isSecure,
}; };
if(client.log.debug()) { if(client.log.debug()) {
connInfo.port = clientSock.localPort; connInfo.port = clientSock.localPort;
connInfo.family = clientSock.localFamily; connInfo.family = clientSock.localFamily;
} }
client.log.info(connInfo, 'Client connected'); client.log.info(connInfo, 'Client connected');
Events.emit( Events.emit(
Events.getSystemEvents().ClientConnected, Events.getSystemEvents().ClientConnected,
{ client : client, connectionCount : clientConnections.length } { client : client, connectionCount : clientConnections.length }
); );
return id; return id;
} }
function removeClient(client) { function removeClient(client) {
client.end(); client.end();
const i = clientConnections.indexOf(client); const i = clientConnections.indexOf(client);
if(i > -1) { if(i > -1) {
clientConnections.splice(i, 1); clientConnections.splice(i, 1);
logger.log.info( logger.log.info(
{ {
connectionCount : clientConnections.length, connectionCount : clientConnections.length,
clientId : client.session.id clientId : client.session.id
}, },
'Client disconnected' 'Client disconnected'
); );
if(client.user && client.user.isValid()) { if(client.user && client.user.isValid()) {
Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user } ); Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user } );
} }
Events.emit( Events.emit(
Events.getSystemEvents().ClientDisconnected, Events.getSystemEvents().ClientDisconnected,
{ client : client, connectionCount : clientConnections.length } { client : client, connectionCount : clientConnections.length }
); );
} }
} }
function getConnectionByUserId(userId) { function getConnectionByUserId(userId) {
return getActiveConnections().find( ac => userId === ac.user.userId ); return getActiveConnections().find( ac => userId === ac.user.userId );
} }

View File

@ -13,185 +13,185 @@ var _ = require('lodash');
exports.ClientTerminal = ClientTerminal; exports.ClientTerminal = ClientTerminal;
function ClientTerminal(output) { function ClientTerminal(output) {
this.output = output; this.output = output;
var outputEncoding = 'cp437'; var outputEncoding = 'cp437';
assert(iconv.encodingExists(outputEncoding)); assert(iconv.encodingExists(outputEncoding));
// convert line feeds such as \n -> \r\n // convert line feeds such as \n -> \r\n
this.convertLF = true; this.convertLF = true;
// //
// Some terminal we handle specially // Some terminal we handle specially
// They can also be found in this.env{} // They can also be found in this.env{}
// //
var termType = 'unknown'; var termType = 'unknown';
var termHeight = 0; var termHeight = 0;
var termWidth = 0; var termWidth = 0;
var termClient = 'unknown'; var termClient = 'unknown';
this.currentSyncFont = 'not_set'; this.currentSyncFont = 'not_set';
// Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc. // Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc.
this.env = {}; this.env = {};
Object.defineProperty(this, 'outputEncoding', { Object.defineProperty(this, 'outputEncoding', {
get : function() { get : function() {
return outputEncoding; return outputEncoding;
}, },
set : function(enc) { set : function(enc) {
if(iconv.encodingExists(enc)) { if(iconv.encodingExists(enc)) {
outputEncoding = enc; outputEncoding = enc;
} else { } else {
Log.warn({ encoding : enc }, 'Unknown encoding'); Log.warn({ encoding : enc }, 'Unknown encoding');
} }
} }
}); });
Object.defineProperty(this, 'termType', { Object.defineProperty(this, 'termType', {
get : function() { get : function() {
return termType; return termType;
}, },
set : function(ttype) { set : function(ttype) {
termType = ttype.toLowerCase(); termType = ttype.toLowerCase();
if(this.isANSI()) { if(this.isANSI()) {
this.outputEncoding = 'cp437'; this.outputEncoding = 'cp437';
} else { } else {
// :TODO: See how x84 does this -- only set if local/remote are binary // :TODO: See how x84 does this -- only set if local/remote are binary
this.outputEncoding = 'utf8'; this.outputEncoding = 'utf8';
} }
// :TODO: according to this: http://mud-dev.wikidot.com/article:telnet-client-identification // :TODO: according to this: http://mud-dev.wikidot.com/article:telnet-client-identification
// Windows telnet will send "VTNT". If so, set termClient='windows' // Windows telnet will send "VTNT". If so, set termClient='windows'
// there are some others on the page as well // there are some others on the page as well
Log.debug( { encoding : this.outputEncoding }, 'Set output encoding due to terminal type change'); Log.debug( { encoding : this.outputEncoding }, 'Set output encoding due to terminal type change');
} }
}); });
Object.defineProperty(this, 'termWidth', { Object.defineProperty(this, 'termWidth', {
get : function() { get : function() {
return termWidth; return termWidth;
}, },
set : function(width) { set : function(width) {
if(width > 0) { if(width > 0) {
termWidth = width; termWidth = width;
} }
} }
}); });
Object.defineProperty(this, 'termHeight', { Object.defineProperty(this, 'termHeight', {
get : function() { get : function() {
return termHeight; return termHeight;
}, },
set : function(height) { set : function(height) {
if(height > 0) { if(height > 0) {
termHeight = height; termHeight = height;
} }
} }
}); });
Object.defineProperty(this, 'termClient', { Object.defineProperty(this, 'termClient', {
get : function() { get : function() {
return termClient; return termClient;
}, },
set : function(tc) { set : function(tc) {
termClient = tc; termClient = tc;
Log.debug( { termClient : this.termClient }, 'Set known terminal client'); Log.debug( { termClient : this.termClient }, 'Set known terminal client');
} }
}); });
} }
ClientTerminal.prototype.disconnect = function() { ClientTerminal.prototype.disconnect = function() {
this.output = null; this.output = null;
}; };
ClientTerminal.prototype.isNixTerm = function() { ClientTerminal.prototype.isNixTerm = function() {
// //
// Standard *nix type terminals // Standard *nix type terminals
// //
if(this.termType.startsWith('xterm')) { if(this.termType.startsWith('xterm')) {
return true; return true;
} }
return [ 'xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator' ].includes(this.termType); return [ 'xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator' ].includes(this.termType);
}; };
ClientTerminal.prototype.isANSI = function() { ClientTerminal.prototype.isANSI = function() {
// //
// ANSI terminals should be encoded to CP437 // ANSI terminals should be encoded to CP437
// //
// Some terminal types provided by Mercyful Fate / Enthral: // Some terminal types provided by Mercyful Fate / Enthral:
// ANSI-BBS // ANSI-BBS
// PC-ANSI // PC-ANSI
// QANSI // QANSI
// SCOANSI // SCOANSI
// VT100 // VT100
// QNX // QNX
// //
// Reports from various terminals // Reports from various terminals
// //
// syncterm: // syncterm:
// * SyncTERM // * SyncTERM
// //
// xterm: // xterm:
// * PuTTY // * PuTTY
// //
// ansi-bbs: // ansi-bbs:
// * fTelnet // * fTelnet
// //
// pcansi: // pcansi:
// * ZOC // * ZOC
// //
// screen: // screen:
// * ConnectBot (Android) // * ConnectBot (Android)
// //
// linux: // linux:
// * JuiceSSH (note: TERM=linux also) // * JuiceSSH (note: TERM=linux also)
// //
return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].includes(this.termType); return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].includes(this.termType);
}; };
// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) // :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it)
ClientTerminal.prototype.write = function(s, convertLineFeeds, cb) { ClientTerminal.prototype.write = function(s, convertLineFeeds, cb) {
this.rawWrite(this.encode(s, convertLineFeeds), cb); this.rawWrite(this.encode(s, convertLineFeeds), cb);
}; };
ClientTerminal.prototype.rawWrite = function(s, cb) { ClientTerminal.prototype.rawWrite = function(s, cb) {
if(this.output) { if(this.output) {
this.output.write(s, err => { this.output.write(s, err => {
if(cb) { if(cb) {
return cb(err); return cb(err);
} }
if(err) { if(err) {
Log.warn( { error : err.message }, 'Failed writing to socket'); Log.warn( { error : err.message }, 'Failed writing to socket');
} }
}); });
} }
}; };
ClientTerminal.prototype.pipeWrite = function(s, spec, cb) { ClientTerminal.prototype.pipeWrite = function(s, spec, cb) {
spec = spec || 'renegade'; spec = spec || 'renegade';
var conv = { var conv = {
enigma : enigmaToAnsi, enigma : enigmaToAnsi,
renegade : renegadeToAnsi, renegade : renegadeToAnsi,
}[spec] || renegadeToAnsi; }[spec] || renegadeToAnsi;
this.write(conv(s, this), null, cb); // null = use default for |convertLineFeeds| this.write(conv(s, this), null, cb); // null = use default for |convertLineFeeds|
}; };
ClientTerminal.prototype.encode = function(s, convertLineFeeds) { ClientTerminal.prototype.encode = function(s, convertLineFeeds) {
convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF; convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF;
if(convertLineFeeds && _.isString(s)) { if(convertLineFeeds && _.isString(s)) {
s = s.replace(/\n/g, '\r\n'); s = s.replace(/\n/g, '\r\n');
} }
return iconv.encode(s, this.outputEncoding); return iconv.encode(s, this.outputEncoding);
}; };

View File

@ -28,139 +28,139 @@ exports.controlCodesToAnsi = controlCodesToAnsi;
// :TODO: rid of enigmaToAnsi() -- never really use. Instead, create bbsToAnsi() that supports renegade, PCB, WWIV, etc... // :TODO: rid of enigmaToAnsi() -- never really use. Instead, create bbsToAnsi() that supports renegade, PCB, WWIV, etc...
function enigmaToAnsi(s, client) { function enigmaToAnsi(s, client) {
if(-1 == s.indexOf('|')) { if(-1 == s.indexOf('|')) {
return s; // no pipe codes present return s; // no pipe codes present
} }
var result = ''; var result = '';
var re = /\|([A-Z\d]{2}|\|)/g; var re = /\|([A-Z\d]{2}|\|)/g;
var m; var m;
var lastIndex = 0; var lastIndex = 0;
while((m = re.exec(s))) { while((m = re.exec(s))) {
var val = m[1]; var val = m[1];
if('|' == val) { if('|' == val) {
result += '|'; result += '|';
continue; continue;
} }
// convert to number // convert to number
val = parseInt(val, 10); val = parseInt(val, 10);
if(isNaN(val)) { if(isNaN(val)) {
// //
// ENiGMA MCI code? Only available if |client| // ENiGMA MCI code? Only available if |client|
// is supplied. // is supplied.
// //
val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal
} }
if(_.isString(val)) { if(_.isString(val)) {
result += s.substr(lastIndex, m.index - lastIndex) + val; result += s.substr(lastIndex, m.index - lastIndex) + val;
} else { } else {
assert(val >= 0 && val <= 47); assert(val >= 0 && val <= 47);
var attr = ''; var attr = '';
if(7 == val) { if(7 == val) {
attr = ansi.sgr('normal'); attr = ansi.sgr('normal');
} else if (val < 7 || val >= 16) { } else if (val < 7 || val >= 16) {
attr = ansi.sgr(['normal', val]); attr = ansi.sgr(['normal', val]);
} else if (val <= 15) { } else if (val <= 15) {
attr = ansi.sgr(['normal', val - 8, 'bold']); attr = ansi.sgr(['normal', val - 8, 'bold']);
} }
result += s.substr(lastIndex, m.index - lastIndex) + attr; result += s.substr(lastIndex, m.index - lastIndex) + attr;
} }
lastIndex = re.lastIndex; lastIndex = re.lastIndex;
} }
result = (0 === result.length ? s : result + s.substr(lastIndex)); result = (0 === result.length ? s : result + s.substr(lastIndex));
return result; return result;
} }
function stripEnigmaCodes(s) { function stripEnigmaCodes(s) {
return s.replace(/\|[A-Z\d]{2}/g, ''); return s.replace(/\|[A-Z\d]{2}/g, '');
} }
function enigmaStrLen(s) { function enigmaStrLen(s) {
return stripEnigmaCodes(s).length; return stripEnigmaCodes(s).length;
} }
function ansiSgrFromRenegadeColorCode(cc) { function ansiSgrFromRenegadeColorCode(cc) {
return ansi.sgr({ return ansi.sgr({
0 : [ 'reset', 'black' ], 0 : [ 'reset', 'black' ],
1 : [ 'reset', 'blue' ], 1 : [ 'reset', 'blue' ],
2 : [ 'reset', 'green' ], 2 : [ 'reset', 'green' ],
3 : [ 'reset', 'cyan' ], 3 : [ 'reset', 'cyan' ],
4 : [ 'reset', 'red' ], 4 : [ 'reset', 'red' ],
5 : [ 'reset', 'magenta' ], 5 : [ 'reset', 'magenta' ],
6 : [ 'reset', 'yellow' ], 6 : [ 'reset', 'yellow' ],
7 : [ 'reset', 'white' ], 7 : [ 'reset', 'white' ],
8 : [ 'bold', 'black' ], 8 : [ 'bold', 'black' ],
9 : [ 'bold', 'blue' ], 9 : [ 'bold', 'blue' ],
10 : [ 'bold', 'green' ], 10 : [ 'bold', 'green' ],
11 : [ 'bold', 'cyan' ], 11 : [ 'bold', 'cyan' ],
12 : [ 'bold', 'red' ], 12 : [ 'bold', 'red' ],
13 : [ 'bold', 'magenta' ], 13 : [ 'bold', 'magenta' ],
14 : [ 'bold', 'yellow' ], 14 : [ 'bold', 'yellow' ],
15 : [ 'bold', 'white' ], 15 : [ 'bold', 'white' ],
16 : [ 'blackBG' ], 16 : [ 'blackBG' ],
17 : [ 'blueBG' ], 17 : [ 'blueBG' ],
18 : [ 'greenBG' ], 18 : [ 'greenBG' ],
19 : [ 'cyanBG' ], 19 : [ 'cyanBG' ],
20 : [ 'redBG' ], 20 : [ 'redBG' ],
21 : [ 'magentaBG' ], 21 : [ 'magentaBG' ],
22 : [ 'yellowBG' ], 22 : [ 'yellowBG' ],
23 : [ 'whiteBG' ], 23 : [ 'whiteBG' ],
24 : [ 'blink', 'blackBG' ], 24 : [ 'blink', 'blackBG' ],
25 : [ 'blink', 'blueBG' ], 25 : [ 'blink', 'blueBG' ],
26 : [ 'blink', 'greenBG' ], 26 : [ 'blink', 'greenBG' ],
27 : [ 'blink', 'cyanBG' ], 27 : [ 'blink', 'cyanBG' ],
28 : [ 'blink', 'redBG' ], 28 : [ 'blink', 'redBG' ],
29 : [ 'blink', 'magentaBG' ], 29 : [ 'blink', 'magentaBG' ],
30 : [ 'blink', 'yellowBG' ], 30 : [ 'blink', 'yellowBG' ],
31 : [ 'blink', 'whiteBG' ], 31 : [ 'blink', 'whiteBG' ],
}[cc] || 'normal'); }[cc] || 'normal');
} }
function renegadeToAnsi(s, client) { function renegadeToAnsi(s, client) {
if(-1 == s.indexOf('|')) { if(-1 == s.indexOf('|')) {
return s; // no pipe codes present return s; // no pipe codes present
} }
var result = ''; var result = '';
var re = /\|([A-Z\d]{2}|\|)/g; var re = /\|([A-Z\d]{2}|\|)/g;
var m; var m;
var lastIndex = 0; var lastIndex = 0;
while((m = re.exec(s))) { while((m = re.exec(s))) {
var val = m[1]; var val = m[1];
if('|' == val) { if('|' == val) {
result += '|'; result += '|';
continue; continue;
} }
// convert to number // convert to number
val = parseInt(val, 10); val = parseInt(val, 10);
if(isNaN(val)) { if(isNaN(val)) {
val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal
} }
if(_.isString(val)) { if(_.isString(val)) {
result += s.substr(lastIndex, m.index - lastIndex) + val; result += s.substr(lastIndex, m.index - lastIndex) + val;
} else { } else {
const attr = ansiSgrFromRenegadeColorCode(val); const attr = ansiSgrFromRenegadeColorCode(val);
result += s.substr(lastIndex, m.index - lastIndex) + attr; result += s.substr(lastIndex, m.index - lastIndex) + attr;
} }
lastIndex = re.lastIndex; lastIndex = re.lastIndex;
} }
return (0 === result.length ? s : result + s.substr(lastIndex)); return (0 === result.length ? s : result + s.substr(lastIndex));
} }
// //
@ -180,113 +180,113 @@ function renegadeToAnsi(s, client) {
// * http://wiki.synchro.net/custom:colors // * http://wiki.synchro.net/custom:colors
// //
function controlCodesToAnsi(s, client) { function controlCodesToAnsi(s, client) {
const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex
let m; let m;
let result = ''; let result = '';
let lastIndex = 0; let lastIndex = 0;
let v; let v;
let fg; let fg;
let bg; let bg;
while((m = RE.exec(s))) { while((m = RE.exec(s))) {
switch(m[0].charAt(0)) { switch(m[0].charAt(0)) {
case '|' : case '|' :
// Renegade or ENiGMA MCI // Renegade or ENiGMA MCI
v = parseInt(m[2], 10); v = parseInt(m[2], 10);
if(isNaN(v)) { if(isNaN(v)) {
v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal
} }
if(_.isString(v)) { if(_.isString(v)) {
result += s.substr(lastIndex, m.index - lastIndex) + v; result += s.substr(lastIndex, m.index - lastIndex) + v;
} else { } else {
v = ansiSgrFromRenegadeColorCode(v); v = ansiSgrFromRenegadeColorCode(v);
result += s.substr(lastIndex, m.index - lastIndex) + v; result += s.substr(lastIndex, m.index - lastIndex) + v;
} }
break; break;
case '@' : case '@' :
// PCBoard @X## or Wildcat! @##@ // PCBoard @X## or Wildcat! @##@
if('@' === m[0].substr(-1)) { if('@' === m[0].substr(-1)) {
// Wildcat! // Wildcat!
v = m[6]; v = m[6];
} else { } else {
v = m[4]; v = m[4];
} }
fg = { fg = {
0 : [ 'reset', 'black' ], 0 : [ 'reset', 'black' ],
1 : [ 'reset', 'blue' ], 1 : [ 'reset', 'blue' ],
2 : [ 'reset', 'green' ], 2 : [ 'reset', 'green' ],
3 : [ 'reset', 'cyan' ], 3 : [ 'reset', 'cyan' ],
4 : [ 'reset', 'red' ], 4 : [ 'reset', 'red' ],
5 : [ 'reset', 'magenta' ], 5 : [ 'reset', 'magenta' ],
6 : [ 'reset', 'yellow' ], 6 : [ 'reset', 'yellow' ],
7 : [ 'reset', 'white' ], 7 : [ 'reset', 'white' ],
8 : [ 'blink', 'black' ], 8 : [ 'blink', 'black' ],
9 : [ 'blink', 'blue' ], 9 : [ 'blink', 'blue' ],
A : [ 'blink', 'green' ], A : [ 'blink', 'green' ],
B : [ 'blink', 'cyan' ], B : [ 'blink', 'cyan' ],
C : [ 'blink', 'red' ], C : [ 'blink', 'red' ],
D : [ 'blink', 'magenta' ], D : [ 'blink', 'magenta' ],
E : [ 'blink', 'yellow' ], E : [ 'blink', 'yellow' ],
F : [ 'blink', 'white' ], F : [ 'blink', 'white' ],
}[v.charAt(0)] || ['normal']; }[v.charAt(0)] || ['normal'];
bg = { bg = {
0 : [ 'blackBG' ], 0 : [ 'blackBG' ],
1 : [ 'blueBG' ], 1 : [ 'blueBG' ],
2 : [ 'greenBG' ], 2 : [ 'greenBG' ],
3 : [ 'cyanBG' ], 3 : [ 'cyanBG' ],
4 : [ 'redBG' ], 4 : [ 'redBG' ],
5 : [ 'magentaBG' ], 5 : [ 'magentaBG' ],
6 : [ 'yellowBG' ], 6 : [ 'yellowBG' ],
7 : [ 'whiteBG' ], 7 : [ 'whiteBG' ],
8 : [ 'bold', 'blackBG' ], 8 : [ 'bold', 'blackBG' ],
9 : [ 'bold', 'blueBG' ], 9 : [ 'bold', 'blueBG' ],
A : [ 'bold', 'greenBG' ], A : [ 'bold', 'greenBG' ],
B : [ 'bold', 'cyanBG' ], B : [ 'bold', 'cyanBG' ],
C : [ 'bold', 'redBG' ], C : [ 'bold', 'redBG' ],
D : [ 'bold', 'magentaBG' ], D : [ 'bold', 'magentaBG' ],
E : [ 'bold', 'yellowBG' ], E : [ 'bold', 'yellowBG' ],
F : [ 'bold', 'whiteBG' ], F : [ 'bold', 'whiteBG' ],
}[v.charAt(1)] || [ 'normal' ]; }[v.charAt(1)] || [ 'normal' ];
v = ansi.sgr(fg.concat(bg)); v = ansi.sgr(fg.concat(bg));
result += s.substr(lastIndex, m.index - lastIndex) + v; result += s.substr(lastIndex, m.index - lastIndex) + v;
break; break;
case '\x03' : case '\x03' :
v = parseInt(m[8], 10); v = parseInt(m[8], 10);
if(isNaN(v)) { if(isNaN(v)) {
v += m[0]; v += m[0];
} else { } else {
v = ansi.sgr({ v = ansi.sgr({
0 : [ 'reset', 'black' ], 0 : [ 'reset', 'black' ],
1 : [ 'bold', 'cyan' ], 1 : [ 'bold', 'cyan' ],
2 : [ 'bold', 'yellow' ], 2 : [ 'bold', 'yellow' ],
3 : [ 'reset', 'magenta' ], 3 : [ 'reset', 'magenta' ],
4 : [ 'bold', 'white', 'blueBG' ], 4 : [ 'bold', 'white', 'blueBG' ],
5 : [ 'reset', 'green' ], 5 : [ 'reset', 'green' ],
6 : [ 'bold', 'blink', 'red' ], 6 : [ 'bold', 'blink', 'red' ],
7 : [ 'bold', 'blue' ], 7 : [ 'bold', 'blue' ],
8 : [ 'reset', 'blue' ], 8 : [ 'reset', 'blue' ],
9 : [ 'reset', 'cyan' ], 9 : [ 'reset', 'cyan' ],
}[v] || 'normal'); }[v] || 'normal');
} }
result += s.substr(lastIndex, m.index - lastIndex) + v; result += s.substr(lastIndex, m.index - lastIndex) + v;
break; break;
} }
lastIndex = RE.lastIndex; lastIndex = RE.lastIndex;
} }
return (0 === result.length ? s : result + s.substr(lastIndex)); return (0 === result.length ? s : result + s.substr(lastIndex));
} }

View File

@ -11,107 +11,107 @@ const _ = require('lodash');
const RLogin = require('rlogin'); const RLogin = require('rlogin');
exports.moduleInfo = { exports.moduleInfo = {
name : 'CombatNet', name : 'CombatNet',
desc : 'CombatNet Access Module', desc : 'CombatNet Access Module',
author : 'Dave Stephens', author : 'Dave Stephens',
}; };
exports.getModule = class CombatNetModule extends MenuModule { exports.getModule = class CombatNetModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
// establish defaults // establish defaults
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
this.config.host = this.config.host || 'bbs.combatnet.us'; this.config.host = this.config.host || 'bbs.combatnet.us';
this.config.rloginPort = this.config.rloginPort || 4513; this.config.rloginPort = this.config.rloginPort || 4513;
} }
initSequence() { initSequence() {
const self = this; const self = this;
async.series( async.series(
[ [
function validateConfig(callback) { function validateConfig(callback) {
if(!_.isString(self.config.password)) { if(!_.isString(self.config.password)) {
return callback(new Error('Config requires "password"!')); return callback(new Error('Config requires "password"!'));
} }
if(!_.isString(self.config.bbsTag)) { if(!_.isString(self.config.bbsTag)) {
return callback(new Error('Config requires "bbsTag"!')); return callback(new Error('Config requires "bbsTag"!'));
} }
return callback(null); return callback(null);
}, },
function establishRloginConnection(callback) { function establishRloginConnection(callback) {
self.client.term.write(resetScreen()); self.client.term.write(resetScreen());
self.client.term.write('Connecting to CombatNet, please wait...\n'); self.client.term.write('Connecting to CombatNet, please wait...\n');
const restorePipeToNormal = function() { const restorePipeToNormal = function() {
if(self.client.term.output) { if(self.client.term.output) {
self.client.term.output.removeListener('data', sendToRloginBuffer); self.client.term.output.removeListener('data', sendToRloginBuffer);
} }
}; };
const rlogin = new RLogin( const rlogin = new RLogin(
{ 'clientUsername' : self.config.password, { 'clientUsername' : self.config.password,
'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`, 'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`,
'host' : self.config.host, 'host' : self.config.host,
'port' : self.config.rloginPort, 'port' : self.config.rloginPort,
'terminalType' : self.client.term.termClient, 'terminalType' : self.client.term.termClient,
'terminalSpeed' : 57600 'terminalSpeed' : 57600
} }
); );
// If there was an error ... // If there was an error ...
rlogin.on('error', err => { rlogin.on('error', err => {
self.client.log.info(`CombatNet rlogin client error: ${err.message}`); self.client.log.info(`CombatNet rlogin client error: ${err.message}`);
restorePipeToNormal(); restorePipeToNormal();
return callback(err); return callback(err);
}); });
// If we've been disconnected ... // If we've been disconnected ...
rlogin.on('disconnect', () => { rlogin.on('disconnect', () => {
self.client.log.info('Disconnected from CombatNet'); self.client.log.info('Disconnected from CombatNet');
restorePipeToNormal(); restorePipeToNormal();
return callback(null); return callback(null);
}); });
function sendToRloginBuffer(buffer) { function sendToRloginBuffer(buffer) {
rlogin.send(buffer); rlogin.send(buffer);
} }
rlogin.on('connect', rlogin.on('connect',
/* The 'connect' event handler will be supplied with one argument, /* The 'connect' event handler will be supplied with one argument,
a boolean indicating whether or not the connection was established. */ a boolean indicating whether or not the connection was established. */
function(state) { function(state) {
if(state) { if(state) {
self.client.log.info('Connected to CombatNet'); self.client.log.info('Connected to CombatNet');
self.client.term.output.on('data', sendToRloginBuffer); self.client.term.output.on('data', sendToRloginBuffer);
} else { } else {
return callback(new Error('Failed to establish establish CombatNet connection')); return callback(new Error('Failed to establish establish CombatNet connection'));
} }
} }
); );
// If data (a Buffer) has been received from the server ... // If data (a Buffer) has been received from the server ...
rlogin.on('data', (data) => { rlogin.on('data', (data) => {
self.client.term.rawWrite(data); self.client.term.rawWrite(data);
}); });
// connect... // connect...
rlogin.connect(); rlogin.connect();
// note: no explicit callback() until we're finished! // note: no explicit callback() until we're finished!
} }
], ],
err => { err => {
if(err) { if(err) {
self.client.log.warn( { error : err.message }, 'CombatNet error'); self.client.log.warn( { error : err.message }, 'CombatNet error');
} }
// if the client is still here, go to previous // if the client is still here, go to previous
self.prevMenu(); self.prevMenu();
} }
); );
} }
}; };

View File

@ -12,19 +12,19 @@ exports.sortAreasOrConfs = sortAreasOrConfs;
// Otherwise, use a locale comparison on the sort key or name as a fallback // Otherwise, use a locale comparison on the sort key or name as a fallback
// //
function sortAreasOrConfs(areasOrConfs, type) { function sortAreasOrConfs(areasOrConfs, type) {
let entryA; let entryA;
let entryB; let entryB;
areasOrConfs.sort((a, b) => { areasOrConfs.sort((a, b) => {
entryA = type ? a[type] : a; entryA = type ? a[type] : a;
entryB = type ? b[type] : b; entryB = type ? b[type] : b;
if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) { if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) {
return entryA.sort - entryB.sort; return entryA.sort - entryB.sort;
} else { } else {
const keyA = entryA.sort ? entryA.sort.toString() : entryA.name; const keyA = entryA.sort ? entryA.sort.toString() : entryA.name;
const keyB = entryB.sort ? entryB.sort.toString() : entryB.name; const keyB = entryB.sort ? entryB.sort.toString() : entryB.name;
return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare
} }
}); });
} }

File diff suppressed because it is too large Load Diff

View File

@ -9,64 +9,64 @@ const sane = require('sane');
module.exports = new class ConfigCache module.exports = new class ConfigCache
{ {
constructor() { constructor() {
this.cache = new Map(); // path->parsed config this.cache = new Map(); // path->parsed config
} }
getConfigWithOptions(options, cb) { getConfigWithOptions(options, cb) {
const cached = this.cache.has(options.filePath); const cached = this.cache.has(options.filePath);
if(options.forceReCache || !cached) { if(options.forceReCache || !cached) {
this.recacheConfigFromFile(options.filePath, (err, config) => { this.recacheConfigFromFile(options.filePath, (err, config) => {
if(!err && !cached) { if(!err && !cached) {
if(!options.noWatch) { if(!options.noWatch) {
const watcher = sane( const watcher = sane(
paths.dirname(options.filePath), paths.dirname(options.filePath),
{ {
glob : `**/${paths.basename(options.filePath)}` glob : `**/${paths.basename(options.filePath)}`
} }
); );
watcher.on('change', (fileName, fileRoot) => { watcher.on('change', (fileName, fileRoot) => {
require('./logger.js').log.info( { fileName, fileRoot }, 'Configuration file changed; re-caching'); require('./logger.js').log.info( { fileName, fileRoot }, 'Configuration file changed; re-caching');
this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => { this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => {
if(!err) { if(!err) {
if(options.callback) { if(options.callback) {
options.callback( { fileName, fileRoot } ); options.callback( { fileName, fileRoot } );
} }
} }
}); });
}); });
} }
} }
return cb(err, config, true); return cb(err, config, true);
}); });
} else { } else {
return cb(null, this.cache.get(options.filePath), false); return cb(null, this.cache.get(options.filePath), false);
} }
} }
getConfig(filePath, cb) { getConfig(filePath, cb) {
return this.getConfigWithOptions( { filePath }, cb); return this.getConfigWithOptions( { filePath }, cb);
} }
recacheConfigFromFile(path, cb) { recacheConfigFromFile(path, cb) {
fs.readFile(path, { encoding : 'utf-8' }, (err, data) => { fs.readFile(path, { encoding : 'utf-8' }, (err, data) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
let parsed; let parsed;
try { try {
parsed = hjson.parse(data); parsed = hjson.parse(data);
this.cache.set(path, parsed); this.cache.set(path, parsed);
} catch(e) { } catch(e) {
require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' ); require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' );
return cb(e); return cb(e);
} }
return cb(null, parsed); return cb(null, parsed);
}); });
} }
}; };

View File

@ -13,54 +13,54 @@ exports.init = init;
exports.getFullConfig = getFullConfig; exports.getFullConfig = getFullConfig;
function getConfigPath(filePath) { function getConfigPath(filePath) {
// |filePath| is assumed to be in the config path if it's only a file name // |filePath| is assumed to be in the config path if it's only a file name
if('.' === paths.dirname(filePath)) { if('.' === paths.dirname(filePath)) {
filePath = paths.join(Config().paths.config, filePath); filePath = paths.join(Config().paths.config, filePath);
} }
return filePath; return filePath;
} }
function init(cb) { function init(cb) {
// pre-cache menu.hjson and prompt.hjson + establish events // pre-cache menu.hjson and prompt.hjson + establish events
const changed = ( { fileName, fileRoot } ) => { const changed = ( { fileName, fileRoot } ) => {
const reCachedPath = paths.join(fileRoot, fileName); const reCachedPath = paths.join(fileRoot, fileName);
if(reCachedPath === getConfigPath(Config().general.menuFile)) { if(reCachedPath === getConfigPath(Config().general.menuFile)) {
Events.emit(Events.getSystemEvents().MenusChanged); Events.emit(Events.getSystemEvents().MenusChanged);
} else if(reCachedPath === getConfigPath(Config().general.promptFile)) { } else if(reCachedPath === getConfigPath(Config().general.promptFile)) {
Events.emit(Events.getSystemEvents().PromptsChanged); Events.emit(Events.getSystemEvents().PromptsChanged);
} }
}; };
const config = Config(); const config = Config();
async.series( async.series(
[ [
function menu(callback) { function menu(callback) {
return ConfigCache.getConfigWithOptions( return ConfigCache.getConfigWithOptions(
{ {
filePath : getConfigPath(config.general.menuFile), filePath : getConfigPath(config.general.menuFile),
callback : changed, callback : changed,
}, },
callback callback
); );
}, },
function prompt(callback) { function prompt(callback) {
return ConfigCache.getConfigWithOptions( return ConfigCache.getConfigWithOptions(
{ {
filePath : getConfigPath(config.general.promptFile), filePath : getConfigPath(config.general.promptFile),
callback : changed, callback : changed,
}, },
callback callback
); );
} }
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
} }
function getFullConfig(filePath, cb) { function getFullConfig(filePath, cb) {
ConfigCache.getConfig(getConfigPath(filePath), (err, config) => { ConfigCache.getConfig(getConfigPath(filePath), (err, config) => {
return cb(err, config); return cb(err, config);
}); });
} }

View File

@ -11,177 +11,177 @@ const async = require('async');
exports.connectEntry = connectEntry; exports.connectEntry = connectEntry;
function ansiDiscoverHomePosition(client, cb) { function ansiDiscoverHomePosition(client, cb) {
// //
// We want to find the home position. ANSI-BBS and most terminals // We want to find the home position. ANSI-BBS and most terminals
// utilize 1,1 as home. However, some terminals such as ConnectBot // utilize 1,1 as home. However, some terminals such as ConnectBot
// think of home as 0,0. If this is the case, we need to offset // think of home as 0,0. If this is the case, we need to offset
// our positioning to accomodate for such. // our positioning to accomodate for such.
// //
const done = function(err) { const done = function(err) {
client.removeListener('cursor position report', cprListener); client.removeListener('cursor position report', cprListener);
clearTimeout(giveUpTimer); clearTimeout(giveUpTimer);
return cb(err); return cb(err);
}; };
const cprListener = function(pos) { const cprListener = function(pos) {
const h = pos[0]; const h = pos[0];
const w = pos[1]; const w = pos[1];
// //
// We expect either 0,0, or 1,1. Anything else will be filed as bad data // We expect either 0,0, or 1,1. Anything else will be filed as bad data
// //
if(h > 1 || w > 1) { if(h > 1 || w > 1) {
client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values'); client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values');
return done(new Error('Home position CPR expected to be 0,0, or 1,1')); return done(new Error('Home position CPR expected to be 0,0, or 1,1'));
} }
if(0 === h & 0 === w) { if(0 === h & 0 === w) {
// //
// Store a CPR offset in the client. All CPR's from this point on will offset by this amount // Store a CPR offset in the client. All CPR's from this point on will offset by this amount
// //
client.log.info('Setting CPR offset to 1'); client.log.info('Setting CPR offset to 1');
client.cprOffset = 1; client.cprOffset = 1;
} }
return done(null); return done(null);
}; };
client.once('cursor position report', cprListener); client.once('cursor position report', cprListener);
const giveUpTimer = setTimeout( () => { const giveUpTimer = setTimeout( () => {
return done(new Error('Giving up on home position CPR')); return done(new Error('Giving up on home position CPR'));
}, 3000); // 3s }, 3000); // 3s
client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos
} }
function ansiQueryTermSizeIfNeeded(client, cb) { function ansiQueryTermSizeIfNeeded(client, cb) {
if(client.term.termHeight > 0 || client.term.termWidth > 0) { if(client.term.termHeight > 0 || client.term.termWidth > 0) {
return cb(null); return cb(null);
} }
const done = function(err) { const done = function(err) {
client.removeListener('cursor position report', cprListener); client.removeListener('cursor position report', cprListener);
clearTimeout(giveUpTimer); clearTimeout(giveUpTimer);
return cb(err); return cb(err);
}; };
const cprListener = function(pos) { const cprListener = function(pos) {
// //
// If we've already found out, disregard // If we've already found out, disregard
// //
if(client.term.termHeight > 0 || client.term.termWidth > 0) { if(client.term.termHeight > 0 || client.term.termWidth > 0) {
return done(null); return done(null);
} }
const h = pos[0]; const h = pos[0];
const w = pos[1]; const w = pos[1];
// //
// Netrunner for example gives us 1x1 here. Not really useful. Ignore // Netrunner for example gives us 1x1 here. Not really useful. Ignore
// values that seem obviously bad. // values that seem obviously bad.
// //
if(h < 10 || w < 10) { if(h < 10 || w < 10) {
client.log.warn( client.log.warn(
{ height : h, width : w }, { height : h, width : w },
'Ignoring ANSI CPR screen size query response due to very small values'); 'Ignoring ANSI CPR screen size query response due to very small values');
return done(new Error('Term size <= 10 considered invalid')); return done(new Error('Term size <= 10 considered invalid'));
} }
client.term.termHeight = h; client.term.termHeight = h;
client.term.termWidth = w; client.term.termWidth = w;
client.log.debug( client.log.debug(
{ {
termWidth : client.term.termWidth, termWidth : client.term.termWidth,
termHeight : client.term.termHeight, termHeight : client.term.termHeight,
source : 'ANSI CPR' source : 'ANSI CPR'
}, },
'Window size updated' 'Window size updated'
); );
return done(null); return done(null);
}; };
client.once('cursor position report', cprListener); client.once('cursor position report', cprListener);
// give up after 2s // give up after 2s
const giveUpTimer = setTimeout( () => { const giveUpTimer = setTimeout( () => {
return done(new Error('No term size established by CPR within timeout')); return done(new Error('No term size established by CPR within timeout'));
}, 2000); }, 2000);
// Start the process: Query for CPR // Start the process: Query for CPR
client.term.rawWrite(ansi.queryScreenSize()); client.term.rawWrite(ansi.queryScreenSize());
} }
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
} }
function displayBanner(term) { function displayBanner(term) {
// note: intentional formatting: // note: intentional formatting:
term.pipeWrite(` term.pipeWrite(`
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN |06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|06Copyright (c) 2014-2018 Bryan Ashby |14- |12http://l33t.codes/ |06Copyright (c) 2014-2018 Bryan Ashby |14- |12http://l33t.codes/
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/ |06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|00` |00`
); );
} }
function connectEntry(client, nextMenu) { function connectEntry(client, nextMenu) {
const term = client.term; const term = client.term;
async.series( async.series(
[ [
function basicPrepWork(callback) { function basicPrepWork(callback) {
term.rawWrite(ansi.queryDeviceAttributes(0)); term.rawWrite(ansi.queryDeviceAttributes(0));
return callback(null); return callback(null);
}, },
function discoverHomePosition(callback) { function discoverHomePosition(callback) {
ansiDiscoverHomePosition(client, () => { ansiDiscoverHomePosition(client, () => {
// :TODO: If CPR for home fully fails, we should bail out on the connection with an error, e.g. ANSI support required // :TODO: If CPR for home fully fails, we should bail out on the connection with an error, e.g. ANSI support required
return callback(null); // we try to continue anyway return callback(null); // we try to continue anyway
}); });
}, },
function queryTermSizeByNonStandardAnsi(callback) { function queryTermSizeByNonStandardAnsi(callback) {
ansiQueryTermSizeIfNeeded(client, err => { ansiQueryTermSizeIfNeeded(client, err => {
if(err) { if(err) {
// //
// Check again; We may have got via NAWS/similar before CPR completed. // Check again; We may have got via NAWS/similar before CPR completed.
// //
if(0 === term.termHeight || 0 === term.termWidth) { if(0 === term.termHeight || 0 === term.termWidth) {
// //
// We still don't have something good for term height/width. // We still don't have something good for term height/width.
// Default to DOS size 80x25. // Default to DOS size 80x25.
// //
// :TODO: Netrunner is currenting hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing??? // :TODO: Netrunner is currenting hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing???
client.log.warn( { reason : err.message }, 'Failed to negotiate term size; Defaulting to 80x25!'); client.log.warn( { reason : err.message }, 'Failed to negotiate term size; Defaulting to 80x25!');
term.termHeight = 25; term.termHeight = 25;
term.termWidth = 80; term.termWidth = 80;
} }
} }
return callback(null); return callback(null);
}); });
}, },
], ],
() => { () => {
prepareTerminal(term); prepareTerminal(term);
// //
// Always show an ENiGMA½ banner // Always show an ENiGMA½ banner
// //
displayBanner(term); displayBanner(term);
// fire event // fire event
Events.emit(Events.getSystemEvents().TermDetected, { client : client } ); Events.emit(Events.getSystemEvents().TermDetected, { client : client } );
setTimeout( () => { setTimeout( () => {
return client.menuStack.goto(nextMenu); return client.menuStack.goto(nextMenu);
}, 500); }, 500);
} }
); );
} }

View File

@ -2,90 +2,90 @@
'use strict'; 'use strict';
const CRC32_TABLE = new Int32Array([ const CRC32_TABLE = new Int32Array([
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535,
0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd,
0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d,
0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4,
0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac,
0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab,
0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb,
0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea,
0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce,
0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409,
0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739,
0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268,
0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0,
0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8,
0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703,
0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7,
0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae,
0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6,
0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d,
0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5,
0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
]); ]);
exports.CRC32 = class CRC32 { exports.CRC32 = class CRC32 {
constructor() { constructor() {
this.crc = -1; this.crc = -1;
} }
update(input) { update(input) {
input = Buffer.isBuffer(input) ? input : Buffer.from(input, 'binary'); input = Buffer.isBuffer(input) ? input : Buffer.from(input, 'binary');
return input.length > 10240 ? this.update_8(input) : this.update_4(input); return input.length > 10240 ? this.update_8(input) : this.update_4(input);
} }
update_4(input) { update_4(input) {
const len = input.length - 3; const len = input.length - 3;
let i = 0; let i = 0;
for(i = 0; i < len;) { for(i = 0; i < len;) {
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
} }
while(i < len + 3) { while(i < len + 3) {
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ];
} }
} }
update_8(input) { update_8(input) {
const len = input.length - 7; const len = input.length - 7;
let i = 0; let i = 0;
for(i = 0; i < len;) { for(i = 0; i < len;) {
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
} }
while(i < len + 7) { while(i < len + 7) {
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ];
} }
} }
finalize() { finalize() {
return (this.crc ^ (-1)) >>> 0; return (this.crc ^ (-1)) >>> 0;
} }
}; };

View File

@ -25,98 +25,98 @@ exports.initializeDatabases = initializeDatabases;
exports.dbs = dbs; exports.dbs = dbs;
function getTransactionDatabase(db) { function getTransactionDatabase(db) {
return sqlite3Trans.wrap(db); return sqlite3Trans.wrap(db);
} }
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) { function getModDatabasePath(moduleInfo, suffix) {
// //
// Mods that use a database are stored in Config.paths.modsDb (e.g. enigma-bbs/db/mods) // 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 // We expect that moduleInfo defines packageName which will be the base of the modules
// filename. An optional suffix may be supplied as well. // 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])$/; 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(_.isObject(moduleInfo));
assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!'); assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!');
let full = moduleInfo.packageName; let full = moduleInfo.packageName;
if(suffix) { if(suffix) {
full += `.${suffix}`; full += `.${suffix}`;
} }
assert( assert(
(full.split('.').length > 1 && HOST_RE.test(full)), (full.split('.').length > 1 && HOST_RE.test(full)),
'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation'); '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`); return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`);
} }
function getISOTimestampString(ts) { function getISOTimestampString(ts) {
ts = ts || moment(); ts = ts || moment();
return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ');
} }
function sanatizeString(s) { function sanatizeString(s) {
return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex
switch (c) { switch (c) {
case '\0' : return '\\0'; case '\0' : return '\\0';
case '\x08' : return '\\b'; case '\x08' : return '\\b';
case '\x09' : return '\\t'; case '\x09' : return '\\t';
case '\x1a' : return '\\z'; case '\x1a' : return '\\z';
case '\n' : return '\\n'; case '\n' : return '\\n';
case '\r' : return '\\r'; case '\r' : return '\\r';
case '"' : case '"' :
case '\'' : case '\'' :
return `${c}${c}`; return `${c}${c}`;
case '\\' : case '\\' :
case '%' : case '%' :
return `\\${c}`; return `\\${c}`;
} }
}); });
} }
function initializeDatabases(cb) { function initializeDatabases(cb) {
async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => {
dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => { dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
dbs[dbName].serialize( () => { dbs[dbName].serialize( () => {
DB_INIT_TABLE[dbName]( () => { DB_INIT_TABLE[dbName]( () => {
return next(null); return next(null);
}); });
}); });
})); }));
}, err => { }, err => {
return cb(err); return cb(err);
}); });
} }
function enableForeignKeys(db) { function enableForeignKeys(db) {
db.run('PRAGMA foreign_keys = ON;'); db.run('PRAGMA foreign_keys = ON;');
} }
const DB_INIT_TABLE = { const DB_INIT_TABLE = {
system : (cb) => { system : (cb) => {
enableForeignKeys(dbs.system); enableForeignKeys(dbs.system);
// Various stat/event logging - see stat_log.js // Various stat/event logging - see stat_log.js
dbs.system.run( dbs.system.run(
`CREATE TABLE IF NOT EXISTS system_stat ( `CREATE TABLE IF NOT EXISTS system_stat (
stat_name VARCHAR PRIMARY KEY NOT NULL, stat_name VARCHAR PRIMARY KEY NOT NULL,
stat_value VARCHAR NOT NULL stat_value VARCHAR NOT NULL
);` );`
); );
dbs.system.run( dbs.system.run(
`CREATE TABLE IF NOT EXISTS system_event_log ( `CREATE TABLE IF NOT EXISTS system_event_log (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
timestamp DATETIME NOT NULL, timestamp DATETIME NOT NULL,
log_name VARCHAR NOT NULL, log_name VARCHAR NOT NULL,
@ -124,10 +124,10 @@ const DB_INIT_TABLE = {
UNIQUE(timestamp, log_name) UNIQUE(timestamp, log_name)
);` );`
); );
dbs.system.run( dbs.system.run(
`CREATE TABLE IF NOT EXISTS user_event_log ( `CREATE TABLE IF NOT EXISTS user_event_log (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
timestamp DATETIME NOT NULL, timestamp DATETIME NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
@ -136,58 +136,58 @@ const DB_INIT_TABLE = {
UNIQUE(timestamp, user_id, log_name) UNIQUE(timestamp, user_id, log_name)
);` );`
); );
return cb(null); return cb(null);
}, },
user : (cb) => { user : (cb) => {
enableForeignKeys(dbs.user); enableForeignKeys(dbs.user);
dbs.user.run( dbs.user.run(
`CREATE TABLE IF NOT EXISTS user ( `CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
user_name VARCHAR NOT NULL, user_name VARCHAR NOT NULL,
UNIQUE(user_name) UNIQUE(user_name)
);` );`
); );
// :TODO: create FK on delete/etc. // :TODO: create FK on delete/etc.
dbs.user.run( dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_property ( `CREATE TABLE IF NOT EXISTS user_property (
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
prop_name VARCHAR NOT NULL, prop_name VARCHAR NOT NULL,
prop_value VARCHAR, prop_value VARCHAR,
UNIQUE(user_id, prop_name), UNIQUE(user_id, prop_name),
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
);` );`
); );
dbs.user.run( dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_group_member ( `CREATE TABLE IF NOT EXISTS user_group_member (
group_name VARCHAR NOT NULL, group_name VARCHAR NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
UNIQUE(group_name, user_id) UNIQUE(group_name, user_id)
);` );`
); );
dbs.user.run( dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_login_history ( `CREATE TABLE IF NOT EXISTS user_login_history (
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
user_name VARCHAR NOT NULL, user_name VARCHAR NOT NULL,
timestamp DATETIME NOT NULL timestamp DATETIME NOT NULL
);` );`
); );
return cb(null); return cb(null);
}, },
message : (cb) => { message : (cb) => {
enableForeignKeys(dbs.message); enableForeignKeys(dbs.message);
dbs.message.run( dbs.message.run(
`CREATE TABLE IF NOT EXISTS message ( `CREATE TABLE IF NOT EXISTS message (
message_id INTEGER PRIMARY KEY, message_id INTEGER PRIMARY KEY,
area_tag VARCHAR NOT NULL, area_tag VARCHAR NOT NULL,
message_uuid VARCHAR(36) NOT NULL, message_uuid VARCHAR(36) NOT NULL,
@ -200,47 +200,47 @@ const DB_INIT_TABLE = {
view_count INTEGER NOT NULL DEFAULT 0, view_count INTEGER NOT NULL DEFAULT 0,
UNIQUE(message_uuid) UNIQUE(message_uuid)
);` );`
); );
dbs.message.run( dbs.message.run(
`CREATE INDEX IF NOT EXISTS message_by_area_tag_index `CREATE INDEX IF NOT EXISTS message_by_area_tag_index
ON message (area_tag);` ON message (area_tag);`
); );
dbs.message.run( dbs.message.run(
`CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 ( `CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 (
content="message", content="message",
subject, subject,
message message
);` );`
); );
dbs.message.run( dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN `CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN
DELETE FROM message_fts WHERE docid=old.rowid; DELETE FROM message_fts WHERE docid=old.rowid;
END;` END;`
); );
dbs.message.run( dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN `CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN
DELETE FROM message_fts WHERE docid=old.rowid; DELETE FROM message_fts WHERE docid=old.rowid;
END;` END;`
); );
dbs.message.run( dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN `CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN
INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);
END;` END;`
); );
dbs.message.run( dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN `CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN
INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);
END;` END;`
); );
dbs.message.run( dbs.message.run(
`CREATE TABLE IF NOT EXISTS message_meta ( `CREATE TABLE IF NOT EXISTS message_meta (
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
meta_category INTEGER NOT NULL, meta_category INTEGER NOT NULL,
meta_name VARCHAR NOT NULL, meta_name VARCHAR NOT NULL,
@ -248,11 +248,11 @@ const DB_INIT_TABLE = {
UNIQUE(message_id, meta_category, meta_name, meta_value), UNIQUE(message_id, meta_category, meta_name, meta_value),
FOREIGN KEY(message_id) REFERENCES message(message_id) ON DELETE CASCADE FOREIGN KEY(message_id) REFERENCES message(message_id) ON DELETE CASCADE
);` );`
); );
// :TODO: need SQL to ensure cleaned up if delete from message? // :TODO: need SQL to ensure cleaned up if delete from message?
/* /*
dbs.message.run( dbs.message.run(
`CREATE TABLE IF NOT EXISTS hash_tag ( `CREATE TABLE IF NOT EXISTS hash_tag (
hash_tag_id INTEGER PRIMARY KEY, hash_tag_id INTEGER PRIMARY KEY,
@ -270,33 +270,33 @@ const DB_INIT_TABLE = {
); );
*/ */
dbs.message.run( dbs.message.run(
`CREATE TABLE IF NOT EXISTS user_message_area_last_read ( `CREATE TABLE IF NOT EXISTS user_message_area_last_read (
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
area_tag VARCHAR NOT NULL, area_tag VARCHAR NOT NULL,
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
UNIQUE(user_id, area_tag) UNIQUE(user_id, area_tag)
);` );`
); );
dbs.message.run( dbs.message.run(
`CREATE TABLE IF NOT EXISTS message_area_last_scan ( `CREATE TABLE IF NOT EXISTS message_area_last_scan (
scan_toss VARCHAR NOT NULL, scan_toss VARCHAR NOT NULL,
area_tag VARCHAR NOT NULL, area_tag VARCHAR NOT NULL,
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
UNIQUE(scan_toss, area_tag) UNIQUE(scan_toss, area_tag)
);` );`
); );
return cb(null); return cb(null);
}, },
file : (cb) => { file : (cb) => {
enableForeignKeys(dbs.file); enableForeignKeys(dbs.file);
dbs.file.run( dbs.file.run(
// :TODO: should any of this be unique -- file_sha256 unless dupes are allowed on the system // :TODO: should any of this be unique -- file_sha256 unless dupes are allowed on the system
`CREATE TABLE IF NOT EXISTS file ( `CREATE TABLE IF NOT EXISTS file (
file_id INTEGER PRIMARY KEY, file_id INTEGER PRIMARY KEY,
area_tag VARCHAR NOT NULL, area_tag VARCHAR NOT NULL,
file_sha256 VARCHAR NOT NULL, file_sha256 VARCHAR NOT NULL,
@ -306,105 +306,105 @@ const DB_INIT_TABLE = {
desc_long, /* FTS @ file_fts */ desc_long, /* FTS @ file_fts */
upload_timestamp DATETIME NOT NULL upload_timestamp DATETIME NOT NULL
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE INDEX IF NOT EXISTS file_by_area_tag_index `CREATE INDEX IF NOT EXISTS file_by_area_tag_index
ON file (area_tag);` ON file (area_tag);`
); );
dbs.file.run( dbs.file.run(
`CREATE INDEX IF NOT EXISTS file_by_sha256_index `CREATE INDEX IF NOT EXISTS file_by_sha256_index
ON file (file_sha256);` ON file (file_sha256);`
); );
dbs.file.run( dbs.file.run(
`CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts4 ( `CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts4 (
content="file", content="file",
file_name, file_name,
desc, desc,
desc_long desc_long
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN `CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN
DELETE FROM file_fts WHERE docid=old.rowid; DELETE FROM file_fts WHERE docid=old.rowid;
END;` END;`
); );
dbs.file.run( dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN `CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN
DELETE FROM file_fts WHERE docid=old.rowid; DELETE FROM file_fts WHERE docid=old.rowid;
END;` END;`
); );
dbs.file.run( dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN `CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN
INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long); INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long);
END;` END;`
); );
dbs.file.run( dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN `CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN
INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long); INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long);
END;` END;`
); );
dbs.file.run( dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_meta ( `CREATE TABLE IF NOT EXISTS file_meta (
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,
meta_name VARCHAR NOT NULL, meta_name VARCHAR NOT NULL,
meta_value VARCHAR NOT NULL, meta_value VARCHAR NOT NULL,
UNIQUE(file_id, meta_name, meta_value), UNIQUE(file_id, meta_name, meta_value),
FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE TABLE IF NOT EXISTS hash_tag ( `CREATE TABLE IF NOT EXISTS hash_tag (
hash_tag_id INTEGER PRIMARY KEY, hash_tag_id INTEGER PRIMARY KEY,
hash_tag VARCHAR NOT NULL, hash_tag VARCHAR NOT NULL,
UNIQUE(hash_tag) UNIQUE(hash_tag)
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_hash_tag ( `CREATE TABLE IF NOT EXISTS file_hash_tag (
hash_tag_id INTEGER NOT NULL, hash_tag_id INTEGER NOT NULL,
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,
UNIQUE(hash_tag_id, file_id) UNIQUE(hash_tag_id, file_id)
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_user_rating ( `CREATE TABLE IF NOT EXISTS file_user_rating (
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
rating INTEGER NOT NULL, rating INTEGER NOT NULL,
UNIQUE(file_id, user_id) UNIQUE(file_id, user_id)
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_web_serve ( `CREATE TABLE IF NOT EXISTS file_web_serve (
hash_id VARCHAR NOT NULL PRIMARY KEY, hash_id VARCHAR NOT NULL PRIMARY KEY,
expire_timestamp DATETIME NOT NULL expire_timestamp DATETIME NOT NULL
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_web_serve_batch ( `CREATE TABLE IF NOT EXISTS file_web_serve_batch (
hash_id VARCHAR NOT NULL, hash_id VARCHAR NOT NULL,
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,
UNIQUE(hash_id, file_id) UNIQUE(hash_id, file_id)
);` );`
); );
return cb(null); return cb(null);
} }
}; };

View File

@ -7,66 +7,66 @@ const iconv = require('iconv-lite');
const async = require('async'); const async = require('async');
module.exports = class DescriptIonFile { module.exports = class DescriptIonFile {
constructor() { constructor() {
this.entries = new Map(); this.entries = new Map();
} }
get(fileName) { get(fileName) {
return this.entries.get(fileName); return this.entries.get(fileName);
} }
getDescription(fileName) { getDescription(fileName) {
const entry = this.get(fileName); const entry = this.get(fileName);
if(entry) { if(entry) {
return entry.desc; return entry.desc;
} }
} }
static createFromFile(path, cb) { static createFromFile(path, cb) {
fs.readFile(path, (err, descData) => { fs.readFile(path, (err, descData) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const descIonFile = new DescriptIonFile(); const descIonFile = new DescriptIonFile();
// DESCRIPT.ION entries are terminated with a CR and/or LF // DESCRIPT.ION entries are terminated with a CR and/or LF
const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g); const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g);
async.each(lines, (entryData, nextLine) => { async.each(lines, (entryData, nextLine) => {
// //
// We allow quoted (long) filenames or non-quoted filenames. // We allow quoted (long) filenames or non-quoted filenames.
// FILENAME<SPC>DESC<0x04><program data><CR/LF> // FILENAME<SPC>DESC<0x04><program data><CR/LF>
// //
const parts = entryData.match(/^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/); // eslint-disable-line no-control-regex const parts = entryData.match(/^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/); // eslint-disable-line no-control-regex
if(!parts) { if(!parts) {
return nextLine(null); return nextLine(null);
} }
const fileName = parts[1] || parts[2]; const fileName = parts[1] || parts[2];
// //
// Un-escape CR/LF's // Un-escape CR/LF's
// - escapped \r and/or \n // - escapped \r and/or \n
// - BBBS style @n - See https://www.bbbs.net/sysop.html // - BBBS style @n - See https://www.bbbs.net/sysop.html
// //
const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n'); const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n');
descIonFile.entries.set( descIonFile.entries.set(
fileName, fileName,
{ {
desc : desc, desc : desc,
programId : parts[4], programId : parts[4],
programData : parts[5], programData : parts[5],
} }
); );
return nextLine(null); return nextLine(null);
}, },
() => { () => {
return cb(null, descIonFile); return cb(null, descIonFile);
}); });
}); });
} }
}; };

View File

@ -13,137 +13,137 @@ const createServer = require('net').createServer;
exports.Door = Door; exports.Door = Door;
function Door(client, exeInfo) { function Door(client, exeInfo) {
events.EventEmitter.call(this); events.EventEmitter.call(this);
const self = this; const self = this;
this.client = client; this.client = client;
this.exeInfo = exeInfo; this.exeInfo = exeInfo;
this.exeInfo.encoding = (this.exeInfo.encoding || 'cp437').toLowerCase(); this.exeInfo.encoding = (this.exeInfo.encoding || 'cp437').toLowerCase();
let restored = false; let restored = false;
// //
// Members of exeInfo: // Members of exeInfo:
// cmd // cmd
// args[] // args[]
// env{} // env{}
// cwd // cwd
// io // io
// encoding // encoding
// dropFile // dropFile
// node // node
// inhSocket // inhSocket
// //
this.doorDataHandler = function(data) { this.doorDataHandler = function(data) {
self.client.term.write(decode(data, self.exeInfo.encoding)); self.client.term.write(decode(data, self.exeInfo.encoding));
}; };
this.restoreIo = function(piped) { this.restoreIo = function(piped) {
if(!restored && self.client.term.output) { if(!restored && self.client.term.output) {
self.client.term.output.unpipe(piped); self.client.term.output.unpipe(piped);
self.client.term.output.resume(); self.client.term.output.resume();
restored = true; restored = true;
} }
}; };
this.prepareSocketIoServer = function(cb) { this.prepareSocketIoServer = function(cb) {
if('socket' === self.exeInfo.io) { if('socket' === self.exeInfo.io) {
const sockServer = createServer(conn => { const sockServer = createServer(conn => {
sockServer.getConnections( (err, count) => { sockServer.getConnections( (err, count) => {
// We expect only one connection from our DOOR/emulator/etc. // We expect only one connection from our DOOR/emulator/etc.
if(!err && count <= 1) { if(!err && count <= 1) {
self.client.term.output.pipe(conn); self.client.term.output.pipe(conn);
conn.on('data', self.doorDataHandler); conn.on('data', self.doorDataHandler);
conn.once('end', () => { conn.once('end', () => {
return self.restoreIo(conn); return self.restoreIo(conn);
}); });
conn.once('error', err => { conn.once('error', err => {
self.client.log.info( { error : err.toString() }, 'Door socket server connection'); self.client.log.info( { error : err.toString() }, 'Door socket server connection');
return self.restoreIo(conn); return self.restoreIo(conn);
}); });
} }
}); });
}); });
sockServer.listen(0, () => { sockServer.listen(0, () => {
return cb(null, sockServer); return cb(null, sockServer);
}); });
} else { } else {
return cb(null); return cb(null);
} }
}; };
this.doorExited = function() { this.doorExited = function() {
self.emit('finished'); self.emit('finished');
}; };
} }
require('util').inherits(Door, events.EventEmitter); require('util').inherits(Door, events.EventEmitter);
Door.prototype.run = function() { Door.prototype.run = function() {
const self = this; const self = this;
this.prepareSocketIoServer( (err, sockServer) => { this.prepareSocketIoServer( (err, sockServer) => {
if(err) { if(err) {
this.client.log.warn( { error : err.toString() }, 'Failed executing door'); this.client.log.warn( { error : err.toString() }, 'Failed executing door');
return self.doorExited(); return self.doorExited();
} }
// Expand arg strings, e.g. {dropFile} -> DOOR32.SYS // Expand arg strings, e.g. {dropFile} -> DOOR32.SYS
// :TODO: Use .map() here // :TODO: Use .map() here
let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified
for(let i = 0; i < args.length; ++i) { for(let i = 0; i < args.length; ++i) {
args[i] = stringFormat(self.exeInfo.args[i], { args[i] = stringFormat(self.exeInfo.args[i], {
dropFile : self.exeInfo.dropFile, dropFile : self.exeInfo.dropFile,
node : self.exeInfo.node.toString(), node : self.exeInfo.node.toString(),
srvPort : sockServer ? sockServer.address().port.toString() : '-1', srvPort : sockServer ? sockServer.address().port.toString() : '-1',
userId : self.client.user.userId.toString(), userId : self.client.user.userId.toString(),
}); });
} }
const door = pty.spawn(self.exeInfo.cmd, args, { const door = pty.spawn(self.exeInfo.cmd, args, {
cols : self.client.term.termWidth, cols : self.client.term.termWidth,
rows : self.client.term.termHeight, rows : self.client.term.termHeight,
// :TODO: cwd // :TODO: cwd
env : self.exeInfo.env, env : self.exeInfo.env,
encoding : null, // we want to handle all encoding ourself encoding : null, // we want to handle all encoding ourself
}); });
if('stdio' === self.exeInfo.io) { if('stdio' === self.exeInfo.io) {
self.client.log.debug('Using stdio for door I/O'); self.client.log.debug('Using stdio for door I/O');
self.client.term.output.pipe(door); self.client.term.output.pipe(door);
door.on('data', self.doorDataHandler); door.on('data', self.doorDataHandler);
door.once('close', () => { door.once('close', () => {
return self.restoreIo(door); return self.restoreIo(door);
}); });
} else if('socket' === self.exeInfo.io) { } else if('socket' === self.exeInfo.io) {
self.client.log.debug( { port : sockServer.address().port }, 'Using temporary socket server for door I/O'); self.client.log.debug( { port : sockServer.address().port }, 'Using temporary socket server for door I/O');
} }
door.once('exit', exitCode => { door.once('exit', exitCode => {
self.client.log.info( { exitCode : exitCode }, 'Door exited'); self.client.log.info( { exitCode : exitCode }, 'Door exited');
if(sockServer) { if(sockServer) {
sockServer.close(); sockServer.close();
} }
// we may not get a close // we may not get a close
if('stdio' === self.exeInfo.io) { if('stdio' === self.exeInfo.io) {
self.restoreIo(door); self.restoreIo(door);
} }
door.removeAllListeners(); door.removeAllListeners();
return self.doorExited(); return self.doorExited();
}); });
}); });
}; };

View File

@ -11,121 +11,121 @@ const _ = require('lodash');
const SSHClient = require('ssh2').Client; const SSHClient = require('ssh2').Client;
exports.moduleInfo = { exports.moduleInfo = {
name : 'DoorParty', name : 'DoorParty',
desc : 'DoorParty Access Module', desc : 'DoorParty Access Module',
author : 'NuSkooler', author : 'NuSkooler',
}; };
exports.getModule = class DoorPartyModule extends MenuModule { exports.getModule = class DoorPartyModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
// establish defaults // establish defaults
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
this.config.host = this.config.host || 'dp.throwbackbbs.com'; this.config.host = this.config.host || 'dp.throwbackbbs.com';
this.config.sshPort = this.config.sshPort || 2022; this.config.sshPort = this.config.sshPort || 2022;
this.config.rloginPort = this.config.rloginPort || 513; this.config.rloginPort = this.config.rloginPort || 513;
} }
initSequence() { initSequence() {
let clientTerminated; let clientTerminated;
const self = this; const self = this;
async.series( async.series(
[ [
function validateConfig(callback) { function validateConfig(callback) {
if(!_.isString(self.config.username)) { if(!_.isString(self.config.username)) {
return callback(new Error('Config requires "username"!')); return callback(new Error('Config requires "username"!'));
} }
if(!_.isString(self.config.password)) { if(!_.isString(self.config.password)) {
return callback(new Error('Config requires "password"!')); return callback(new Error('Config requires "password"!'));
} }
if(!_.isString(self.config.bbsTag)) { if(!_.isString(self.config.bbsTag)) {
return callback(new Error('Config requires "bbsTag"!')); return callback(new Error('Config requires "bbsTag"!'));
} }
return callback(null); return callback(null);
}, },
function establishSecureConnection(callback) { function establishSecureConnection(callback) {
self.client.term.write(resetScreen()); self.client.term.write(resetScreen());
self.client.term.write('Connecting to DoorParty, please wait...\n'); self.client.term.write('Connecting to DoorParty, please wait...\n');
const sshClient = new SSHClient(); const sshClient = new SSHClient();
let pipeRestored = false; let pipeRestored = false;
let pipedStream; let pipedStream;
const restorePipe = function() { const restorePipe = function() {
if(pipedStream && !pipeRestored && !clientTerminated) { if(pipedStream && !pipeRestored && !clientTerminated) {
self.client.term.output.unpipe(pipedStream); self.client.term.output.unpipe(pipedStream);
self.client.term.output.resume(); self.client.term.output.resume();
} }
}; };
sshClient.on('ready', () => { sshClient.on('ready', () => {
// track client termination so we can clean up early // track client termination so we can clean up early
self.client.once('end', () => { self.client.once('end', () => {
self.client.log.info('Connection ended. Terminating DoorParty connection'); self.client.log.info('Connection ended. Terminating DoorParty connection');
clientTerminated = true; clientTerminated = true;
sshClient.end(); sshClient.end();
}); });
// establish tunnel for rlogin // establish tunnel for rlogin
sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => { sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => {
if(err) { if(err) {
return callback(new Error('Failed to establish tunnel')); return callback(new Error('Failed to establish tunnel'));
} }
// //
// Send rlogin // Send rlogin
// DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g. // DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g.
// [XA]nuskooler // [XA]nuskooler
// //
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
stream.write(rlogin); stream.write(rlogin);
pipedStream = stream; // :TODO: this is hacky... pipedStream = stream; // :TODO: this is hacky...
self.client.term.output.pipe(stream); self.client.term.output.pipe(stream);
stream.on('data', d => { stream.on('data', d => {
// :TODO: we should just pipe this... // :TODO: we should just pipe this...
self.client.term.rawWrite(d); self.client.term.rawWrite(d);
}); });
stream.on('close', () => { stream.on('close', () => {
restorePipe(); restorePipe();
sshClient.end(); sshClient.end();
}); });
}); });
}); });
sshClient.on('error', err => { sshClient.on('error', err => {
self.client.log.info(`DoorParty SSH client error: ${err.message}`); self.client.log.info(`DoorParty SSH client error: ${err.message}`);
}); });
sshClient.on('close', () => { sshClient.on('close', () => {
restorePipe(); restorePipe();
callback(null); callback(null);
}); });
sshClient.connect( { sshClient.connect( {
host : self.config.host, host : self.config.host,
port : self.config.sshPort, port : self.config.sshPort,
username : self.config.username, username : self.config.username,
password : self.config.password, password : self.config.password,
}); });
// note: no explicit callback() until we're finished! // note: no explicit callback() until we're finished!
} }
], ],
err => { err => {
if(err) { if(err) {
self.client.log.warn( { error : err.message }, 'DoorParty error'); self.client.log.warn( { error : err.message }, 'DoorParty error');
} }
// if the client is stil here, go to previous // if the client is stil here, go to previous
if(!clientTerminated) { if(!clientTerminated) {
self.prevMenu(); self.prevMenu();
} }
} }
); );
} }
}; };

View File

@ -7,72 +7,72 @@ const FileEntry = require('./file_entry.js');
const { partition } = require('lodash'); const { partition } = require('lodash');
module.exports = class DownloadQueue { module.exports = class DownloadQueue {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
if(!Array.isArray(this.client.user.downloadQueue)) { if(!Array.isArray(this.client.user.downloadQueue)) {
if(this.client.user.properties.dl_queue) { if(this.client.user.properties.dl_queue) {
this.loadFromProperty(this.client.user.properties.dl_queue); this.loadFromProperty(this.client.user.properties.dl_queue);
} else { } else {
this.client.user.downloadQueue = []; this.client.user.downloadQueue = [];
} }
} }
} }
get items() { get items() {
return this.client.user.downloadQueue; return this.client.user.downloadQueue;
} }
clear() { clear() {
this.client.user.downloadQueue = []; this.client.user.downloadQueue = [];
} }
toggle(fileEntry, systemFile=false) { toggle(fileEntry, systemFile=false) {
if(this.isQueued(fileEntry)) { if(this.isQueued(fileEntry)) {
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId); this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId);
} else { } else {
this.add(fileEntry, systemFile); this.add(fileEntry, systemFile);
} }
} }
add(fileEntry, systemFile=false) { add(fileEntry, systemFile=false) {
this.client.user.downloadQueue.push({ this.client.user.downloadQueue.push({
fileId : fileEntry.fileId, fileId : fileEntry.fileId,
areaTag : fileEntry.areaTag, areaTag : fileEntry.areaTag,
fileName : fileEntry.fileName, fileName : fileEntry.fileName,
path : fileEntry.filePath, path : fileEntry.filePath,
byteSize : fileEntry.meta.byte_size || 0, byteSize : fileEntry.meta.byte_size || 0,
systemFile : systemFile, systemFile : systemFile,
}); });
} }
removeItems(fileIds) { removeItems(fileIds) {
if(!Array.isArray(fileIds)) { if(!Array.isArray(fileIds)) {
fileIds = [ fileIds ]; fileIds = [ fileIds ];
} }
const [ remain, removed ] = partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) )); const [ remain, removed ] = partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) ));
this.client.user.downloadQueue = remain; this.client.user.downloadQueue = remain;
return removed; return removed;
} }
isQueued(entryOrId) { isQueued(entryOrId) {
if(entryOrId instanceof FileEntry) { if(entryOrId instanceof FileEntry) {
entryOrId = entryOrId.fileId; entryOrId = entryOrId.fileId;
} }
return this.client.user.downloadQueue.find(e => entryOrId === e.fileId) ? true : false; return this.client.user.downloadQueue.find(e => entryOrId === e.fileId) ? true : false;
} }
toProperty() { return JSON.stringify(this.client.user.downloadQueue); } toProperty() { return JSON.stringify(this.client.user.downloadQueue); }
loadFromProperty(prop) { loadFromProperty(prop) {
try { try {
this.client.user.downloadQueue = JSON.parse(prop); this.client.user.downloadQueue = JSON.parse(prop);
} catch(e) { } catch(e) {
this.client.user.downloadQueue = []; this.client.user.downloadQueue = [];
this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property'); this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property');
} }
} }
}; };

View File

@ -23,189 +23,189 @@ exports.DropFile = DropFile;
function DropFile(client, fileType) { function DropFile(client, fileType) {
var self = this; var self = this;
this.client = client; this.client = client;
this.fileType = (fileType || 'DORINFO').toUpperCase(); this.fileType = (fileType || 'DORINFO').toUpperCase();
Object.defineProperty(this, 'fullPath', { Object.defineProperty(this, 'fullPath', {
get : function() { get : function() {
return paths.join(Config().paths.dropFiles, ('node' + self.client.node), self.fileName); return paths.join(Config().paths.dropFiles, ('node' + self.client.node), self.fileName);
} }
}); });
Object.defineProperty(this, 'fileName', { Object.defineProperty(this, 'fileName', {
get : function() { get : function() {
return { return {
DOOR : 'DOOR.SYS', // GAP BBS, many others DOOR : 'DOOR.SYS', // GAP BBS, many others
DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ... DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ...
CALLINFO : 'CALLINFO.BBS', // Citadel? CALLINFO : 'CALLINFO.BBS', // Citadel?
DORINFO : self.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... DORINFO : self.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ...
CHAIN : 'CHAIN.TXT', // WWIV CHAIN : 'CHAIN.TXT', // WWIV
CURRUSER : 'CURRUSER.BBS', // RyBBS CURRUSER : 'CURRUSER.BBS', // RyBBS
SFDOORS : 'SFDOORS.DAT', // Spitfire SFDOORS : 'SFDOORS.DAT', // Spitfire
PCBOARD : 'PCBOARD.SYS', // PCBoard PCBOARD : 'PCBOARD.SYS', // PCBoard
TRIBBS : 'TRIBBS.SYS', // TriBBS TRIBBS : 'TRIBBS.SYS', // TriBBS
USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+ USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+
JUMPER : 'JUMPER.DAT', // 2AM BBS JUMPER : 'JUMPER.DAT', // 2AM BBS
SXDOOR : // System/X, dESiRE SXDOOR : // System/X, dESiRE
'SXDOOR.' + _.pad(self.client.node.toString(), 3, '0'), 'SXDOOR.' + _.pad(self.client.node.toString(), 3, '0'),
INFO : 'INFO.BBS', // Phoenix BBS INFO : 'INFO.BBS', // Phoenix BBS
}[self.fileType]; }[self.fileType];
} }
}); });
Object.defineProperty(this, 'dropFileContents', { Object.defineProperty(this, 'dropFileContents', {
get : function() { get : function() {
return { return {
DOOR : self.getDoorSysBuffer(), DOOR : self.getDoorSysBuffer(),
DOOR32 : self.getDoor32Buffer(), DOOR32 : self.getDoor32Buffer(),
DORINFO : self.getDoorInfoDefBuffer(), DORINFO : self.getDoorInfoDefBuffer(),
}[self.fileType]; }[self.fileType];
} }
}); });
this.getDoorInfoFileName = function() { this.getDoorInfoFileName = function() {
var x; var x;
var node = self.client.node; var node = self.client.node;
if(10 === node) { if(10 === node) {
x = 0; x = 0;
} else if(node < 10) { } else if(node < 10) {
x = node; x = node;
} else { } else {
x = String.fromCharCode('a'.charCodeAt(0) + (node - 11)); x = String.fromCharCode('a'.charCodeAt(0) + (node - 11));
} }
return 'DORINFO' + x + '.DEF'; return 'DORINFO' + x + '.DEF';
}; };
this.getDoorSysBuffer = function() { this.getDoorSysBuffer = function() {
var up = self.client.user.properties; var up = self.client.user.properties;
var now = moment(); var now = moment();
var secLevel = self.client.user.getLegacySecurityLevel().toString(); var secLevel = self.client.user.getLegacySecurityLevel().toString();
// :TODO: fix time remaining // :TODO: fix time remaining
// :TODO: fix default protocol -- user prop: transfer_protocol // :TODO: fix default protocol -- user prop: transfer_protocol
return iconv.encode( [ return iconv.encode( [
'COM1:', // "Comm Port - COM0: = LOCAL MODE" 'COM1:', // "Comm Port - COM0: = LOCAL MODE"
'57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!) '57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!)
'8', // "Parity - 7 or 8" '8', // "Parity - 7 or 8"
self.client.node.toString(), // "Node Number - 1 to 99" self.client.node.toString(), // "Node Number - 1 to 99"
'57600', // "DTE Rate. Actual BPS rate to use. (kg)" '57600', // "DTE Rate. Actual BPS rate to use. (kg)"
'Y', // "Screen Display - Y=On N=Off (Default to Y)" 'Y', // "Screen Display - Y=On N=Off (Default to Y)"
'Y', // "Printer Toggle - Y=On N=Off (Default to Y)" 'Y', // "Printer Toggle - Y=On N=Off (Default to Y)"
'Y', // "Page Bell - Y=On N=Off (Default to Y)" 'Y', // "Page Bell - Y=On N=Off (Default to Y)"
'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)"
up.real_name || self.client.user.username, // "User Full Name" up.real_name || self.client.user.username, // "User Full Name"
up.location || 'Anywhere', // "Calling From" up.location || 'Anywhere', // "Calling From"
'123-456-7890', // "Home Phone" '123-456-7890', // "Home Phone"
'123-456-7890', // "Work/Data Phone" '123-456-7890', // "Work/Data Phone"
'NOPE', // "Password" (Note: this is never given out or even stored plaintext) 'NOPE', // "Password" (Note: this is never given out or even stored plaintext)
secLevel, // "Security Level" secLevel, // "Security Level"
up.login_count.toString(), // "Total Times On" up.login_count.toString(), // "Total Times On"
now.format('MM/DD/YY'), // "Last Date Called" now.format('MM/DD/YY'), // "Last Date Called"
'15360', // "Seconds Remaining THIS call (for those that particular)" '15360', // "Seconds Remaining THIS call (for those that particular)"
'256', // "Minutes Remaining THIS call" '256', // "Minutes Remaining THIS call"
'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller" 'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller"
self.client.term.termHeight.toString(), // "Page Length" self.client.term.termHeight.toString(), // "Page Length"
'N', // "User Mode - Y = Expert, N = Novice" 'N', // "User Mode - Y = Expert, N = Novice"
'1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)" '1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)"
'1', // "Conference Exited To DOOR From (G)" '1', // "Conference Exited To DOOR From (G)"
'01/01/99', // "User Expiration Date (mm/dd/yy)" '01/01/99', // "User Expiration Date (mm/dd/yy)"
self.client.user.userId.toString(), // "User File's Record Number" self.client.user.userId.toString(), // "User File's Record Number"
'Z', // "Default Protocol - X, C, Y, G, I, N, Etc." 'Z', // "Default Protocol - X, C, Y, G, I, N, Etc."
// :TODO: fix up, down, etc. form user properties // :TODO: fix up, down, etc. form user properties
'0', // "Total Uploads" '0', // "Total Uploads"
'0', // "Total Downloads" '0', // "Total Downloads"
'0', // "Daily Download "K" Total" '0', // "Daily Download "K" Total"
'999999', // "Daily Download Max. "K" Limit" '999999', // "Daily Download Max. "K" Limit"
moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate" moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate"
'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)"
'X:\\GEN\\', // "Path to the GEN directory" 'X:\\GEN\\', // "Path to the GEN directory"
StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)" StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)"
self.client.user.username, // "Alias name" self.client.user.username, // "Alias name"
'00:05', // "Event time (hh:mm)" (note: wat?) '00:05', // "Event time (hh:mm)" (note: wat?)
'Y', // "If its an error correcting connection (Y/N)" 'Y', // "If its an error correcting connection (Y/N)"
'Y', // "ANSI supported & caller using NG mode (Y/N)" 'Y', // "ANSI supported & caller using NG mode (Y/N)"
'Y', // "Use Record Locking (Y/N)" 'Y', // "Use Record Locking (Y/N)"
'7', // "BBS Default Color (Standard IBM color code, ie, 1-15)" '7', // "BBS Default Color (Standard IBM color code, ie, 1-15)"
// :TODO: fix minutes here also: // :TODO: fix minutes here also:
'256', // "Time Credits In Minutes (positive/negative)" '256', // "Time Credits In Minutes (positive/negative)"
'07/07/90', // "Last New Files Scan Date (mm/dd/yy)" '07/07/90', // "Last New Files Scan Date (mm/dd/yy)"
// :TODO: fix last vs now times: // :TODO: fix last vs now times:
now.format('hh:mm'), // "Time of This Call" now.format('hh:mm'), // "Time of This Call"
now.format('hh:mm'), // "Time of Last Call (hh:mm)" now.format('hh:mm'), // "Time of Last Call (hh:mm)"
'9999', // "Maximum daily files available" '9999', // "Maximum daily files available"
// :TODO: fix these stats: // :TODO: fix these stats:
'0', // "Files d/led so far today" '0', // "Files d/led so far today"
'0', // "Total "K" Bytes Uploaded" '0', // "Total "K" Bytes Uploaded"
'0', // "Total "K" Bytes Downloaded" '0', // "Total "K" Bytes Downloaded"
up.user_comment || 'None', // "User Comment" up.user_comment || 'None', // "User Comment"
'0', // "Total Doors Opened" '0', // "Total Doors Opened"
'0', // "Total Messages Left" '0', // "Total Messages Left"
].join('\r\n') + '\r\n', 'cp437'); ].join('\r\n') + '\r\n', 'cp437');
}; };
this.getDoor32Buffer = function() { this.getDoor32Buffer = function() {
// //
// Resources: // Resources:
// * http://wiki.bbses.info/index.php/DOOR32.SYS // * http://wiki.bbses.info/index.php/DOOR32.SYS
// //
// :TODO: local/serial/telnet need to be configurable -- which also changes socket handle! // :TODO: local/serial/telnet need to be configurable -- which also changes socket handle!
return iconv.encode([ return iconv.encode([
'2', // :TODO: This needs to be configurable! '2', // :TODO: This needs to be configurable!
// :TODO: Completely broken right now -- This need to be configurable & come from temp socket server most likely // :TODO: Completely broken right now -- This need to be configurable & come from temp socket server most likely
'-1', // self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows! '-1', // self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows!
'57600', '57600',
Config().general.boardName, Config().general.boardName,
self.client.user.userId.toString(), self.client.user.userId.toString(),
self.client.user.properties.real_name || self.client.user.username, self.client.user.properties.real_name || self.client.user.username,
self.client.user.username, self.client.user.username,
self.client.user.getLegacySecurityLevel().toString(), self.client.user.getLegacySecurityLevel().toString(),
'546', // :TODO: Minutes left! '546', // :TODO: Minutes left!
'1', // ANSI '1', // ANSI
self.client.node.toString(), self.client.node.toString(),
].join('\r\n') + '\r\n', 'cp437'); ].join('\r\n') + '\r\n', 'cp437');
}; };
this.getDoorInfoDefBuffer = function() { this.getDoorInfoDefBuffer = function() {
// :TODO: fix time remaining // :TODO: fix time remaining
// //
// Resources: // Resources:
// * http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm // * http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm
// //
// Note that usernames are just used for first/last names here // Note that usernames are just used for first/last names here
// //
var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0];
var un = /[^\s]*/.exec(self.client.user.username)[0]; var un = /[^\s]*/.exec(self.client.user.username)[0];
var secLevel = self.client.user.getLegacySecurityLevel().toString(); var secLevel = self.client.user.getLegacySecurityLevel().toString();
return iconv.encode( [ return iconv.encode( [
Config().general.boardName, // "The name of the system." Config().general.boardName, // "The name of the system."
opUn, // "The sysop's name up to the first space." opUn, // "The sysop's name up to the first space."
opUn, // "The sysop's name following the first space." opUn, // "The sysop's name following the first space."
'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console."
'57600', // "The current port (DTE) rate." '57600', // "The current port (DTE) rate."
'0', // "The number "0"" '0', // "The number "0""
un, // "The current user's name, up to the first space." un, // "The current user's name, up to the first space."
un, // "The current user's name, following the first space." un, // "The current user's name, following the first space."
self.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown." self.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown."
'1', // "The number "0" if TTY, or "1" if ANSI." '1', // "The number "0" if TTY, or "1" if ANSI."
secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops."
'546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software."
'-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." '-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines."
].join('\r\n') + '\r\n', 'cp437'); ].join('\r\n') + '\r\n', 'cp437');
}; };
} }
DropFile.fileTypes = [ 'DORINFO' ]; DropFile.fileTypes = [ 'DORINFO' ];
DropFile.prototype.createFile = function(cb) { DropFile.prototype.createFile = function(cb) {
fs.writeFile(this.fullPath, this.dropFileContents, function written(err) { fs.writeFile(this.fullPath, this.dropFileContents, function written(err) {
cb(err); cb(err);
}); });
}; };

View File

@ -12,79 +12,79 @@ const _ = require('lodash');
exports.EditTextView = EditTextView; exports.EditTextView = EditTextView;
function EditTextView(options) { function EditTextView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
options.resizable = false; options.resizable = false;
TextView.call(this, options); TextView.call(this, options);
this.cursorPos = { row : 0, col : 0 }; this.cursorPos = { row : 0, col : 0 };
this.clientBackspace = function() { this.clientBackspace = function() {
const fillCharSGR = this.getStyleSGR(1) || this.getSGR(); const fillCharSGR = this.getStyleSGR(1) || this.getSGR();
this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`); this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`);
}; };
} }
require('util').inherits(EditTextView, TextView); require('util').inherits(EditTextView, TextView);
EditTextView.prototype.onKeyPress = function(ch, key) { EditTextView.prototype.onKeyPress = function(ch, key) {
if(key) { if(key) {
if(this.isKeyMapped('backspace', key.name)) { if(this.isKeyMapped('backspace', key.name)) {
if(this.text.length > 0) { if(this.text.length > 0) {
this.text = this.text.substr(0, this.text.length - 1); this.text = this.text.substr(0, this.text.length - 1);
if(this.text.length >= this.dimens.width) { if(this.text.length >= this.dimens.width) {
this.redraw(); this.redraw();
} else { } else {
this.cursorPos.col -= 1; this.cursorPos.col -= 1;
if(this.cursorPos.col >= 0) { if(this.cursorPos.col >= 0) {
this.clientBackspace(); this.clientBackspace();
} }
} }
} }
return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); return EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
} else if(this.isKeyMapped('clearLine', key.name)) { } else if(this.isKeyMapped('clearLine', key.name)) {
this.text = ''; this.text = '';
this.cursorPos.col = 0; this.cursorPos.col = 0;
this.setFocus(true); // resetting focus will redraw & adjust cursor this.setFocus(true); // resetting focus will redraw & adjust cursor
return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); return EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
} }
} }
if(ch && strUtil.isPrintable(ch)) { if(ch && strUtil.isPrintable(ch)) {
if(this.text.length < this.maxLength) { if(this.text.length < this.maxLength) {
ch = strUtil.stylizeString(ch, this.textStyle); ch = strUtil.stylizeString(ch, this.textStyle);
this.text += ch; this.text += ch;
if(this.text.length > this.dimens.width) { if(this.text.length > this.dimens.width) {
// no shortcuts - redraw the view // no shortcuts - redraw the view
this.redraw(); this.redraw();
} else { } else {
this.cursorPos.col += 1; this.cursorPos.col += 1;
if(_.isString(this.textMaskChar)) { if(_.isString(this.textMaskChar)) {
if(this.textMaskChar.length > 0) { if(this.textMaskChar.length > 0) {
this.client.term.write(this.textMaskChar); this.client.term.write(this.textMaskChar);
} }
} else { } else {
this.client.term.write(ch); this.client.term.write(ch);
} }
} }
} }
} }
EditTextView.super_.prototype.onKeyPress.call(this, ch, key); EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
}; };
EditTextView.prototype.setText = function(text) { EditTextView.prototype.setText = function(text) {
// draw & set |text| // draw & set |text|
EditTextView.super_.prototype.setText.call(this, text); EditTextView.super_.prototype.setText.call(this, text);
// adjust local cursor tracking // adjust local cursor tracking
this.cursorPos = { row : 0, col : text.length }; this.cursorPos = { row : 0, col : text.length };
}; };

View File

@ -13,20 +13,20 @@ const nodeMailer = require('nodemailer');
exports.sendMail = sendMail; exports.sendMail = sendMail;
function sendMail(message, cb) { function sendMail(message, cb) {
const config = Config(); const config = Config();
if(!_.has(config, 'email.transport')) { if(!_.has(config, 'email.transport')) {
return cb(Errors.MissingConfig('Email "email::transport" configuration missing')); return cb(Errors.MissingConfig('Email "email::transport" configuration missing'));
} }
message.from = message.from || config.email.defaultFrom; message.from = message.from || config.email.defaultFrom;
const transportOptions = Object.assign( {}, config.email.transport, { const transportOptions = Object.assign( {}, config.email.transport, {
logger : Log, logger : Log,
}); });
const transport = nodeMailer.createTransport(transportOptions); const transport = nodeMailer.createTransport(transportOptions);
transport.sendMail(message, (err, info) => { transport.sendMail(message, (err, info) => {
return cb(err, info); return cb(err, info);
}); });
} }

View File

@ -2,45 +2,45 @@
'use strict'; 'use strict';
class EnigError extends Error { class EnigError extends Error {
constructor(message, code, reason, reasonCode) { constructor(message, code, reason, reasonCode) {
super(message); super(message);
this.name = this.constructor.name; this.name = this.constructor.name;
this.message = message; this.message = message;
this.code = code; this.code = code;
this.reason = reason; this.reason = reason;
this.reasonCode = reasonCode; this.reasonCode = reasonCode;
if(this.reason) { if(this.reason) {
this.message += `: ${this.reason}`; this.message += `: ${this.reason}`;
} }
if(typeof Error.captureStackTrace === 'function') { if(typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);
} else { } else {
this.stack = (new Error(message)).stack; this.stack = (new Error(message)).stack;
} }
} }
} }
exports.EnigError = EnigError; exports.EnigError = EnigError;
exports.Errors = { exports.Errors = {
General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode), General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode),
MenuStack : (reason, reasonCode) => new EnigError('Menu stack error', -33001, reason, reasonCode), MenuStack : (reason, reasonCode) => new EnigError('Menu stack error', -33001, reason, reasonCode),
DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode), DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode),
AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode), AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode),
Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode), Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode),
ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode),
MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode),
UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode), UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode),
MissingParam : (reason, reasonCode) => new EnigError('Missing paramater(s)', -32008, reason, reasonCode), MissingParam : (reason, reasonCode) => new EnigError('Missing paramater(s)', -32008, reason, reasonCode),
}; };
exports.ErrorReasons = { exports.ErrorReasons = {
AlreadyThere : 'ALREADYTHERE', AlreadyThere : 'ALREADYTHERE',
InvalidNextMenu : 'BADNEXT', InvalidNextMenu : 'BADNEXT',
NoPreviousMenu : 'NOPREV', NoPreviousMenu : 'NOPREV',
NoConditionMatch : 'NOCONDMATCH', NoConditionMatch : 'NOCONDMATCH',
NotEnabled : 'NOTENABLED', NotEnabled : 'NOTENABLED',
}; };

View File

@ -9,10 +9,10 @@ const Log = require('./logger.js').log;
const assert = require('assert'); const assert = require('assert');
module.exports = function(condition, message) { module.exports = function(condition, message) {
if(Config().debug.assertsEnabled) { if(Config().debug.assertsEnabled) {
assert.apply(this, arguments); assert.apply(this, arguments);
} else if(!(condition)) { } else if(!(condition)) {
const stack = new Error().stack; const stack = new Error().stack;
Log.error( { condition : condition, stack : stack }, message || 'Assertion failed' ); Log.error( { condition : condition, stack : stack }, message || 'Assertion failed' );
} }
}; };

View File

@ -23,157 +23,157 @@ const net = require('net');
exports.getModule = ErcClientModule; exports.getModule = ErcClientModule;
exports.moduleInfo = { exports.moduleInfo = {
name : 'ENiGMA Relay Chat Client', name : 'ENiGMA Relay Chat Client',
desc : 'Chat with other ENiGMA BBSes', desc : 'Chat with other ENiGMA BBSes',
author : 'Andrew Pamment', author : 'Andrew Pamment',
}; };
var MciViewIds = { var MciViewIds = {
ChatDisplay : 1, ChatDisplay : 1,
InputArea : 3, InputArea : 3,
}; };
// :TODO: needs converted to ES6 MenuModule subclass // :TODO: needs converted to ES6 MenuModule subclass
function ErcClientModule(options) { function ErcClientModule(options) {
MenuModule.prototype.ctorShim.call(this, options); MenuModule.prototype.ctorShim.call(this, options);
const self = this; const self = this;
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}'; this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}';
this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}'; this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}';
this.finishedLoading = function() { this.finishedLoading = function() {
async.waterfall( async.waterfall(
[ [
function validateConfig(callback) { function validateConfig(callback) {
if(_.isString(self.config.host) && if(_.isString(self.config.host) &&
_.isNumber(self.config.port) && _.isNumber(self.config.port) &&
_.isString(self.config.bbsTag)) _.isString(self.config.bbsTag))
{ {
return callback(null); return callback(null);
} else { } else {
return callback(new Error('Configuration is missing required option(s)')); return callback(new Error('Configuration is missing required option(s)'));
} }
}, },
function connectToServer(callback) { function connectToServer(callback) {
const connectOpts = { const connectOpts = {
port : self.config.port, port : self.config.port,
host : self.config.host, host : self.config.host,
}; };
const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay);
chatMessageView.setText('Connecting to server...'); chatMessageView.setText('Connecting to server...');
chatMessageView.redraw(); chatMessageView.redraw();
self.viewControllers.menu.switchFocus(MciViewIds.InputArea); self.viewControllers.menu.switchFocus(MciViewIds.InputArea);
// :TODO: Track actual client->enig connection for optional prevMenu @ final CB // :TODO: Track actual client->enig connection for optional prevMenu @ final CB
self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host); self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host);
self.chatConnection.on('data', data => { self.chatConnection.on('data', data => {
data = data.toString(); data = data.toString();
if(data.startsWith('ERCHANDSHAKE')) { if(data.startsWith('ERCHANDSHAKE')) {
self.chatConnection.write(`ERCMAGIC|${self.config.bbsTag}|${self.client.user.username}\r\n`); self.chatConnection.write(`ERCMAGIC|${self.config.bbsTag}|${self.client.user.username}\r\n`);
} else if(data.startsWith('{')) { } else if(data.startsWith('{')) {
try { try {
data = JSON.parse(data); data = JSON.parse(data);
} catch(e) { } catch(e) {
return self.client.log.warn( { error : e.message }, 'ERC: Error parsing ERC data from server'); return self.client.log.warn( { error : e.message }, 'ERC: Error parsing ERC data from server');
} }
let text; let text;
try { try {
if(data.userName) { if(data.userName) {
// user message // user message
text = stringFormat(self.chatEntryFormat, data); text = stringFormat(self.chatEntryFormat, data);
} else { } else {
// system message // system message
text = stringFormat(self.systemEntryFormat, data); text = stringFormat(self.systemEntryFormat, data);
} }
} catch(e) { } catch(e) {
return self.client.log.warn( { error : e.message }, 'ERC: chatEntryFormat error'); return self.client.log.warn( { error : e.message }, 'ERC: chatEntryFormat error');
} }
chatMessageView.addText(text); chatMessageView.addText(text);
if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height? if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height?
chatMessageView.deleteLine(0); chatMessageView.deleteLine(0);
chatMessageView.scrollDown(); chatMessageView.scrollDown();
} }
chatMessageView.redraw(); chatMessageView.redraw();
self.viewControllers.menu.switchFocus(MciViewIds.InputArea); self.viewControllers.menu.switchFocus(MciViewIds.InputArea);
} }
}); });
self.chatConnection.once('end', () => { self.chatConnection.once('end', () => {
return callback(null); return callback(null);
}); });
self.chatConnection.once('error', err => { self.chatConnection.once('error', err => {
self.client.log.info(`ERC connection error: ${err.message}`); self.client.log.info(`ERC connection error: ${err.message}`);
return callback(new Error('Failed connecting to ERC server!')); return callback(new Error('Failed connecting to ERC server!'));
}); });
} }
], ],
err => { err => {
if(err) { if(err) {
self.client.log.warn( { error : err.message }, 'ERC error'); self.client.log.warn( { error : err.message }, 'ERC error');
} }
self.prevMenu(); self.prevMenu();
} }
); );
}; };
this.scrollHandler = function(keyName) { this.scrollHandler = function(keyName) {
const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea);
const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay);
if('up arrow' === keyName) { if('up arrow' === keyName) {
chatDisplayView.scrollUp(); chatDisplayView.scrollUp();
} else { } else {
chatDisplayView.scrollDown(); chatDisplayView.scrollDown();
} }
chatDisplayView.redraw(); chatDisplayView.redraw();
inputAreaView.setFocus(true); inputAreaView.setFocus(true);
}; };
this.menuMethods = { this.menuMethods = {
inputAreaSubmit : function(formData, extraArgs, cb) { inputAreaSubmit : function(formData, extraArgs, cb) {
const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea);
const inputData = inputAreaView.getData(); const inputData = inputAreaView.getData();
if('/quit' === inputData.toLowerCase()) { if('/quit' === inputData.toLowerCase()) {
self.chatConnection.end(); self.chatConnection.end();
} else { } else {
try { try {
self.chatConnection.write(`${inputData}\r\n`); self.chatConnection.write(`${inputData}\r\n`);
} catch(e) { } catch(e) {
self.client.log.warn( { error : e.message }, 'ERC error'); self.client.log.warn( { error : e.message }, 'ERC error');
} }
inputAreaView.clearText(); inputAreaView.clearText();
} }
return cb(null); return cb(null);
}, },
scrollUp : function(formData, extraArgs, cb) { scrollUp : function(formData, extraArgs, cb) {
self.scrollHandler(formData.key.name); self.scrollHandler(formData.key.name);
return cb(null); return cb(null);
}, },
scrollDown : function(formData, extraArgs, cb) { scrollDown : function(formData, extraArgs, cb) {
self.scrollHandler(formData.key.name); self.scrollHandler(formData.key.name);
return cb(null); return cb(null);
} }
}; };
} }
require('util').inherits(ErcClientModule, MenuModule); require('util').inherits(ErcClientModule, MenuModule);
ErcClientModule.prototype.mciReady = function(mciData, cb) { ErcClientModule.prototype.mciReady = function(mciData, cb) {
this.standardMCIReadyHandler(mciData, cb); this.standardMCIReadyHandler(mciData, cb);
}; };

View File

@ -19,251 +19,251 @@ exports.getModule = EventSchedulerModule;
exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart
exports.moduleInfo = { exports.moduleInfo = {
name : 'Event Scheduler', name : 'Event Scheduler',
desc : 'Support for scheduling arbritary events', desc : 'Support for scheduling arbritary events',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/; const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/;
const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/; const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/;
class ScheduledEvent { class ScheduledEvent {
constructor(events, name) { constructor(events, name) {
this.name = name; this.name = name;
this.schedule = this.parseScheduleString(events[name].schedule); this.schedule = this.parseScheduleString(events[name].schedule);
this.action = this.parseActionSpec(events[name].action); this.action = this.parseActionSpec(events[name].action);
if(this.action) { if(this.action) {
this.action.args = events[name].args || []; this.action.args = events[name].args || [];
} }
} }
get isValid() { get isValid() {
if((!this.schedule || (!this.schedule.sched && !this.schedule.watchFile)) || !this.action) { if((!this.schedule || (!this.schedule.sched && !this.schedule.watchFile)) || !this.action) {
return false; return false;
} }
if('method' === this.action.type && !this.action.location) { if('method' === this.action.type && !this.action.location) {
return false; return false;
} }
return true; return true;
} }
parseScheduleString(schedStr) { parseScheduleString(schedStr) {
if(!schedStr) { if(!schedStr) {
return false; return false;
} }
let schedule = {}; let schedule = {};
const m = SCHEDULE_REGEXP.exec(schedStr); const m = SCHEDULE_REGEXP.exec(schedStr);
if(m) { if(m) {
schedStr = schedStr.substr(0, m.index).trim(); schedStr = schedStr.substr(0, m.index).trim();
if('@watch:' === m[1]) { if('@watch:' === m[1]) {
schedule.watchFile = m[2]; schedule.watchFile = m[2];
} }
} }
if(schedStr.length > 0) { if(schedStr.length > 0) {
const sched = later.parse.text(schedStr); const sched = later.parse.text(schedStr);
if(-1 === sched.error) { if(-1 === sched.error) {
schedule.sched = sched; schedule.sched = sched;
} }
} }
// return undefined if we couldn't parse out anything useful // return undefined if we couldn't parse out anything useful
if(!_.isEmpty(schedule)) { if(!_.isEmpty(schedule)) {
return schedule; return schedule;
} }
} }
parseActionSpec(actionSpec) { parseActionSpec(actionSpec) {
if(actionSpec) { if(actionSpec) {
if('@' === actionSpec[0]) { if('@' === actionSpec[0]) {
const m = ACTION_REGEXP.exec(actionSpec); const m = ACTION_REGEXP.exec(actionSpec);
if(m) { if(m) {
if(m[2].indexOf(':') > -1) { if(m[2].indexOf(':') > -1) {
const parts = m[2].split(':'); const parts = m[2].split(':');
return { return {
type : m[1], type : m[1],
location : parts[0], location : parts[0],
what : parts[1], what : parts[1],
}; };
} else { } else {
return { return {
type : m[1], type : m[1],
what : m[2], what : m[2],
}; };
} }
} }
} else { } else {
return { return {
type : 'execute', type : 'execute',
what : actionSpec, what : actionSpec,
}; };
} }
} }
} }
executeAction(reason, cb) { executeAction(reason, cb) {
Log.info( { eventName : this.name, action : this.action, reason : reason }, 'Executing scheduled event action...'); Log.info( { eventName : this.name, action : this.action, reason : reason }, 'Executing scheduled event action...');
if('method' === this.action.type) { if('method' === this.action.type) {
const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js') const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js')
try { try {
const methodModule = require(modulePath); const methodModule = require(modulePath);
methodModule[this.action.what](this.action.args, err => { methodModule[this.action.what](this.action.args, err => {
if(err) { if(err) {
Log.debug( Log.debug(
{ error : err.toString(), eventName : this.name, action : this.action }, { error : err.toString(), eventName : this.name, action : this.action },
'Error performing scheduled event action'); 'Error performing scheduled event action');
} }
return cb(err); return cb(err);
}); });
} catch(e) { } catch(e) {
Log.warn( Log.warn(
{ error : e.toString(), eventName : this.name, action : this.action }, { error : e.toString(), eventName : this.name, action : this.action },
'Failed to perform scheduled event action'); 'Failed to perform scheduled event action');
return cb(e); return cb(e);
} }
} else if('execute' === this.action.type) { } else if('execute' === this.action.type) {
const opts = { const opts = {
// :TODO: cwd // :TODO: cwd
name : this.name, name : this.name,
cols : 80, cols : 80,
rows : 24, rows : 24,
env : process.env, env : process.env,
}; };
const proc = pty.spawn(this.action.what, this.action.args, opts); const proc = pty.spawn(this.action.what, this.action.args, opts);
proc.once('exit', exitCode => { proc.once('exit', exitCode => {
if(exitCode) { if(exitCode) {
Log.warn( Log.warn(
{ eventName : this.name, action : this.action, exitCode : exitCode }, { eventName : this.name, action : this.action, exitCode : exitCode },
'Bad exit code while performing scheduled event action'); 'Bad exit code while performing scheduled event action');
} }
return cb(exitCode ? new Error(`Bad exit code while performing scheduled event action: ${exitCode}`) : null); return cb(exitCode ? new Error(`Bad exit code while performing scheduled event action: ${exitCode}`) : null);
}); });
} }
} }
} }
function EventSchedulerModule(options) { function EventSchedulerModule(options) {
PluginModule.call(this, options); PluginModule.call(this, options);
const config = Config(); const config = Config();
if(_.has(config, 'eventScheduler')) { if(_.has(config, 'eventScheduler')) {
this.moduleConfig = config.eventScheduler; this.moduleConfig = config.eventScheduler;
} }
const self = this; const self = this;
this.runningActions = new Set(); this.runningActions = new Set();
this.performAction = function(schedEvent, reason) { this.performAction = function(schedEvent, reason) {
if(self.runningActions.has(schedEvent.name)) { if(self.runningActions.has(schedEvent.name)) {
return; // already running return; // already running
} }
self.runningActions.add(schedEvent.name); self.runningActions.add(schedEvent.name);
schedEvent.executeAction(reason, () => { schedEvent.executeAction(reason, () => {
self.runningActions.delete(schedEvent.name); self.runningActions.delete(schedEvent.name);
}); });
}; };
} }
// convienence static method for direct load + start // convienence static method for direct load + start
EventSchedulerModule.loadAndStart = function(cb) { EventSchedulerModule.loadAndStart = function(cb) {
const loadModuleEx = require('./module_util.js').loadModuleEx; const loadModuleEx = require('./module_util.js').loadModuleEx;
const loadOpts = { const loadOpts = {
name : path.basename(__filename, '.js'), name : path.basename(__filename, '.js'),
path : __dirname, path : __dirname,
}; };
loadModuleEx(loadOpts, (err, mod) => { loadModuleEx(loadOpts, (err, mod) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const modInst = new mod.getModule(); const modInst = new mod.getModule();
modInst.startup( err => { modInst.startup( err => {
return cb(err, modInst); return cb(err, modInst);
}); });
}); });
}; };
EventSchedulerModule.prototype.startup = function(cb) { EventSchedulerModule.prototype.startup = function(cb) {
this.eventTimers = []; this.eventTimers = [];
const self = this; const self = this;
if(this.moduleConfig && _.has(this.moduleConfig, 'events')) { if(this.moduleConfig && _.has(this.moduleConfig, 'events')) {
const events = Object.keys(this.moduleConfig.events).map( name => { const events = Object.keys(this.moduleConfig.events).map( name => {
return new ScheduledEvent(this.moduleConfig.events, name); return new ScheduledEvent(this.moduleConfig.events, name);
}); });
events.forEach( schedEvent => { events.forEach( schedEvent => {
if(!schedEvent.isValid) { if(!schedEvent.isValid) {
Log.warn( { eventName : schedEvent.name }, 'Invalid scheduled event entry'); Log.warn( { eventName : schedEvent.name }, 'Invalid scheduled event entry');
return; return;
} }
Log.debug( Log.debug(
{ {
eventName : schedEvent.name, eventName : schedEvent.name,
schedule : this.moduleConfig.events[schedEvent.name].schedule, schedule : this.moduleConfig.events[schedEvent.name].schedule,
action : schedEvent.action, action : schedEvent.action,
next : schedEvent.schedule.sched ? moment(later.schedule(schedEvent.schedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', next : schedEvent.schedule.sched ? moment(later.schedule(schedEvent.schedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A',
}, },
'Scheduled event loaded' 'Scheduled event loaded'
); );
if(schedEvent.schedule.sched) { if(schedEvent.schedule.sched) {
this.eventTimers.push(later.setInterval( () => { this.eventTimers.push(later.setInterval( () => {
self.performAction(schedEvent, 'Schedule'); self.performAction(schedEvent, 'Schedule');
}, schedEvent.schedule.sched)); }, schedEvent.schedule.sched));
} }
if(schedEvent.schedule.watchFile) { if(schedEvent.schedule.watchFile) {
const watcher = sane( const watcher = sane(
paths.dirname(schedEvent.schedule.watchFile), paths.dirname(schedEvent.schedule.watchFile),
{ {
glob : `**/${paths.basename(schedEvent.schedule.watchFile)}` glob : `**/${paths.basename(schedEvent.schedule.watchFile)}`
} }
); );
// :TODO: should track watched files & stop watching @ shutdown? // :TODO: should track watched files & stop watching @ shutdown?
[ 'change', 'add', 'delete' ].forEach(event => { [ 'change', 'add', 'delete' ].forEach(event => {
watcher.on(event, (fileName, fileRoot) => { watcher.on(event, (fileName, fileRoot) => {
const eventPath = paths.join(fileRoot, fileName); const eventPath = paths.join(fileRoot, fileName);
if(schedEvent.schedule.watchFile === eventPath) { if(schedEvent.schedule.watchFile === eventPath) {
self.performAction(schedEvent, `Watch file: ${eventPath}`); self.performAction(schedEvent, `Watch file: ${eventPath}`);
} }
}); });
}); });
fse.exists(schedEvent.schedule.watchFile, exists => { fse.exists(schedEvent.schedule.watchFile, exists => {
if(exists) { if(exists) {
self.performAction(schedEvent, `Watch file: ${schedEvent.schedule.watchFile}`); self.performAction(schedEvent, `Watch file: ${schedEvent.schedule.watchFile}`);
} }
}); });
} }
}); });
} }
cb(null); cb(null);
}; };
EventSchedulerModule.prototype.shutdown = function(cb) { EventSchedulerModule.prototype.shutdown = function(cb) {
if(this.eventTimers) { if(this.eventTimers) {
this.eventTimers.forEach( et => et.clear() ); this.eventTimers.forEach( et => et.clear() );
} }
cb(null); cb(null);
}; };

View File

@ -12,68 +12,68 @@ const async = require('async');
const glob = require('glob'); const glob = require('glob');
module.exports = new class Events extends events.EventEmitter { module.exports = new class Events extends events.EventEmitter {
constructor() { constructor() {
super(); super();
this.setMaxListeners(32); // :TODO: play with this... this.setMaxListeners(32); // :TODO: play with this...
} }
getSystemEvents() { getSystemEvents() {
return SystemEvents; return SystemEvents;
} }
addListener(event, listener) { addListener(event, listener) {
Log.trace( { event : event }, 'Registering event listener'); Log.trace( { event : event }, 'Registering event listener');
return super.addListener(event, listener); return super.addListener(event, listener);
} }
emit(event, ...args) { emit(event, ...args) {
Log.trace( { event : event }, 'Emitting event'); Log.trace( { event : event }, 'Emitting event');
return super.emit(event, ...args); return super.emit(event, ...args);
} }
on(event, listener) { on(event, listener) {
Log.trace( { event : event }, 'Registering event listener'); Log.trace( { event : event }, 'Registering event listener');
return super.on(event, listener); return super.on(event, listener);
} }
once(event, listener) { once(event, listener) {
Log.trace( { event : event }, 'Registering single use event listener'); Log.trace( { event : event }, 'Registering single use event listener');
return super.once(event, listener); return super.once(event, listener);
} }
removeListener(event, listener) { removeListener(event, listener) {
Log.trace( { event : event }, 'Removing listener'); Log.trace( { event : event }, 'Removing listener');
return super.removeListener(event, listener); return super.removeListener(event, listener);
} }
startup(cb) { startup(cb) {
async.each(require('./module_util.js').getModulePaths(), (modulePath, nextPath) => { async.each(require('./module_util.js').getModulePaths(), (modulePath, nextPath) => {
glob('*{.js,/*.js}', { cwd : modulePath }, (err, files) => { glob('*{.js,/*.js}', { cwd : modulePath }, (err, files) => {
if(err) { if(err) {
return nextPath(err); return nextPath(err);
} }
async.each(files, (moduleName, nextModule) => { async.each(files, (moduleName, nextModule) => {
const fullModulePath = paths.join(modulePath, moduleName); const fullModulePath = paths.join(modulePath, moduleName);
try { try {
const mod = require(fullModulePath); const mod = require(fullModulePath);
if(_.isFunction(mod.registerEvents)) { if(_.isFunction(mod.registerEvents)) {
// :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ? // :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ?
mod.registerEvents(this); mod.registerEvents(this);
} }
} catch(e) { } catch(e) {
Log.warn( { error : e }, 'Exception during module "registerEvents"'); Log.warn( { error : e }, 'Exception during module "registerEvents"');
} }
return nextModule(null); return nextModule(null);
}, err => { }, err => {
return nextPath(err); return nextPath(err);
}); });
}); });
}, err => { }, err => {
return cb(err); return cb(err);
}); });
} }
}; };

View File

@ -49,183 +49,183 @@ const SSHClient = require('ssh2').Client;
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'Exodus', name : 'Exodus',
desc : 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/', desc : 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/',
author : 'NuSkooler', author : 'NuSkooler',
}; };
exports.getModule = class ExodusModule extends MenuModule { exports.getModule = class ExodusModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = options.menuConfig.config || {}; this.config = options.menuConfig.config || {};
this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org'; this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org';
this.config.ticketPort = this.config.ticketPort || 1984, this.config.ticketPort = this.config.ticketPort || 1984,
this.config.ticketPath = this.config.ticketPath || '/exodus'; this.config.ticketPath = this.config.ticketPath || '/exodus';
this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true); this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true);
this.config.sshHost = this.config.sshHost || this.config.ticketHost; this.config.sshHost = this.config.sshHost || this.config.ticketHost;
this.config.sshPort = this.config.sshPort || 22; this.config.sshPort = this.config.sshPort || 22;
this.config.sshUser = this.config.sshUser || 'exodus_server'; this.config.sshUser = this.config.sshUser || 'exodus_server';
this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa'); this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa');
} }
initSequence() { initSequence() {
const self = this; const self = this;
let clientTerminated = false; let clientTerminated = false;
async.waterfall( async.waterfall(
[ [
function validateConfig(callback) { function validateConfig(callback) {
// very basic validation on optionals // very basic validation on optionals
async.each( [ 'board', 'key', 'door' ], (key, next) => { async.each( [ 'board', 'key', 'door' ], (key, next) => {
return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`)); return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`));
}, callback); }, callback);
}, },
function loadCertAuthorities(callback) { function loadCertAuthorities(callback) {
if(!_.isString(self.config.caPem)) { if(!_.isString(self.config.caPem)) {
return callback(null, null); return callback(null, null);
} }
fs.readFile(self.config.caPem, (err, certAuthorities) => { fs.readFile(self.config.caPem, (err, certAuthorities) => {
return callback(err, certAuthorities); return callback(err, certAuthorities);
}); });
}, },
function getTicket(certAuthorities, callback) { function getTicket(certAuthorities, callback) {
const now = moment.utc().unix(); const now = moment.utc().unix();
const sha256 = crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex'); const sha256 = crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex');
const token = `${sha256}|${now}`; const token = `${sha256}|${now}`;
const postData = querystring.stringify({ const postData = querystring.stringify({
token : token, token : token,
board : self.config.board, board : self.config.board,
user : self.client.user.username, user : self.client.user.username,
door : self.config.door, door : self.config.door,
}); });
const reqOptions = { const reqOptions = {
hostname : self.config.ticketHost, hostname : self.config.ticketHost,
port : self.config.ticketPort, port : self.config.ticketPort,
path : self.config.ticketPath, path : self.config.ticketPath,
rejectUnauthorized : self.config.rejectUnauthorized, rejectUnauthorized : self.config.rejectUnauthorized,
method : 'POST', method : 'POST',
headers : { headers : {
'Content-Type' : 'application/x-www-form-urlencoded', 'Content-Type' : 'application/x-www-form-urlencoded',
'Content-Length' : postData.length, 'Content-Length' : postData.length,
'User-Agent' : getEnigmaUserAgent(), 'User-Agent' : getEnigmaUserAgent(),
} }
}; };
if(certAuthorities) { if(certAuthorities) {
reqOptions.ca = certAuthorities; reqOptions.ca = certAuthorities;
} }
let ticket = ''; let ticket = '';
const req = https.request(reqOptions, res => { const req = https.request(reqOptions, res => {
res.on('data', data => { res.on('data', data => {
ticket += data; ticket += data;
}); });
res.on('end', () => { res.on('end', () => {
if(ticket.length !== 36) { if(ticket.length !== 36) {
return callback(Errors.Invalid(`Invalid Exodus ticket: ${ticket}`)); return callback(Errors.Invalid(`Invalid Exodus ticket: ${ticket}`));
} }
return callback(null, ticket); return callback(null, ticket);
}); });
}); });
req.on('error', err => { req.on('error', err => {
return callback(Errors.General(`Exodus error: ${err.message}`)); return callback(Errors.General(`Exodus error: ${err.message}`));
}); });
req.write(postData); req.write(postData);
req.end(); req.end();
}, },
function loadPrivateKey(ticket, callback) { function loadPrivateKey(ticket, callback) {
fs.readFile(self.config.sshKeyPem, (err, privateKey) => { fs.readFile(self.config.sshKeyPem, (err, privateKey) => {
return callback(err, ticket, privateKey); return callback(err, ticket, privateKey);
}); });
}, },
function establishSecureConnection(ticket, privateKey, callback) { function establishSecureConnection(ticket, privateKey, callback) {
let pipeRestored = false; let pipeRestored = false;
let pipedStream; let pipedStream;
function restorePipe() { function restorePipe() {
if(pipedStream && !pipeRestored && !clientTerminated) { if(pipedStream && !pipeRestored && !clientTerminated) {
self.client.term.output.unpipe(pipedStream); self.client.term.output.unpipe(pipedStream);
self.client.term.output.resume(); self.client.term.output.resume();
} }
} }
self.client.term.write(resetScreen()); self.client.term.write(resetScreen());
self.client.term.write('Connecting to Exodus server, please wait...\n'); self.client.term.write('Connecting to Exodus server, please wait...\n');
const sshClient = new SSHClient(); const sshClient = new SSHClient();
const window = { const window = {
rows : self.client.term.termHeight, rows : self.client.term.termHeight,
cols : self.client.term.termWidth, cols : self.client.term.termWidth,
width : 0, width : 0,
height : 0, height : 0,
term : 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :( term : 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :(
}; };
const options = { const options = {
env : { env : {
exodus : ticket, exodus : ticket,
}, },
}; };
sshClient.on('ready', () => { sshClient.on('ready', () => {
self.client.once('end', () => { self.client.once('end', () => {
self.client.log.info('Connection ended. Terminating Exodus connection'); self.client.log.info('Connection ended. Terminating Exodus connection');
clientTerminated = true; clientTerminated = true;
return sshClient.end(); return sshClient.end();
}); });
sshClient.shell(window, options, (err, stream) => { sshClient.shell(window, options, (err, stream) => {
pipedStream = stream; // :TODO: ewwwwwwwww hack pipedStream = stream; // :TODO: ewwwwwwwww hack
self.client.term.output.pipe(stream); self.client.term.output.pipe(stream);
stream.on('data', d => { stream.on('data', d => {
return self.client.term.rawWrite(d); return self.client.term.rawWrite(d);
}); });
stream.on('close', () => { stream.on('close', () => {
restorePipe(); restorePipe();
return sshClient.end(); return sshClient.end();
}); });
stream.on('error', err => { stream.on('error', err => {
Log.warn( { error : err.message }, 'Exodus SSH client stream error'); Log.warn( { error : err.message }, 'Exodus SSH client stream error');
}); });
}); });
}); });
sshClient.on('close', () => { sshClient.on('close', () => {
restorePipe(); restorePipe();
return callback(null); return callback(null);
}); });
sshClient.connect({ sshClient.connect({
host : self.config.sshHost, host : self.config.sshHost,
port : self.config.sshPort, port : self.config.sshPort,
username : self.config.sshUser, username : self.config.sshUser,
privateKey : privateKey, privateKey : privateKey,
}); });
} }
], ],
err => { err => {
if(err) { if(err) {
self.client.log.warn( { error : err.message }, 'Exodus error'); self.client.log.warn( { error : err.message }, 'Exodus error');
} }
if(!clientTerminated) { if(!clientTerminated) {
self.prevMenu(); self.prevMenu();
} }
} }
); );
} }
}; };

View File

@ -12,328 +12,328 @@ const stringFormat = require('./string_format.js');
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Area Filter Editor', name : 'File Area Filter Editor',
desc : 'Module for adding, deleting, and modifying file base filters', desc : 'Module for adding, deleting, and modifying file base filters',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
editor : { editor : {
searchTerms : 1, searchTerms : 1,
tags : 2, tags : 2,
area : 3, area : 3,
sort : 4, sort : 4,
order : 5, order : 5,
filterName : 6, filterName : 6,
navMenu : 7, navMenu : 7,
// :TODO: use the customs new standard thing - filter obj can have active/selected, etc. // :TODO: use the customs new standard thing - filter obj can have active/selected, etc.
selectedFilterInfo : 10, // { ...filter object ... } selectedFilterInfo : 10, // { ...filter object ... }
activeFilterInfo : 11, // { ...filter object ... } activeFilterInfo : 11, // { ...filter object ... }
error : 12, // validation errors error : 12, // validation errors
} }
}; };
exports.getModule = class FileAreaFilterEdit extends MenuModule { exports.getModule = class FileAreaFilterEdit extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them
this.currentFilterIndex = 0; // into |filtersArray| this.currentFilterIndex = 0; // into |filtersArray|
// //
// Lexical sort + keep currently active filter (if any) as the first item in |filtersArray| // Lexical sort + keep currently active filter (if any) as the first item in |filtersArray|
// //
const activeFilter = FileBaseFilters.getActiveFilter(this.client); const activeFilter = FileBaseFilters.getActiveFilter(this.client);
this.filtersArray.sort( (filterA, filterB) => { this.filtersArray.sort( (filterA, filterB) => {
if(activeFilter) { if(activeFilter) {
if(filterA.uuid === activeFilter.uuid) { if(filterA.uuid === activeFilter.uuid) {
return -1; return -1;
} }
if(filterB.uuid === activeFilter.uuid) { if(filterB.uuid === activeFilter.uuid) {
return 1; return 1;
} }
} }
return filterA.name.localeCompare(filterB.name, { sensitivity : false, numeric : true } ); return filterA.name.localeCompare(filterB.name, { sensitivity : false, numeric : true } );
}); });
this.menuMethods = { this.menuMethods = {
saveFilter : (formData, extraArgs, cb) => { saveFilter : (formData, extraArgs, cb) => {
return this.saveCurrentFilter(formData, cb); return this.saveCurrentFilter(formData, cb);
}, },
prevFilter : (formData, extraArgs, cb) => { prevFilter : (formData, extraArgs, cb) => {
this.currentFilterIndex -= 1; this.currentFilterIndex -= 1;
if(this.currentFilterIndex < 0) { if(this.currentFilterIndex < 0) {
this.currentFilterIndex = this.filtersArray.length - 1; this.currentFilterIndex = this.filtersArray.length - 1;
} }
this.loadDataForFilter(this.currentFilterIndex); this.loadDataForFilter(this.currentFilterIndex);
return cb(null); return cb(null);
}, },
nextFilter : (formData, extraArgs, cb) => { nextFilter : (formData, extraArgs, cb) => {
this.currentFilterIndex += 1; this.currentFilterIndex += 1;
if(this.currentFilterIndex >= this.filtersArray.length) { if(this.currentFilterIndex >= this.filtersArray.length) {
this.currentFilterIndex = 0; this.currentFilterIndex = 0;
} }
this.loadDataForFilter(this.currentFilterIndex); this.loadDataForFilter(this.currentFilterIndex);
return cb(null); return cb(null);
}, },
makeFilterActive : (formData, extraArgs, cb) => { makeFilterActive : (formData, extraArgs, cb) => {
const filters = new FileBaseFilters(this.client); const filters = new FileBaseFilters(this.client);
filters.setActive(this.filtersArray[this.currentFilterIndex].uuid); filters.setActive(this.filtersArray[this.currentFilterIndex].uuid);
this.updateActiveLabel(); this.updateActiveLabel();
return cb(null); return cb(null);
}, },
newFilter : (formData, extraArgs, cb) => { newFilter : (formData, extraArgs, cb) => {
this.currentFilterIndex = this.filtersArray.length; // next avail slot this.currentFilterIndex = this.filtersArray.length; // next avail slot
this.clearForm(MciViewIds.editor.searchTerms); this.clearForm(MciViewIds.editor.searchTerms);
return cb(null); return cb(null);
}, },
deleteFilter : (formData, extraArgs, cb) => { deleteFilter : (formData, extraArgs, cb) => {
const selectedFilter = this.filtersArray[this.currentFilterIndex]; const selectedFilter = this.filtersArray[this.currentFilterIndex];
const filterUuid = selectedFilter.uuid; const filterUuid = selectedFilter.uuid;
// cannot delete built-in/system filters // cannot delete built-in/system filters
if(true === selectedFilter.system) { if(true === selectedFilter.system) {
this.showError('Cannot delete built in filters!'); this.showError('Cannot delete built in filters!');
return cb(null); return cb(null);
} }
this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry
// remove from stored properties // remove from stored properties
const filters = new FileBaseFilters(this.client); const filters = new FileBaseFilters(this.client);
filters.remove(filterUuid); filters.remove(filterUuid);
filters.persist( () => { filters.persist( () => {
// //
// If the item was also the active filter, we need to make a new one active // If the item was also the active filter, we need to make a new one active
// //
if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) { if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) {
const newActive = this.filtersArray[this.currentFilterIndex]; const newActive = this.filtersArray[this.currentFilterIndex];
if(newActive) { if(newActive) {
filters.setActive(newActive.uuid); filters.setActive(newActive.uuid);
} else { } else {
// nothing to set active to // nothing to set active to
this.client.user.removeProperty('file_base_filter_active_uuid'); this.client.user.removeProperty('file_base_filter_active_uuid');
} }
} }
// update UI // update UI
this.updateActiveLabel(); this.updateActiveLabel();
if(this.filtersArray.length > 0) { if(this.filtersArray.length > 0) {
this.loadDataForFilter(this.currentFilterIndex); this.loadDataForFilter(this.currentFilterIndex);
} else { } else {
this.clearForm(); this.clearForm();
} }
return cb(null); return cb(null);
}); });
}, },
viewValidationListener : (err, cb) => { viewValidationListener : (err, cb) => {
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error); const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
let newFocusId; let newFocusId;
if(errorView) { if(errorView) {
if(err) { if(err) {
errorView.setText(err.message); errorView.setText(err.message);
err.view.clearText(); // clear out the invalid data err.view.clearText(); // clear out the invalid data
} else { } else {
errorView.clearText(); errorView.clearText();
} }
} }
return cb(newFocusId); return cb(newFocusId);
}, },
}; };
} }
showError(errMsg) { showError(errMsg) {
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error); const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
if(errorView) { if(errorView) {
if(errMsg) { if(errMsg) {
errorView.setText(errMsg); errorView.setText(errMsg);
} else { } else {
errorView.clearText(); errorView.clearText();
} }
} }
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) ); const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) );
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
}, },
function populateAreas(callback) { function populateAreas(callback) {
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []);
const areasView = vc.getView(MciViewIds.editor.area); const areasView = vc.getView(MciViewIds.editor.area);
if(areasView) { if(areasView) {
areasView.setItems( self.availAreas.map( a => a.name ) ); areasView.setItems( self.availAreas.map( a => a.name ) );
} }
self.updateActiveLabel(); self.updateActiveLabel();
self.loadDataForFilter(self.currentFilterIndex); self.loadDataForFilter(self.currentFilterIndex);
self.viewControllers.editor.resetInitialFocus(); self.viewControllers.editor.resetInitialFocus();
return callback(null); return callback(null);
} }
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
}); });
} }
getCurrentFilter() { getCurrentFilter() {
return this.filtersArray[this.currentFilterIndex]; return this.filtersArray[this.currentFilterIndex];
} }
setText(mciId, text) { setText(mciId, text) {
const view = this.viewControllers.editor.getView(mciId); const view = this.viewControllers.editor.getView(mciId);
if(view) { if(view) {
view.setText(text); view.setText(text);
} }
} }
updateActiveLabel() { updateActiveLabel() {
const activeFilter = FileBaseFilters.getActiveFilter(this.client); const activeFilter = FileBaseFilters.getActiveFilter(this.client);
if(activeFilter) { if(activeFilter) {
const activeFormat = this.menuConfig.config.activeFormat || '{name}'; const activeFormat = this.menuConfig.config.activeFormat || '{name}';
this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter)); this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter));
} }
} }
setFocusItemIndex(mciId, index) { setFocusItemIndex(mciId, index) {
const view = this.viewControllers.editor.getView(mciId); const view = this.viewControllers.editor.getView(mciId);
if(view) { if(view) {
view.setFocusItemIndex(index); view.setFocusItemIndex(index);
} }
} }
clearForm(newFocusId) { clearForm(newFocusId) {
[ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => { [ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => {
this.setText(mciId, ''); this.setText(mciId, '');
}); });
[ MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort ].forEach(mciId => { [ MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort ].forEach(mciId => {
this.setFocusItemIndex(mciId, 0); this.setFocusItemIndex(mciId, 0);
}); });
if(newFocusId) { if(newFocusId) {
this.viewControllers.editor.switchFocus(newFocusId); this.viewControllers.editor.switchFocus(newFocusId);
} else { } else {
this.viewControllers.editor.resetInitialFocus(); this.viewControllers.editor.resetInitialFocus();
} }
} }
getSelectedAreaTag(index) { getSelectedAreaTag(index) {
if(0 === index) { if(0 === index) {
return ''; // -ALL- return ''; // -ALL-
} }
const area = this.availAreas[index]; const area = this.availAreas[index];
if(!area) { if(!area) {
return ''; return '';
} }
return area.areaTag; return area.areaTag;
} }
getOrderBy(index) { getOrderBy(index) {
return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0]; return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0];
} }
setAreaIndexFromCurrentFilter() { setAreaIndexFromCurrentFilter() {
let index; let index;
const filter = this.getCurrentFilter(); const filter = this.getCurrentFilter();
if(filter) { if(filter) {
// special treatment: areaTag saved as blank ("") if -ALL- // special treatment: areaTag saved as blank ("") if -ALL-
index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0; index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0;
} else { } else {
index = 0; index = 0;
} }
this.setFocusItemIndex(MciViewIds.editor.area, index); this.setFocusItemIndex(MciViewIds.editor.area, index);
} }
setOrderByFromCurrentFilter() { setOrderByFromCurrentFilter() {
let index; let index;
const filter = this.getCurrentFilter(); const filter = this.getCurrentFilter();
if(filter) { if(filter) {
index = FileBaseFilters.OrderByValues.findIndex( ob => filter.order === ob ) || 0; index = FileBaseFilters.OrderByValues.findIndex( ob => filter.order === ob ) || 0;
} else { } else {
index = 0; index = 0;
} }
this.setFocusItemIndex(MciViewIds.editor.order, index); this.setFocusItemIndex(MciViewIds.editor.order, index);
} }
setSortByFromCurrentFilter() { setSortByFromCurrentFilter() {
let index; let index;
const filter = this.getCurrentFilter(); const filter = this.getCurrentFilter();
if(filter) { if(filter) {
index = FileBaseFilters.SortByValues.findIndex( sb => filter.sort === sb ) || 0; index = FileBaseFilters.SortByValues.findIndex( sb => filter.sort === sb ) || 0;
} else { } else {
index = 0; index = 0;
} }
this.setFocusItemIndex(MciViewIds.editor.sort, index); this.setFocusItemIndex(MciViewIds.editor.sort, index);
} }
getSortBy(index) { getSortBy(index) {
return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0]; return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0];
} }
setFilterValuesFromFormData(filter, formData) { setFilterValuesFromFormData(filter, formData) {
filter.name = formData.value.name; filter.name = formData.value.name;
filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex); filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex);
filter.terms = formData.value.searchTerms; filter.terms = formData.value.searchTerms;
filter.tags = formData.value.tags; filter.tags = formData.value.tags;
filter.order = this.getOrderBy(formData.value.orderByIndex); filter.order = this.getOrderBy(formData.value.orderByIndex);
filter.sort = this.getSortBy(formData.value.sortByIndex); filter.sort = this.getSortBy(formData.value.sortByIndex);
} }
saveCurrentFilter(formData, cb) { saveCurrentFilter(formData, cb) {
const filters = new FileBaseFilters(this.client); const filters = new FileBaseFilters(this.client);
const selectedFilter = this.filtersArray[this.currentFilterIndex]; const selectedFilter = this.filtersArray[this.currentFilterIndex];
if(selectedFilter) { if(selectedFilter) {
// *update* currently selected filter // *update* currently selected filter
this.setFilterValuesFromFormData(selectedFilter, formData); this.setFilterValuesFromFormData(selectedFilter, formData);
filters.replace(selectedFilter.uuid, selectedFilter); filters.replace(selectedFilter.uuid, selectedFilter);
} else { } else {
// add a new entry; note that UUID will be generated // add a new entry; note that UUID will be generated
const newFilter = {}; const newFilter = {};
this.setFilterValuesFromFormData(newFilter, formData); this.setFilterValuesFromFormData(newFilter, formData);
// set current to what we just saved // set current to what we just saved
newFilter.uuid = filters.add(newFilter); newFilter.uuid = filters.add(newFilter);
// add to our array (at current index position) // add to our array (at current index position)
this.filtersArray[this.currentFilterIndex] = newFilter; this.filtersArray[this.currentFilterIndex] = newFilter;
} }
return filters.persist(cb); return filters.persist(cb);
} }
loadDataForFilter(filterIndex) { loadDataForFilter(filterIndex) {
const filter = this.filtersArray[filterIndex]; const filter = this.filtersArray[filterIndex];
if(filter) { if(filter) {
this.setText(MciViewIds.editor.searchTerms, filter.terms); this.setText(MciViewIds.editor.searchTerms, filter.terms);
this.setText(MciViewIds.editor.tags, filter.tags); this.setText(MciViewIds.editor.tags, filter.tags);
this.setText(MciViewIds.editor.filterName, filter.name); this.setText(MciViewIds.editor.filterName, filter.name);
this.setAreaIndexFromCurrentFilter(); this.setAreaIndexFromCurrentFilter();
this.setSortByFromCurrentFilter(); this.setSortByFromCurrentFilter();
this.setOrderByFromCurrentFilter(); this.setOrderByFromCurrentFilter();
} }
} }
}; };

File diff suppressed because it is too large Load Diff

View File

@ -26,470 +26,470 @@ const mimeTypes = require('mime-types');
const yazl = require('yazl'); const yazl = require('yazl');
function notEnabledError() { function notEnabledError() {
return Errors.General('Web server is not enabled', ErrNotEnabled); return Errors.General('Web server is not enabled', ErrNotEnabled);
} }
class FileAreaWebAccess { class FileAreaWebAccess {
constructor() { constructor() {
this.hashids = new hashids(Config().general.boardName); this.hashids = new hashids(Config().general.boardName);
this.expireTimers = {}; // hashId->timer this.expireTimers = {}; // hashId->timer
} }
startup(cb) { startup(cb) {
const self = this; const self = this;
async.series( async.series(
[ [
function initFromDb(callback) { function initFromDb(callback) {
return self.load(callback); return self.load(callback);
}, },
function addWebRoute(callback) { function addWebRoute(callback) {
self.webServer = getServer(webServerPackageName); self.webServer = getServer(webServerPackageName);
if(!self.webServer) { if(!self.webServer) {
return callback(Errors.DoesNotExist(`Server with package name "${webServerPackageName}" does not exist`)); return callback(Errors.DoesNotExist(`Server with package name "${webServerPackageName}" does not exist`));
} }
if(self.isEnabled()) { if(self.isEnabled()) {
const routeAdded = self.webServer.instance.addRoute({ const routeAdded = self.webServer.instance.addRoute({
method : 'GET', method : 'GET',
path : Config().fileBase.web.routePath, path : Config().fileBase.web.routePath,
handler : self.routeWebRequest.bind(self), handler : self.routeWebRequest.bind(self),
}); });
return callback(routeAdded ? null : Errors.General('Failed adding route')); return callback(routeAdded ? null : Errors.General('Failed adding route'));
} else { } else {
return callback(null); // not enabled, but no error return callback(null); // not enabled, but no error
} }
} }
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
} }
shutdown(cb) { shutdown(cb) {
return cb(null); return cb(null);
} }
isEnabled() { isEnabled() {
return this.webServer.instance.isEnabled(); return this.webServer.instance.isEnabled();
} }
static getHashIdTypes() { static getHashIdTypes() {
return { return {
SingleFile : 0, SingleFile : 0,
BatchArchive : 1, BatchArchive : 1,
}; };
} }
load(cb) { load(cb) {
// //
// Load entries, register expiration timers // Load entries, register expiration timers
// //
FileDb.each( FileDb.each(
`SELECT hash_id, expire_timestamp `SELECT hash_id, expire_timestamp
FROM file_web_serve;`, FROM file_web_serve;`,
(err, row) => { (err, row) => {
if(row) { if(row) {
this.scheduleExpire(row.hash_id, moment(row.expire_timestamp)); this.scheduleExpire(row.hash_id, moment(row.expire_timestamp));
} }
}, },
err => { err => {
return cb(err); return cb(err);
} }
); );
} }
removeEntry(hashId) { removeEntry(hashId) {
// //
// Delete record from DB, and our timer // Delete record from DB, and our timer
// //
FileDb.run( FileDb.run(
`DELETE FROM file_web_serve `DELETE FROM file_web_serve
WHERE hash_id = ?;`, WHERE hash_id = ?;`,
[ hashId ] [ hashId ]
); );
delete this.expireTimers[hashId]; delete this.expireTimers[hashId];
} }
scheduleExpire(hashId, expireTime) { scheduleExpire(hashId, expireTime) {
// remove any previous entry for this hashId // remove any previous entry for this hashId
const previous = this.expireTimers[hashId]; const previous = this.expireTimers[hashId];
if(previous) { if(previous) {
clearTimeout(previous); clearTimeout(previous);
delete this.expireTimers[hashId]; delete this.expireTimers[hashId];
} }
const timeoutMs = expireTime.diff(moment()); const timeoutMs = expireTime.diff(moment());
if(timeoutMs <= 0) { if(timeoutMs <= 0) {
setImmediate( () => { setImmediate( () => {
this.removeEntry(hashId); this.removeEntry(hashId);
}); });
} else { } else {
this.expireTimers[hashId] = setTimeout( () => { this.expireTimers[hashId] = setTimeout( () => {
this.removeEntry(hashId); this.removeEntry(hashId);
}, timeoutMs); }, timeoutMs);
} }
} }
loadServedHashId(hashId, cb) { loadServedHashId(hashId, cb) {
FileDb.get( FileDb.get(
`SELECT expire_timestamp FROM `SELECT expire_timestamp FROM
file_web_serve file_web_serve
WHERE hash_id = ?`, WHERE hash_id = ?`,
[ hashId ], [ hashId ],
(err, result) => { (err, result) => {
if(err || !result) { if(err || !result) {
return cb(err ? err : Errors.DoesNotExist('Invalid or missing hash ID')); return cb(err ? err : Errors.DoesNotExist('Invalid or missing hash ID'));
} }
const decoded = this.hashids.decode(hashId); const decoded = this.hashids.decode(hashId);
// decode() should provide an array of [ userId, hashIdType, id, ... ] // decode() should provide an array of [ userId, hashIdType, id, ... ]
if(!Array.isArray(decoded) || decoded.length < 3) { if(!Array.isArray(decoded) || decoded.length < 3) {
return cb(Errors.Invalid('Invalid or unknown hash ID')); return cb(Errors.Invalid('Invalid or unknown hash ID'));
} }
const servedItem = { const servedItem = {
hashId : hashId, hashId : hashId,
userId : decoded[0], userId : decoded[0],
hashIdType : decoded[1], hashIdType : decoded[1],
expireTimestamp : moment(result.expire_timestamp), expireTimestamp : moment(result.expire_timestamp),
}; };
if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) { if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) {
servedItem.fileIds = decoded.slice(2); servedItem.fileIds = decoded.slice(2);
} }
return cb(null, servedItem); return cb(null, servedItem);
} }
); );
} }
getSingleFileHashId(client, fileEntry) { getSingleFileHashId(client, fileEntry) {
return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [ fileEntry.fileId ] ); return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [ fileEntry.fileId ] );
} }
getBatchArchiveHashId(client, batchId) { getBatchArchiveHashId(client, batchId) {
return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().BatchArchive, batchId); return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().BatchArchive, batchId);
} }
getHashId(client, hashIdType, identifier) { getHashId(client, hashIdType, identifier) {
return this.hashids.encode(client.user.userId, hashIdType, identifier); return this.hashids.encode(client.user.userId, hashIdType, identifier);
} }
buildSingleFileTempDownloadLink(client, fileEntry, hashId) { buildSingleFileTempDownloadLink(client, fileEntry, hashId) {
hashId = hashId || this.getSingleFileHashId(client, fileEntry); hashId = hashId || this.getSingleFileHashId(client, fileEntry);
return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`); return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`);
} }
buildBatchArchiveTempDownloadLink(client, hashId) { buildBatchArchiveTempDownloadLink(client, hashId) {
return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`); return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`);
} }
getExistingTempDownloadServeItem(client, fileEntry, cb) { getExistingTempDownloadServeItem(client, fileEntry, cb) {
if(!this.isEnabled()) { if(!this.isEnabled()) {
return cb(notEnabledError()); return cb(notEnabledError());
} }
const hashId = this.getSingleFileHashId(client, fileEntry); const hashId = this.getSingleFileHashId(client, fileEntry);
this.loadServedHashId(hashId, (err, servedItem) => { this.loadServedHashId(hashId, (err, servedItem) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
servedItem.url = this.buildSingleFileTempDownloadLink(client, fileEntry); servedItem.url = this.buildSingleFileTempDownloadLink(client, fileEntry);
return cb(null, servedItem); return cb(null, servedItem);
}); });
} }
_addOrUpdateHashIdRecord(dbOrTrans, hashId, expireTime, cb) { _addOrUpdateHashIdRecord(dbOrTrans, hashId, expireTime, cb) {
// add/update rec with hash id and (latest) timestamp // add/update rec with hash id and (latest) timestamp
dbOrTrans.run( dbOrTrans.run(
`REPLACE INTO file_web_serve (hash_id, expire_timestamp) `REPLACE INTO file_web_serve (hash_id, expire_timestamp)
VALUES (?, ?);`, VALUES (?, ?);`,
[ hashId, getISOTimestampString(expireTime) ], [ hashId, getISOTimestampString(expireTime) ],
err => { err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
this.scheduleExpire(hashId, expireTime); this.scheduleExpire(hashId, expireTime);
return cb(null); return cb(null);
} }
); );
} }
createAndServeTempDownload(client, fileEntry, options, cb) { createAndServeTempDownload(client, fileEntry, options, cb) {
if(!this.isEnabled()) { if(!this.isEnabled()) {
return cb(notEnabledError()); return cb(notEnabledError());
} }
const hashId = this.getSingleFileHashId(client, fileEntry); const hashId = this.getSingleFileHashId(client, fileEntry);
const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId); const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId);
options.expireTime = options.expireTime || moment().add(2, 'days'); options.expireTime = options.expireTime || moment().add(2, 'days');
this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => { this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => {
return cb(err, url); return cb(err, url);
}); });
} }
createAndServeTempBatchDownload(client, fileEntries, options, cb) { createAndServeTempBatchDownload(client, fileEntries, options, cb) {
if(!this.isEnabled()) { if(!this.isEnabled()) {
return cb(notEnabledError()); return cb(notEnabledError());
} }
const batchId = moment().utc().unix(); const batchId = moment().utc().unix();
const hashId = this.getBatchArchiveHashId(client, batchId); const hashId = this.getBatchArchiveHashId(client, batchId);
const url = this.buildBatchArchiveTempDownloadLink(client, hashId); const url = this.buildBatchArchiveTempDownloadLink(client, hashId);
options.expireTime = options.expireTime || moment().add(2, 'days'); options.expireTime = options.expireTime || moment().add(2, 'days');
FileDb.beginTransaction( (err, trans) => { FileDb.beginTransaction( (err, trans) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => { this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => {
if(err) { if(err) {
return trans.rollback( () => { return trans.rollback( () => {
return cb(err); return cb(err);
}); });
} }
async.eachSeries(fileEntries, (entry, nextEntry) => { async.eachSeries(fileEntries, (entry, nextEntry) => {
trans.run( trans.run(
`INSERT INTO file_web_serve_batch (hash_id, file_id) `INSERT INTO file_web_serve_batch (hash_id, file_id)
VALUES (?, ?);`, VALUES (?, ?);`,
[ hashId, entry.fileId ], [ hashId, entry.fileId ],
err => { err => {
return nextEntry(err); return nextEntry(err);
} }
); );
}, err => { }, err => {
trans[err ? 'rollback' : 'commit']( () => { trans[err ? 'rollback' : 'commit']( () => {
return cb(err, url); return cb(err, url);
}); });
}); });
}); });
}); });
} }
fileNotFound(resp) { fileNotFound(resp) {
return this.webServer.instance.fileNotFound(resp); return this.webServer.instance.fileNotFound(resp);
} }
routeWebRequest(req, resp) { routeWebRequest(req, resp) {
const hashId = paths.basename(req.url); const hashId = paths.basename(req.url);
Log.debug( { hashId : hashId, url : req.url }, 'File area web request'); Log.debug( { hashId : hashId, url : req.url }, 'File area web request');
this.loadServedHashId(hashId, (err, servedItem) => { this.loadServedHashId(hashId, (err, servedItem) => {
if(err) { if(err) {
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
const hashIdTypes = FileAreaWebAccess.getHashIdTypes(); const hashIdTypes = FileAreaWebAccess.getHashIdTypes();
switch(servedItem.hashIdType) { switch(servedItem.hashIdType) {
case hashIdTypes.SingleFile : case hashIdTypes.SingleFile :
return this.routeWebRequestForSingleFile(servedItem, req, resp); return this.routeWebRequestForSingleFile(servedItem, req, resp);
case hashIdTypes.BatchArchive : case hashIdTypes.BatchArchive :
return this.routeWebRequestForBatchArchive(servedItem, req, resp); return this.routeWebRequestForBatchArchive(servedItem, req, resp);
default : default :
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
}); });
} }
routeWebRequestForSingleFile(servedItem, req, resp) { routeWebRequestForSingleFile(servedItem, req, resp) {
Log.debug( { servedItem : servedItem }, 'Single file web request'); Log.debug( { servedItem : servedItem }, 'Single file web request');
const fileEntry = new FileEntry(); const fileEntry = new FileEntry();
servedItem.fileId = servedItem.fileIds[0]; servedItem.fileId = servedItem.fileIds[0];
fileEntry.load(servedItem.fileId, err => { fileEntry.load(servedItem.fileId, err => {
if(err) { if(err) {
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
const filePath = fileEntry.filePath; const filePath = fileEntry.filePath;
if(!filePath) { if(!filePath) {
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
fs.stat(filePath, (err, stats) => { fs.stat(filePath, (err, stats) => {
if(err) { if(err) {
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
resp.on('close', () => { resp.on('close', () => {
// connection closed *before* the response was fully sent // connection closed *before* the response was fully sent
// :TODO: Log and such // :TODO: Log and such
}); });
resp.on('finish', () => { resp.on('finish', () => {
// transfer completed fully // transfer completed fully
this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]); this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]);
}); });
const headers = { const headers = {
'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'), 'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'),
'Content-Length' : stats.size, 'Content-Length' : stats.size,
'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`, 'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`,
}; };
const readStream = fs.createReadStream(filePath); const readStream = fs.createReadStream(filePath);
resp.writeHead(200, headers); resp.writeHead(200, headers);
return readStream.pipe(resp); return readStream.pipe(resp);
}); });
}); });
} }
routeWebRequestForBatchArchive(servedItem, req, resp) { routeWebRequestForBatchArchive(servedItem, req, resp) {
Log.debug( { servedItem : servedItem }, 'Batch file web request'); Log.debug( { servedItem : servedItem }, 'Batch file web request');
// //
// We are going to build an on-the-fly zip file stream of 1:n // We are going to build an on-the-fly zip file stream of 1:n
// files in the batch. // files in the batch.
// //
// First, collect all file IDs // First, collect all file IDs
// //
const self = this; const self = this;
async.waterfall( async.waterfall(
[ [
function fetchFileIds(callback) { function fetchFileIds(callback) {
FileDb.all( FileDb.all(
`SELECT file_id `SELECT file_id
FROM file_web_serve_batch FROM file_web_serve_batch
WHERE hash_id = ?;`, WHERE hash_id = ?;`,
[ servedItem.hashId ], [ servedItem.hashId ],
(err, fileIdRows) => { (err, fileIdRows) => {
if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) { if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) {
return callback(Errors.DoesNotExist('Could not get file IDs for batch')); return callback(Errors.DoesNotExist('Could not get file IDs for batch'));
} }
return callback(null, fileIdRows.map(r => r.file_id)); return callback(null, fileIdRows.map(r => r.file_id));
} }
); );
}, },
function loadFileEntries(fileIds, callback) { function loadFileEntries(fileIds, callback) {
async.map(fileIds, (fileId, nextFileId) => { async.map(fileIds, (fileId, nextFileId) => {
const fileEntry = new FileEntry(); const fileEntry = new FileEntry();
fileEntry.load(fileId, err => { fileEntry.load(fileId, err => {
return nextFileId(err, fileEntry); return nextFileId(err, fileEntry);
}); });
}, (err, fileEntries) => { }, (err, fileEntries) => {
if(err) { if(err) {
return callback(Errors.DoesNotExist('Could not load file IDs for batch')); return callback(Errors.DoesNotExist('Could not load file IDs for batch'));
} }
return callback(null, fileEntries); return callback(null, fileEntries);
}); });
}, },
function createAndServeStream(fileEntries, callback) { function createAndServeStream(fileEntries, callback) {
const filePaths = fileEntries.map(fe => fe.filePath); const filePaths = fileEntries.map(fe => fe.filePath);
Log.trace( { filePaths : filePaths }, 'Creating zip archive for batch web request'); Log.trace( { filePaths : filePaths }, 'Creating zip archive for batch web request');
const zipFile = new yazl.ZipFile(); const zipFile = new yazl.ZipFile();
zipFile.on('error', err => { zipFile.on('error', err => {
Log.warn( { error : err.message }, 'Error adding file to batch web request archive'); Log.warn( { error : err.message }, 'Error adding file to batch web request archive');
}); });
filePaths.forEach(fp => { filePaths.forEach(fp => {
zipFile.addFile( zipFile.addFile(
fp, // path to physical file fp, // path to physical file
paths.basename(fp), // filename/path *stored in archive* paths.basename(fp), // filename/path *stored in archive*
{ {
compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us. compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us.
} }
); );
}); });
zipFile.end( finalZipSize => { zipFile.end( finalZipSize => {
if(-1 === finalZipSize) { if(-1 === finalZipSize) {
return callback(Errors.UnexpectedState('Unable to acquire final zip size')); return callback(Errors.UnexpectedState('Unable to acquire final zip size'));
} }
resp.on('close', () => { resp.on('close', () => {
// connection closed *before* the response was fully sent // connection closed *before* the response was fully sent
// :TODO: Log and such // :TODO: Log and such
}); });
resp.on('finish', () => { resp.on('finish', () => {
// transfer completed fully // transfer completed fully
self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries); self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries);
}); });
const batchFileName = `batch_${servedItem.hashId}.zip`; const batchFileName = `batch_${servedItem.hashId}.zip`;
const headers = { const headers = {
'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'), 'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'),
'Content-Length' : finalZipSize, 'Content-Length' : finalZipSize,
'Content-Disposition' : `attachment; filename="${batchFileName}"`, 'Content-Disposition' : `attachment; filename="${batchFileName}"`,
}; };
resp.writeHead(200, headers); resp.writeHead(200, headers);
return zipFile.outputStream.pipe(resp); return zipFile.outputStream.pipe(resp);
}); });
} }
], ],
err => { err => {
if(err) { if(err) {
// :TODO: Log me! // :TODO: Log me!
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
// ...otherwise, we would have called resp() already. // ...otherwise, we would have called resp() already.
} }
); );
} }
updateDownloadStatsForUserIdAndSystem(userId, dlBytes, fileEntries) { updateDownloadStatsForUserIdAndSystem(userId, dlBytes, fileEntries) {
async.waterfall( async.waterfall(
[ [
function fetchActiveUser(callback) { function fetchActiveUser(callback) {
const clientForUserId = getConnectionByUserId(userId); const clientForUserId = getConnectionByUserId(userId);
if(clientForUserId) { if(clientForUserId) {
return callback(null, clientForUserId.user); return callback(null, clientForUserId.user);
} }
// not online now - look 'em up // not online now - look 'em up
User.getUser(userId, (err, assocUser) => { User.getUser(userId, (err, assocUser) => {
return callback(err, assocUser); return callback(err, assocUser);
}); });
}, },
function updateStats(user, callback) { function updateStats(user, callback) {
StatLog.incrementUserStat(user, 'dl_total_count', 1); StatLog.incrementUserStat(user, 'dl_total_count', 1);
StatLog.incrementUserStat(user, 'dl_total_bytes', dlBytes); StatLog.incrementUserStat(user, 'dl_total_bytes', dlBytes);
StatLog.incrementSystemStat('dl_total_count', 1); StatLog.incrementSystemStat('dl_total_count', 1);
StatLog.incrementSystemStat('dl_total_bytes', dlBytes); StatLog.incrementSystemStat('dl_total_bytes', dlBytes);
return callback(null, user); return callback(null, user);
}, },
function sendEvent(user, callback) { function sendEvent(user, callback) {
Events.emit( Events.emit(
Events.getSystemEvents().UserDownload, Events.getSystemEvents().UserDownload,
{ {
user : user, user : user,
files : fileEntries, files : fileEntries,
} }
); );
return callback(null); return callback(null);
} }
] ]
); );
} }
} }
module.exports = new FileAreaWebAccess(); module.exports = new FileAreaWebAccess();

File diff suppressed because it is too large Load Diff

View File

@ -10,78 +10,78 @@ const StatLog = require('./stat_log.js');
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Area Selector', name : 'File Area Selector',
desc : 'Select from available file areas', desc : 'Select from available file areas',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
areaList : 1, areaList : 1,
}; };
exports.getModule = class FileAreaSelectModule extends MenuModule { exports.getModule = class FileAreaSelectModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.menuMethods = { this.menuMethods = {
selectArea : (formData, extraArgs, cb) => { selectArea : (formData, extraArgs, cb) => {
const filterCriteria = { const filterCriteria = {
areaTag : formData.value.areaTag, areaTag : formData.value.areaTag,
}; };
const menuOpts = { const menuOpts = {
extraArgs : { extraArgs : {
filterCriteria : filterCriteria, filterCriteria : filterCriteria,
}, },
menuFlags : [ 'popParent', 'mergeFlags' ], menuFlags : [ 'popParent', 'mergeFlags' ],
}; };
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
} }
}; };
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
async.waterfall( async.waterfall(
[ [
function mergeAreaStats(callback) { function mergeAreaStats(callback) {
const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} }; const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} };
// we could use 'sort' alone, but area/conf sorting has some special properties; user can still override // we could use 'sort' alone, but area/conf sorting has some special properties; user can still override
const availAreas = getSortedAvailableFileAreas(self.client); const availAreas = getSortedAvailableFileAreas(self.client);
availAreas.forEach(area => { availAreas.forEach(area => {
const stats = areaStats.areas[area.areaTag]; const stats = areaStats.areas[area.areaTag];
area.totalFiles = stats ? stats.files : 0; area.totalFiles = stats ? stats.files : 0;
area.totalBytes = stats ? stats.bytes : 0; area.totalBytes = stats ? stats.bytes : 0;
}); });
return callback(null, availAreas); return callback(null, availAreas);
}, },
function prepView(availAreas, callback) { function prepView(availAreas, callback) {
self.prepViewController('allViews', 0, mciData.menu, (err, vc) => { self.prepViewController('allViews', 0, mciData.menu, (err, vc) => {
if(err) { if(err) {
return callback(err); return callback(err);
} }
const areaListView = vc.getView(MciViewIds.areaList); const areaListView = vc.getView(MciViewIds.areaList);
areaListView.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } ))); areaListView.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } )));
areaListView.redraw(); areaListView.redraw();
return callback(null); return callback(null);
}); });
} }
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
}); });
} }
}; };

View File

@ -17,226 +17,226 @@ const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Base Download Queue Manager', name : 'File Base Download Queue Manager',
desc : 'Module for interacting with download queue/batch', desc : 'Module for interacting with download queue/batch',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const FormIds = { const FormIds = {
queueManager : 0, queueManager : 0,
}; };
const MciViewIds = { const MciViewIds = {
queueManager : { queueManager : {
queue : 1, queue : 1,
navMenu : 2, navMenu : 2,
customRangeStart : 10, customRangeStart : 10,
}, },
}; };
exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.dlQueue = new DownloadQueue(this.client); this.dlQueue = new DownloadQueue(this.client);
if(_.has(options, 'lastMenuResult.sentFileIds')) { if(_.has(options, 'lastMenuResult.sentFileIds')) {
this.sentFileIds = options.lastMenuResult.sentFileIds; this.sentFileIds = options.lastMenuResult.sentFileIds;
} }
this.fallbackOnly = options.lastMenuResult ? true : false; this.fallbackOnly = options.lastMenuResult ? true : false;
this.menuMethods = { this.menuMethods = {
downloadAll : (formData, extraArgs, cb) => { downloadAll : (formData, extraArgs, cb) => {
const modOpts = { const modOpts = {
extraArgs : { extraArgs : {
sendQueue : this.dlQueue.items, sendQueue : this.dlQueue.items,
direction : 'send', direction : 'send',
} }
}; };
return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb); return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb);
}, },
removeItem : (formData, extraArgs, cb) => { removeItem : (formData, extraArgs, cb) => {
const selectedItem = this.dlQueue.items[formData.value.queueItem]; const selectedItem = this.dlQueue.items[formData.value.queueItem];
if(!selectedItem) { if(!selectedItem) {
return cb(null); return cb(null);
} }
this.dlQueue.removeItems(selectedItem.fileId); this.dlQueue.removeItems(selectedItem.fileId);
// :TODO: broken: does not redraw menu properly - needs fixed! // :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb);
}, },
clearQueue : (formData, extraArgs, cb) => { clearQueue : (formData, extraArgs, cb) => {
this.dlQueue.clear(); this.dlQueue.clear();
// :TODO: broken: does not redraw menu properly - needs fixed! // :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView('all', cb); return this.removeItemsFromDownloadQueueView('all', cb);
} }
}; };
} }
initSequence() { initSequence() {
if(0 === this.dlQueue.items.length) { if(0 === this.dlQueue.items.length) {
if(this.sendFileIds) { if(this.sendFileIds) {
// we've finished everything up - just fall back // we've finished everything up - just fall back
return this.prevMenu(); return this.prevMenu();
} }
// Simply an empty D/L queue: Present a specialized "empty queue" page // Simply an empty D/L queue: Present a specialized "empty queue" page
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue');
} }
const self = this; const self = this;
async.series( async.series(
[ [
function beforeArt(callback) { function beforeArt(callback) {
return self.beforeArt(callback); return self.beforeArt(callback);
}, },
function display(callback) { function display(callback) {
return self.displayQueueManagerPage(false, callback); return self.displayQueueManagerPage(false, callback);
} }
], ],
() => { () => {
return self.finishedLoading(); return self.finishedLoading();
} }
); );
} }
removeItemsFromDownloadQueueView(itemIndex, cb) { removeItemsFromDownloadQueueView(itemIndex, cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
if(!queueView) { if(!queueView) {
return cb(Errors.DoesNotExist('Queue view does not exist')); return cb(Errors.DoesNotExist('Queue view does not exist'));
} }
if('all' === itemIndex) { if('all' === itemIndex) {
queueView.setItems([]); queueView.setItems([]);
queueView.setFocusItems([]); queueView.setFocusItems([]);
} else { } else {
queueView.removeItem(itemIndex); queueView.removeItem(itemIndex);
} }
queueView.redraw(); queueView.redraw();
return cb(null); return cb(null);
} }
displayWebDownloadLinkForFileEntry(fileEntry) { displayWebDownloadLinkForFileEntry(fileEntry) {
FileAreaWeb.getExistingTempDownloadServeItem(this.client, fileEntry, (err, serveItem) => { FileAreaWeb.getExistingTempDownloadServeItem(this.client, fileEntry, (err, serveItem) => {
if(serveItem && serveItem.url) { if(serveItem && serveItem.url) {
const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
} else { } else {
fileEntry.webDlLink = ''; fileEntry.webDlLink = '';
fileEntry.webDlExpire = ''; fileEntry.webDlExpire = '';
} }
this.updateCustomViewTextsWithFilter( this.updateCustomViewTextsWithFilter(
'queueManager', 'queueManager',
MciViewIds.queueManager.customRangeStart, fileEntry, MciViewIds.queueManager.customRangeStart, fileEntry,
{ filter : [ '{webDlLink}', '{webDlExpire}' ] } { filter : [ '{webDlLink}', '{webDlExpire}' ] }
); );
}); });
} }
updateDownloadQueueView(cb) { updateDownloadQueueView(cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
if(!queueView) { if(!queueView) {
return cb(Errors.DoesNotExist('Queue view does not exist')); return cb(Errors.DoesNotExist('Queue view does not exist'));
} }
const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}'; const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}';
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
queueView.on('index update', idx => { queueView.on('index update', idx => {
const fileEntry = this.dlQueue.items[idx]; const fileEntry = this.dlQueue.items[idx];
this.displayWebDownloadLinkForFileEntry(fileEntry); this.displayWebDownloadLinkForFileEntry(fileEntry);
}); });
queueView.redraw(); queueView.redraw();
this.displayWebDownloadLinkForFileEntry(this.dlQueue.items[0]); this.displayWebDownloadLinkForFileEntry(this.dlQueue.items[0]);
return cb(null); return cb(null);
} }
displayQueueManagerPage(clearScreen, cb) { displayQueueManagerPage(clearScreen, cb) {
const self = this; const self = this;
async.series( async.series(
[ [
function prepArtAndViewController(callback) { function prepArtAndViewController(callback) {
return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback);
}, },
function populateViews(callback) { function populateViews(callback) {
return self.updateDownloadQueueView(callback); return self.updateDownloadQueueView(callback);
} }
], ],
err => { err => {
if(cb) { if(cb) {
return cb(err); return cb(err);
} }
} }
); );
} }
displayArtAndPrepViewController(name, options, cb) { displayArtAndPrepViewController(name, options, cb) {
const self = this; const self = this;
const config = this.menuConfig.config; const config = this.menuConfig.config;
async.waterfall( async.waterfall(
[ [
function readyAndDisplayArt(callback) { function readyAndDisplayArt(callback) {
if(options.clearScreen) { if(options.clearScreen) {
self.client.term.rawWrite(ansi.resetScreen()); self.client.term.rawWrite(ansi.resetScreen());
} }
theme.displayThemedAsset( theme.displayThemedAsset(
config.art[name], config.art[name],
self.client, self.client,
{ font : self.menuConfig.font, trailingLF : false }, { font : self.menuConfig.font, trailingLF : false },
(err, artData) => { (err, artData) => {
return callback(err, artData); return callback(err, artData);
} }
); );
}, },
function prepeareViewController(artData, callback) { function prepeareViewController(artData, callback) {
if(_.isUndefined(self.viewControllers[name])) { if(_.isUndefined(self.viewControllers[name])) {
const vcOpts = { const vcOpts = {
client : self.client, client : self.client,
formId : FormIds[name], formId : FormIds[name],
}; };
if(!_.isUndefined(options.noInput)) { if(!_.isUndefined(options.noInput)) {
vcOpts.noInput = options.noInput; vcOpts.noInput = options.noInput;
} }
const vc = self.addViewController(name, new ViewController(vcOpts)); const vc = self.addViewController(name, new ViewController(vcOpts));
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds[name], formId : FormIds[name],
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
} }
self.viewControllers[name].setFocus(true); self.viewControllers[name].setFocus(true);
return callback(null); return callback(null);
}, },
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
} }
}; };

View File

@ -6,150 +6,150 @@ const _ = require('lodash');
const uuidV4 = require('uuid/v4'); const uuidV4 = require('uuid/v4');
module.exports = class FileBaseFilters { module.exports = class FileBaseFilters {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
this.load(); this.load();
} }
static get OrderByValues() { static get OrderByValues() {
return [ 'descending', 'ascending' ]; return [ 'descending', 'ascending' ];
} }
static get SortByValues() { static get SortByValues() {
return [ return [
'upload_timestamp', 'upload_timestamp',
'upload_by_username', 'upload_by_username',
'dl_count', 'dl_count',
'user_rating', 'user_rating',
'est_release_year', 'est_release_year',
'byte_size', 'byte_size',
'file_name', 'file_name',
]; ];
} }
toArray() { toArray() {
return _.map(this.filters, (filter, uuid) => { return _.map(this.filters, (filter, uuid) => {
return Object.assign( { uuid : uuid }, filter ); return Object.assign( { uuid : uuid }, filter );
}); });
} }
get(filterUuid) { get(filterUuid) {
return this.filters[filterUuid]; return this.filters[filterUuid];
} }
add(filterInfo) { add(filterInfo) {
const filterUuid = uuidV4(); const filterUuid = uuidV4();
filterInfo.tags = this.cleanTags(filterInfo.tags); filterInfo.tags = this.cleanTags(filterInfo.tags);
this.filters[filterUuid] = filterInfo; this.filters[filterUuid] = filterInfo;
return filterUuid; return filterUuid;
} }
replace(filterUuid, filterInfo) { replace(filterUuid, filterInfo) {
const filter = this.get(filterUuid); const filter = this.get(filterUuid);
if(!filter) { if(!filter) {
return false; return false;
} }
filterInfo.tags = this.cleanTags(filterInfo.tags); filterInfo.tags = this.cleanTags(filterInfo.tags);
this.filters[filterUuid] = filterInfo; this.filters[filterUuid] = filterInfo;
return true; return true;
} }
remove(filterUuid) { remove(filterUuid) {
delete this.filters[filterUuid]; delete this.filters[filterUuid];
} }
load() { load() {
let filtersProperty = this.client.user.properties.file_base_filters; let filtersProperty = this.client.user.properties.file_base_filters;
let defaulted; let defaulted;
if(!filtersProperty) { if(!filtersProperty) {
filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters()); filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters());
defaulted = true; defaulted = true;
} }
try { try {
this.filters = JSON.parse(filtersProperty); this.filters = JSON.parse(filtersProperty);
} catch(e) { } catch(e) {
this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :( this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :(
defaulted = true; defaulted = true;
this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' ); this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' );
} }
if(defaulted) { if(defaulted) {
this.persist( err => { this.persist( err => {
if(!err) { if(!err) {
const defaultActiveUuid = this.toArray()[0].uuid; const defaultActiveUuid = this.toArray()[0].uuid;
this.setActive(defaultActiveUuid); this.setActive(defaultActiveUuid);
} }
}); });
} }
} }
persist(cb) { persist(cb) {
return this.client.user.persistProperty('file_base_filters', JSON.stringify(this.filters), cb); return this.client.user.persistProperty('file_base_filters', JSON.stringify(this.filters), cb);
} }
cleanTags(tags) { cleanTags(tags) {
return tags.toLowerCase().replace(/,?\s+|,/g, ' ').trim(); return tags.toLowerCase().replace(/,?\s+|,/g, ' ').trim();
} }
setActive(filterUuid) { setActive(filterUuid) {
const activeFilter = this.get(filterUuid); const activeFilter = this.get(filterUuid);
if(activeFilter) { if(activeFilter) {
this.activeFilter = activeFilter; this.activeFilter = activeFilter;
this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid); this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid);
return true; return true;
} }
return false; return false;
} }
static getBuiltInSystemFilters() { static getBuiltInSystemFilters() {
const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329'; const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329';
const filters = { const filters = {
[ U_LATEST ] : { [ U_LATEST ] : {
name : 'By Date Added', name : 'By Date Added',
areaTag : '', // all areaTag : '', // all
terms : '', // * terms : '', // *
tags : '', // * tags : '', // *
order : 'descending', order : 'descending',
sort : 'upload_timestamp', sort : 'upload_timestamp',
uuid : U_LATEST, uuid : U_LATEST,
system : true, system : true,
} }
}; };
return filters; return filters;
} }
static getActiveFilter(client) { static getActiveFilter(client) {
return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid); return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid);
} }
static getFileBaseLastViewedFileIdByUser(user) { static getFileBaseLastViewedFileIdByUser(user) {
return parseInt((user.properties.user_file_base_last_viewed || 0)); return parseInt((user.properties.user_file_base_last_viewed || 0));
} }
static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) { static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) {
if(!cb && _.isFunction(allowOlder)) { if(!cb && _.isFunction(allowOlder)) {
cb = allowOlder; cb = allowOlder;
allowOlder = false; allowOlder = false;
} }
const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user); const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user);
if(!allowOlder && fileId < current) { if(!allowOlder && fileId < current) {
if(cb) { if(cb) {
cb(null); cb(null);
} }
return; return;
} }
return user.persistProperty('user_file_base_last_viewed', fileId, cb); return user.persistProperty('user_file_base_last_viewed', fileId, cb);
} }
}; };

View File

@ -8,8 +8,8 @@ const FileArea = require('./file_base_area.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const { const {
splitTextAtTerms, splitTextAtTerms,
isAnsi, isAnsi,
} = require('./string_util.js'); } = require('./string_util.js');
const AnsiPrep = require('./ansi_prep.js'); const AnsiPrep = require('./ansi_prep.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
@ -26,276 +26,276 @@ exports.exportFileList = exportFileList;
exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent; exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent;
function exportFileList(filterCriteria, options, cb) { function exportFileList(filterCriteria, options, cb) {
options.templateEncoding = options.templateEncoding || 'utf8'; options.templateEncoding = options.templateEncoding || 'utf8';
options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc'; options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc';
options.tsFormat = options.tsFormat || 'YYYY-MM-DD'; options.tsFormat = options.tsFormat || 'YYYY-MM-DD';
options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec
options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc? options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc?
if(true === options.escapeDesc) { if(true === options.escapeDesc) {
options.escapeDesc = '\\n'; options.escapeDesc = '\\n';
} }
const state = { const state = {
total : 0, total : 0,
current : 0, current : 0,
step : 'preparing', step : 'preparing',
status : 'Preparing', status : 'Preparing',
}; };
const updateProgress = _.isFunction(options.progress) ? const updateProgress = _.isFunction(options.progress) ?
progCb => { progCb => {
return options.progress(state, progCb); return options.progress(state, progCb);
} : } :
progCb => { progCb => {
return progCb(null); return progCb(null);
} }
; ;
async.waterfall( async.waterfall(
[ [
function readTemplateFiles(callback) { function readTemplateFiles(callback) {
updateProgress(err => { updateProgress(err => {
if(err) { if(err) {
return callback(err); return callback(err);
} }
const templateFiles = [ const templateFiles = [
{ name : options.headerTemplate, req : false }, { name : options.headerTemplate, req : false },
{ name : options.entryTemplate, req : true } { name : options.entryTemplate, req : true }
]; ];
const config = Config(); const config = Config();
async.map(templateFiles, (template, nextTemplate) => { async.map(templateFiles, (template, nextTemplate) => {
if(!template.name && !template.req) { if(!template.name && !template.req) {
return nextTemplate(null, Buffer.from([])); return nextTemplate(null, Buffer.from([]));
} }
template.name = paths.isAbsolute(template.name) ? template.name : paths.join(config.paths.misc, template.name); template.name = paths.isAbsolute(template.name) ? template.name : paths.join(config.paths.misc, template.name);
fs.readFile(template.name, (err, data) => { fs.readFile(template.name, (err, data) => {
return nextTemplate(err, data); return nextTemplate(err, data);
}); });
}, (err, templates) => { }, (err, templates) => {
if(err) { if(err) {
return callback(Errors.General(err.message)); return callback(Errors.General(err.message));
} }
// decode + ensure DOS style CRLF // decode + ensure DOS style CRLF
templates = templates.map(tmp => iconv.decode(tmp, options.templateEncoding).replace(/\r?\n/g, '\r\n') ); templates = templates.map(tmp => iconv.decode(tmp, options.templateEncoding).replace(/\r?\n/g, '\r\n') );
// Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements // Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements
let descIndent = 0; let descIndent = 0;
if(!options.escapeDesc) { if(!options.escapeDesc) {
splitTextAtTerms(templates[1]).some(line => { splitTextAtTerms(templates[1]).some(line => {
const pos = line.indexOf('{fileDesc}'); const pos = line.indexOf('{fileDesc}');
if(pos > -1) { if(pos > -1) {
descIndent = pos; descIndent = pos;
return true; // found it! return true; // found it!
} }
return false; // keep looking return false; // keep looking
}); });
} }
return callback(null, templates[0], templates[1], descIndent); return callback(null, templates[0], templates[1], descIndent);
}); });
}); });
}, },
function findFiles(headerTemplate, entryTemplate, descIndent, callback) { function findFiles(headerTemplate, entryTemplate, descIndent, callback) {
state.step = 'gathering'; state.step = 'gathering';
state.status = 'Gathering files for supplied criteria'; state.status = 'Gathering files for supplied criteria';
updateProgress(err => { updateProgress(err => {
if(err) { if(err) {
return callback(err); return callback(err);
} }
FileEntry.findFiles(filterCriteria, (err, fileIds) => { FileEntry.findFiles(filterCriteria, (err, fileIds) => {
if(0 === fileIds.length) { if(0 === fileIds.length) {
return callback(Errors.General('No results for criteria', 'NORESULTS')); return callback(Errors.General('No results for criteria', 'NORESULTS'));
} }
return callback(err, headerTemplate, entryTemplate, descIndent, fileIds); return callback(err, headerTemplate, entryTemplate, descIndent, fileIds);
}); });
}); });
}, },
function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) { function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) {
const formatObj = { const formatObj = {
totalFileCount : fileIds.length, totalFileCount : fileIds.length,
}; };
let current = 0; let current = 0;
let listBody = ''; let listBody = '';
const totals = { fileCount : fileIds.length, bytes : 0 }; const totals = { fileCount : fileIds.length, bytes : 0 };
state.total = fileIds.length; state.total = fileIds.length;
state.step = 'file'; state.step = 'file';
async.eachSeries(fileIds, (fileId, nextFileId) => { async.eachSeries(fileIds, (fileId, nextFileId) => {
const fileInfo = new FileEntry(); const fileInfo = new FileEntry();
current += 1; current += 1;
fileInfo.load(fileId, err => { fileInfo.load(fileId, err => {
if(err) { if(err) {
return nextFileId(null); // failed, but try the next return nextFileId(null); // failed, but try the next
} }
totals.bytes += fileInfo.meta.byte_size; totals.bytes += fileInfo.meta.byte_size;
const appendFileInfo = () => { const appendFileInfo = () => {
if(options.escapeDesc) { if(options.escapeDesc) {
formatObj.fileDesc = formatObj.fileDesc.replace(/\r?\n/g, options.escapeDesc); formatObj.fileDesc = formatObj.fileDesc.replace(/\r?\n/g, options.escapeDesc);
} }
if(options.maxDescLen) { if(options.maxDescLen) {
formatObj.fileDesc = formatObj.fileDesc.slice(0, options.maxDescLen); formatObj.fileDesc = formatObj.fileDesc.slice(0, options.maxDescLen);
} }
listBody += stringFormat(entryTemplate, formatObj); listBody += stringFormat(entryTemplate, formatObj);
state.current = current; state.current = current;
state.status = `Processing ${fileInfo.fileName}`; state.status = `Processing ${fileInfo.fileName}`;
state.fileInfo = formatObj; state.fileInfo = formatObj;
updateProgress(err => { updateProgress(err => {
return nextFileId(err); return nextFileId(err);
}); });
}; };
const area = FileArea.getFileAreaByTag(fileInfo.areaTag); const area = FileArea.getFileAreaByTag(fileInfo.areaTag);
formatObj.fileId = fileId; formatObj.fileId = fileId;
formatObj.areaName = _.get(area, 'name') || 'N/A'; formatObj.areaName = _.get(area, 'name') || 'N/A';
formatObj.areaDesc = _.get(area, 'desc') || 'N/A'; formatObj.areaDesc = _.get(area, 'desc') || 'N/A';
formatObj.userRating = fileInfo.userRating || 0; formatObj.userRating = fileInfo.userRating || 0;
formatObj.fileName = fileInfo.fileName; formatObj.fileName = fileInfo.fileName;
formatObj.fileSize = fileInfo.meta.byte_size; formatObj.fileSize = fileInfo.meta.byte_size;
formatObj.fileDesc = fileInfo.desc || ''; formatObj.fileDesc = fileInfo.desc || '';
formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth); formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth);
formatObj.fileSha256 = fileInfo.fileSha256; formatObj.fileSha256 = fileInfo.fileSha256;
formatObj.fileCrc32 = fileInfo.meta.file_crc32; formatObj.fileCrc32 = fileInfo.meta.file_crc32;
formatObj.fileMd5 = fileInfo.meta.file_md5; formatObj.fileMd5 = fileInfo.meta.file_md5;
formatObj.fileSha1 = fileInfo.meta.file_sha1; formatObj.fileSha1 = fileInfo.meta.file_sha1;
formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A'; formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A';
formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat); formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat);
formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A'; formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A';
formatObj.currentFile = current; formatObj.currentFile = current;
formatObj.progress = Math.floor( (current / fileIds.length) * 100 ); formatObj.progress = Math.floor( (current / fileIds.length) * 100 );
if(isAnsi(fileInfo.desc)) { if(isAnsi(fileInfo.desc)) {
AnsiPrep( AnsiPrep(
fileInfo.desc, fileInfo.desc,
{ {
cols : Math.min(options.descWidth, 79 - descIndent), cols : Math.min(options.descWidth, 79 - descIndent),
forceLineTerm : true, // ensure each line is term'd forceLineTerm : true, // ensure each line is term'd
asciiMode : true, // export to ASCII asciiMode : true, // export to ASCII
fillLines : false, // don't fill up to |cols| fillLines : false, // don't fill up to |cols|
indent : descIndent, indent : descIndent,
}, },
(err, desc) => { (err, desc) => {
if(desc) { if(desc) {
formatObj.fileDesc = desc; formatObj.fileDesc = desc;
} }
return appendFileInfo(); return appendFileInfo();
} }
); );
} else { } else {
const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : ''; const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : '';
formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n'; formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n';
return appendFileInfo(); return appendFileInfo();
} }
}); });
}, err => { }, err => {
return callback(err, listBody, headerTemplate, totals); return callback(err, listBody, headerTemplate, totals);
}); });
}, },
function buildHeader(listBody, headerTemplate, totals, callback) { function buildHeader(listBody, headerTemplate, totals, callback) {
// header is built last such that we can have totals/etc. // header is built last such that we can have totals/etc.
let filterAreaName; let filterAreaName;
let filterAreaDesc; let filterAreaDesc;
if(filterCriteria.areaTag) { if(filterCriteria.areaTag) {
const area = FileArea.getFileAreaByTag(filterCriteria.areaTag); const area = FileArea.getFileAreaByTag(filterCriteria.areaTag);
filterAreaName = _.get(area, 'name') || 'N/A'; filterAreaName = _.get(area, 'name') || 'N/A';
filterAreaDesc = _.get(area, 'desc') || 'N/A'; filterAreaDesc = _.get(area, 'desc') || 'N/A';
} else { } else {
filterAreaName = '-ALL-'; filterAreaName = '-ALL-';
filterAreaDesc = 'All areas'; filterAreaDesc = 'All areas';
} }
const headerFormatObj = { const headerFormatObj = {
nowTs : moment().format(options.tsFormat), nowTs : moment().format(options.tsFormat),
boardName : Config().general.boardName, boardName : Config().general.boardName,
totalFileCount : totals.fileCount, totalFileCount : totals.fileCount,
totalFileSize : totals.bytes, totalFileSize : totals.bytes,
filterAreaTag : filterCriteria.areaTag || '-ALL-', filterAreaTag : filterCriteria.areaTag || '-ALL-',
filterAreaName : filterAreaName, filterAreaName : filterAreaName,
filterAreaDesc : filterAreaDesc, filterAreaDesc : filterAreaDesc,
filterTerms : filterCriteria.terms || '(none)', filterTerms : filterCriteria.terms || '(none)',
filterHashTags : filterCriteria.tags || '(none)', filterHashTags : filterCriteria.tags || '(none)',
}; };
listBody = stringFormat(headerTemplate, headerFormatObj) + listBody; listBody = stringFormat(headerTemplate, headerFormatObj) + listBody;
return callback(null, listBody); return callback(null, listBody);
}, },
function done(listBody, callback) { function done(listBody, callback) {
delete state.fileInfo; delete state.fileInfo;
state.step = 'finished'; state.step = 'finished';
state.status = 'Finished processing'; state.status = 'Finished processing';
updateProgress( () => { updateProgress( () => {
return callback(null, listBody); return callback(null, listBody);
}); });
} }
], (err, listBody) => { ], (err, listBody) => {
return cb(err, listBody); return cb(err, listBody);
} }
); );
} }
function updateFileBaseDescFilesScheduledEvent(args, cb) { function updateFileBaseDescFilesScheduledEvent(args, cb) {
// //
// For each area, loop over storage locations and build // For each area, loop over storage locations and build
// DESCRIPT.ION file to store in the same directory. // DESCRIPT.ION file to store in the same directory.
// //
// Standard-ish 4DOS spec is as such: // Standard-ish 4DOS spec is as such:
// * Entry: <QUOTED_LFN> <DESC>[0x04<AppData>]\r\n // * Entry: <QUOTED_LFN> <DESC>[0x04<AppData>]\r\n
// * Multi line descriptions are stored with *escaped* \r\n pairs // * Multi line descriptions are stored with *escaped* \r\n pairs
// * Default template uses 0x2c for <AppData> as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec // * Default template uses 0x2c for <AppData> as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec
// //
const entryTemplate = args[0]; const entryTemplate = args[0];
const headerTemplate = args[1]; const headerTemplate = args[1];
const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck : true }); const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck : true });
async.each(areas, (area, nextArea) => { async.each(areas, (area, nextArea) => {
const storageLocations = FileArea.getAreaStorageLocations(area); const storageLocations = FileArea.getAreaStorageLocations(area);
async.each(storageLocations, (storageLoc, nextStorageLoc) => { async.each(storageLocations, (storageLoc, nextStorageLoc) => {
const filterCriteria = { const filterCriteria = {
areaTag : area.areaTag, areaTag : area.areaTag,
storageTag : storageLoc.storageTag, storageTag : storageLoc.storageTag,
}; };
const exportOpts = { const exportOpts = {
headerTemplate : headerTemplate, headerTemplate : headerTemplate,
entryTemplate : entryTemplate, entryTemplate : entryTemplate,
escapeDesc : true, // escape CRLF's escapeDesc : true, // escape CRLF's
maxDescLen : 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes" maxDescLen : 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes"
}; };
exportFileList(filterCriteria, exportOpts, (err, listBody) => { exportFileList(filterCriteria, exportOpts, (err, listBody) => {
const descIonPath = paths.join(storageLoc.dir, 'DESCRIPT.ION'); const descIonPath = paths.join(storageLoc.dir, 'DESCRIPT.ION');
fs.writeFile(descIonPath, iconv.encode(listBody, 'cp437'), err => { fs.writeFile(descIonPath, iconv.encode(listBody, 'cp437'), err => {
if(err) { if(err) {
Log.warn( { error : err.message, path : descIonPath }, 'Failed (re)creating DESCRIPT.ION'); Log.warn( { error : err.message, path : descIonPath }, 'Failed (re)creating DESCRIPT.ION');
} else { } else {
Log.debug( { path : descIonPath }, '(Re)generated DESCRIPT.ION'); Log.debug( { path : descIonPath }, '(Re)generated DESCRIPT.ION');
} }
return nextStorageLoc(null); return nextStorageLoc(null);
}); });
}); });
}, () => { }, () => {
return nextArea(null); return nextArea(null);
}); });
}, () => { }, () => {
return cb(null); return cb(null);
}); });
} }

View File

@ -11,110 +11,110 @@ const FileBaseFilters = require('./file_base_filter.js');
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Base Search', name : 'File Base Search',
desc : 'Module for quickly searching the file base', desc : 'Module for quickly searching the file base',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
search : { search : {
searchTerms : 1, searchTerms : 1,
search : 2, search : 2,
tags : 3, tags : 3,
area : 4, area : 4,
orderBy : 5, orderBy : 5,
sort : 6, sort : 6,
advSearch : 7, advSearch : 7,
} }
}; };
exports.getModule = class FileBaseSearch extends MenuModule { exports.getModule = class FileBaseSearch extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.menuMethods = { this.menuMethods = {
search : (formData, extraArgs, cb) => { search : (formData, extraArgs, cb) => {
const isAdvanced = formData.submitId === MciViewIds.search.advSearch; const isAdvanced = formData.submitId === MciViewIds.search.advSearch;
return this.searchNow(formData, isAdvanced, cb); return this.searchNow(formData, isAdvanced, cb);
}, },
}; };
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.addViewController( 'search', new ViewController( { client : this.client } ) ); const vc = self.addViewController( 'search', new ViewController( { client : this.client } ) );
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
}, },
function populateAreas(callback) { function populateAreas(callback) {
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []);
const areasView = vc.getView(MciViewIds.search.area); const areasView = vc.getView(MciViewIds.search.area);
areasView.setItems( self.availAreas.map( a => a.name ) ); areasView.setItems( self.availAreas.map( a => a.name ) );
areasView.redraw(); areasView.redraw();
vc.switchFocus(MciViewIds.search.searchTerms); vc.switchFocus(MciViewIds.search.searchTerms);
return callback(null); return callback(null);
} }
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
}); });
} }
getSelectedAreaTag(index) { getSelectedAreaTag(index) {
if(0 === index) { if(0 === index) {
return ''; // -ALL- return ''; // -ALL-
} }
const area = this.availAreas[index]; const area = this.availAreas[index];
if(!area) { if(!area) {
return ''; return '';
} }
return area.areaTag; return area.areaTag;
} }
getOrderBy(index) { getOrderBy(index) {
return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0]; return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0];
} }
getSortBy(index) { getSortBy(index) {
return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0]; return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0];
} }
getFilterValuesFromFormData(formData, isAdvanced) { getFilterValuesFromFormData(formData, isAdvanced) {
const areaIndex = isAdvanced ? formData.value.areaIndex : 0; const areaIndex = isAdvanced ? formData.value.areaIndex : 0;
const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0; const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0;
const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0; const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0;
return { return {
areaTag : this.getSelectedAreaTag(areaIndex), areaTag : this.getSelectedAreaTag(areaIndex),
terms : formData.value.searchTerms, terms : formData.value.searchTerms,
tags : isAdvanced ? formData.value.tags : '', tags : isAdvanced ? formData.value.tags : '',
order : this.getOrderBy(orderByIndex), order : this.getOrderBy(orderByIndex),
sort : this.getSortBy(sortByIndex), sort : this.getSortBy(sortByIndex),
}; };
} }
searchNow(formData, isAdvanced, cb) { searchNow(formData, isAdvanced, cb) {
const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced); const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced);
const menuOpts = { const menuOpts = {
extraArgs : { extraArgs : {
filterCriteria : filterCriteria, filterCriteria : filterCriteria,
}, },
menuFlags : [ 'popParent' ], menuFlags : [ 'popParent' ],
}; };
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
} }
}; };

View File

@ -46,249 +46,249 @@ const yazl = require('yazl');
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Base List Export', name : 'File Base List Export',
desc : 'Exports file base listings for download', desc : 'Exports file base listings for download',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const FormIds = { const FormIds = {
main : 0, main : 0,
}; };
const MciViewIds = { const MciViewIds = {
main : { main : {
status : 1, status : 1,
progressBar : 2, progressBar : 2,
customRangeStart : 10, customRangeStart : 10,
} }
}; };
exports.getModule = class FileBaseListExport extends MenuModule { exports.getModule = class FileBaseListExport extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs);
this.config.templateEncoding = this.config.templateEncoding || 'utf8'; this.config.templateEncoding = this.config.templateEncoding || 'utf8';
this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short');
this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ
this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1);
this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :) this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :)
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
async.series( async.series(
[ [
(callback) => this.prepViewController('main', FormIds.main, mciData.menu, callback), (callback) => this.prepViewController('main', FormIds.main, mciData.menu, callback),
(callback) => this.prepareList(callback), (callback) => this.prepareList(callback),
], ],
err => { err => {
if(err) { if(err) {
if('NORESULTS' === err.reasonCode) { if('NORESULTS' === err.reasonCode) {
return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'fileBaseExportListNoResults'); return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'fileBaseExportListNoResults');
} }
return this.prevMenu(); return this.prevMenu();
} }
return cb(err); return cb(err);
} }
); );
}); });
} }
finishedLoading() { finishedLoading() {
this.prevMenu(); this.prevMenu();
} }
prepareList(cb) { prepareList(cb) {
const self = this; const self = this;
const statusView = self.viewControllers.main.getView(MciViewIds.main.status); const statusView = self.viewControllers.main.getView(MciViewIds.main.status);
const updateStatus = (status) => { const updateStatus = (status) => {
if(statusView) { if(statusView) {
statusView.setText(status); statusView.setText(status);
} }
}; };
const progBarView = self.viewControllers.main.getView(MciViewIds.main.progressBar); const progBarView = self.viewControllers.main.getView(MciViewIds.main.progressBar);
const updateProgressBar = (curr, total) => { const updateProgressBar = (curr, total) => {
if(progBarView) { if(progBarView) {
const prog = Math.floor( (curr / total) * progBarView.dimens.width ); const prog = Math.floor( (curr / total) * progBarView.dimens.width );
progBarView.setText(self.config.progBarChar.repeat(prog)); progBarView.setText(self.config.progBarChar.repeat(prog));
} }
}; };
let cancel = false; let cancel = false;
const exportListProgress = (state, progNext) => { const exportListProgress = (state, progNext) => {
switch(state.step) { switch(state.step) {
case 'preparing' : case 'preparing' :
case 'gathering' : case 'gathering' :
updateStatus(state.status); updateStatus(state.status);
break; break;
case 'file' : case 'file' :
updateStatus(state.status); updateStatus(state.status);
updateProgressBar(state.current, state.total); updateProgressBar(state.current, state.total);
self.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state.fileInfo); self.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state.fileInfo);
break; break;
default : default :
break; break;
} }
return progNext(cancel ? Errors.General('User canceled') : null); return progNext(cancel ? Errors.General('User canceled') : null);
}; };
const keyPressHandler = (ch, key) => { const keyPressHandler = (ch, key) => {
if('escape' === key.name) { if('escape' === key.name) {
cancel = true; cancel = true;
self.client.removeListener('key press', keyPressHandler); self.client.removeListener('key press', keyPressHandler);
} }
}; };
async.waterfall( async.waterfall(
[ [
function buildList(callback) { function buildList(callback) {
// this may take quite a while; temp disable of idle monitor // this may take quite a while; temp disable of idle monitor
self.client.stopIdleMonitor(); self.client.stopIdleMonitor();
self.client.on('key press', keyPressHandler); self.client.on('key press', keyPressHandler);
const filterCriteria = Object.assign({}, self.config.filterCriteria); const filterCriteria = Object.assign({}, self.config.filterCriteria);
if(!filterCriteria.areaTag) { if(!filterCriteria.areaTag) {
filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(self.client); filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(self.client);
} }
const opts = { const opts = {
templateEncoding : self.config.templateEncoding, templateEncoding : self.config.templateEncoding,
headerTemplate : _.get(self.config, 'templates.header', 'file_list_header.asc'), headerTemplate : _.get(self.config, 'templates.header', 'file_list_header.asc'),
entryTemplate : _.get(self.config, 'templates.entry', 'file_list_entry.asc'), entryTemplate : _.get(self.config, 'templates.entry', 'file_list_entry.asc'),
tsFormat : self.config.tsFormat, tsFormat : self.config.tsFormat,
descWidth : self.config.descWidth, descWidth : self.config.descWidth,
progress : exportListProgress, progress : exportListProgress,
}; };
exportFileList(filterCriteria, opts, (err, listBody) => { exportFileList(filterCriteria, opts, (err, listBody) => {
return callback(err, listBody); return callback(err, listBody);
}); });
}, },
function persistList(listBody, callback) { function persistList(listBody, callback) {
updateStatus('Persisting list'); updateStatus('Persisting list');
const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads);
const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea);
fse.mkdirs(sysTempDownloadDir, err => { fse.mkdirs(sysTempDownloadDir, err => {
if(err) { if(err) {
return callback(err); return callback(err);
} }
const outputFileName = paths.join( const outputFileName = paths.join(
sysTempDownloadDir, sysTempDownloadDir,
`file_list_${uuidv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt` `file_list_${uuidv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt`
); );
fs.writeFile(outputFileName, listBody, 'utf8', err => { fs.writeFile(outputFileName, listBody, 'utf8', err => {
if(err) { if(err) {
return callback(err); return callback(err);
} }
self.getSizeAndCompressIfMeetsSizeThreshold(outputFileName, (err, finalOutputFileName, fileSize) => { self.getSizeAndCompressIfMeetsSizeThreshold(outputFileName, (err, finalOutputFileName, fileSize) => {
return callback(err, finalOutputFileName, fileSize, sysTempDownloadArea); return callback(err, finalOutputFileName, fileSize, sysTempDownloadArea);
}); });
}); });
}); });
}, },
function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) { function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) {
const newEntry = new FileEntry({ const newEntry = new FileEntry({
areaTag : sysTempDownloadArea.areaTag, areaTag : sysTempDownloadArea.areaTag,
fileName : paths.basename(outputFileName), fileName : paths.basename(outputFileName),
storageTag : sysTempDownloadArea.storageTags[0], storageTag : sysTempDownloadArea.storageTags[0],
meta : { meta : {
upload_by_username : self.client.user.username, upload_by_username : self.client.user.username,
upload_by_user_id : self.client.user.userId, upload_by_user_id : self.client.user.userId,
byte_size : fileSize, byte_size : fileSize,
session_temp_dl : 1, // download is valid until session is over session_temp_dl : 1, // download is valid until session is over
} }
}); });
newEntry.desc = 'File List Export'; newEntry.desc = 'File List Export';
newEntry.persist(err => { newEntry.persist(err => {
if(!err) { if(!err) {
// queue it! // queue it!
const dlQueue = new DownloadQueue(self.client); const dlQueue = new DownloadQueue(self.client);
dlQueue.add(newEntry, true); // true=systemFile dlQueue.add(newEntry, true); // true=systemFile
// clean up after ourselves when the session ends // clean up after ourselves when the session ends
const thisClientId = self.client.session.id; const thisClientId = self.client.session.id;
Events.once(Events.getSystemEvents().ClientDisconnected, evt => { Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
if(thisClientId === _.get(evt, 'client.session.id')) { if(thisClientId === _.get(evt, 'client.session.id')) {
FileEntry.removeEntry(newEntry, { removePhysFile : true }, err => { FileEntry.removeEntry(newEntry, { removePhysFile : true }, err => {
if(err) { if(err) {
Log.warn( { fileId : newEntry.fileId, path : outputFileName }, 'Failed removing temporary session download' ); Log.warn( { fileId : newEntry.fileId, path : outputFileName }, 'Failed removing temporary session download' );
} else { } else {
Log.debug( { fileId : newEntry.fileId, path : outputFileName }, 'Removed temporary session download item' ); Log.debug( { fileId : newEntry.fileId, path : outputFileName }, 'Removed temporary session download item' );
} }
}); });
} }
}); });
} }
return callback(err); return callback(err);
}); });
}, },
function done(callback) { function done(callback) {
// re-enable idle monitor // re-enable idle monitor
self.client.startIdleMonitor(); self.client.startIdleMonitor();
updateStatus('Exported list has been added to your download queue'); updateStatus('Exported list has been added to your download queue');
return callback(null); return callback(null);
} }
], ],
err => { err => {
self.client.removeListener('key press', keyPressHandler); self.client.removeListener('key press', keyPressHandler);
return cb(err); return cb(err);
} }
); );
} }
getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) { getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) {
fse.stat(filePath, (err, stats) => { fse.stat(filePath, (err, stats) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
if(stats.size < this.config.compressThreshold) { if(stats.size < this.config.compressThreshold) {
// small enough, keep orig // small enough, keep orig
return cb(null, filePath, stats.size); return cb(null, filePath, stats.size);
} }
const zipFilePath = `${filePath}.zip`; const zipFilePath = `${filePath}.zip`;
const zipFile = new yazl.ZipFile(); const zipFile = new yazl.ZipFile();
zipFile.addFile(filePath, paths.basename(filePath)); zipFile.addFile(filePath, paths.basename(filePath));
zipFile.end( () => { zipFile.end( () => {
const outZipFile = fs.createWriteStream(zipFilePath); const outZipFile = fs.createWriteStream(zipFilePath);
zipFile.outputStream.pipe(outZipFile); zipFile.outputStream.pipe(outZipFile);
zipFile.outputStream.on('finish', () => { zipFile.outputStream.on('finish', () => {
// delete the original // delete the original
fse.unlink(filePath, err => { fse.unlink(filePath, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
// finally stat the new output // finally stat the new output
fse.stat(zipFilePath, (err, stats) => { fse.stat(zipFilePath, (err, stats) => {
return cb(err, zipFilePath, stats ? stats.size : 0); return cb(err, zipFilePath, stats ? stats.size : 0);
}); });
}); });
}); });
}); });
}); });
} }
}; };

View File

@ -19,269 +19,269 @@ const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Base Download Web Queue Manager', name : 'File Base Download Web Queue Manager',
desc : 'Module for interacting with web backed download queue/batch', desc : 'Module for interacting with web backed download queue/batch',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const FormIds = { const FormIds = {
queueManager : 0 queueManager : 0
}; };
const MciViewIds = { const MciViewIds = {
queueManager : { queueManager : {
queue : 1, queue : 1,
navMenu : 2, navMenu : 2,
customRangeStart : 10, customRangeStart : 10,
} }
}; };
exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.dlQueue = new DownloadQueue(this.client); this.dlQueue = new DownloadQueue(this.client);
this.menuMethods = { this.menuMethods = {
removeItem : (formData, extraArgs, cb) => { removeItem : (formData, extraArgs, cb) => {
const selectedItem = this.dlQueue.items[formData.value.queueItem]; const selectedItem = this.dlQueue.items[formData.value.queueItem];
if(!selectedItem) { if(!selectedItem) {
return cb(null); return cb(null);
} }
this.dlQueue.removeItems(selectedItem.fileId); this.dlQueue.removeItems(selectedItem.fileId);
// :TODO: broken: does not redraw menu properly - needs fixed! // :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb);
}, },
clearQueue : (formData, extraArgs, cb) => { clearQueue : (formData, extraArgs, cb) => {
this.dlQueue.clear(); this.dlQueue.clear();
// :TODO: broken: does not redraw menu properly - needs fixed! // :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView('all', cb); return this.removeItemsFromDownloadQueueView('all', cb);
}, },
getBatchLink : (formData, extraArgs, cb) => { getBatchLink : (formData, extraArgs, cb) => {
return this.generateAndDisplayBatchLink(cb); return this.generateAndDisplayBatchLink(cb);
} }
}; };
} }
initSequence() { initSequence() {
if(0 === this.dlQueue.items.length) { if(0 === this.dlQueue.items.length) {
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue');
} }
const self = this; const self = this;
async.series( async.series(
[ [
function beforeArt(callback) { function beforeArt(callback) {
return self.beforeArt(callback); return self.beforeArt(callback);
}, },
function display(callback) { function display(callback) {
return self.displayQueueManagerPage(false, callback); return self.displayQueueManagerPage(false, callback);
} }
], ],
() => { () => {
return self.finishedLoading(); return self.finishedLoading();
} }
); );
} }
removeItemsFromDownloadQueueView(itemIndex, cb) { removeItemsFromDownloadQueueView(itemIndex, cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
if(!queueView) { if(!queueView) {
return cb(Errors.DoesNotExist('Queue view does not exist')); return cb(Errors.DoesNotExist('Queue view does not exist'));
} }
if('all' === itemIndex) { if('all' === itemIndex) {
queueView.setItems([]); queueView.setItems([]);
queueView.setFocusItems([]); queueView.setFocusItems([]);
} else { } else {
queueView.removeItem(itemIndex); queueView.removeItem(itemIndex);
} }
queueView.redraw(); queueView.redraw();
return cb(null); return cb(null);
} }
displayFileInfoForFileEntry(fileEntry) { displayFileInfoForFileEntry(fileEntry) {
this.updateCustomViewTextsWithFilter( this.updateCustomViewTextsWithFilter(
'queueManager', 'queueManager',
MciViewIds.queueManager.customRangeStart, fileEntry, MciViewIds.queueManager.customRangeStart, fileEntry,
{ filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others.... { filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others....
); );
} }
updateDownloadQueueView(cb) { updateDownloadQueueView(cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
if(!queueView) { if(!queueView) {
return cb(Errors.DoesNotExist('Queue view does not exist')); return cb(Errors.DoesNotExist('Queue view does not exist'));
} }
const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}'; const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}';
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
queueView.on('index update', idx => { queueView.on('index update', idx => {
const fileEntry = this.dlQueue.items[idx]; const fileEntry = this.dlQueue.items[idx];
this.displayFileInfoForFileEntry(fileEntry); this.displayFileInfoForFileEntry(fileEntry);
}); });
queueView.redraw(); queueView.redraw();
this.displayFileInfoForFileEntry(this.dlQueue.items[0]); this.displayFileInfoForFileEntry(this.dlQueue.items[0]);
return cb(null); return cb(null);
} }
generateAndDisplayBatchLink(cb) { generateAndDisplayBatchLink(cb) {
const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes'); const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes');
FileAreaWeb.createAndServeTempBatchDownload( FileAreaWeb.createAndServeTempBatchDownload(
this.client, this.client,
this.dlQueue.items, this.dlQueue.items,
{ {
expireTime : expireTime expireTime : expireTime
}, },
(err, webBatchDlLink) => { (err, webBatchDlLink) => {
// :TODO: handle not enabled -> display such // :TODO: handle not enabled -> display such
if(err) { if(err) {
return cb(err); return cb(err);
} }
const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
const formatObj = { const formatObj = {
webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink, webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink,
webBatchDlExpire : expireTime.format(webDlExpireTimeFormat), webBatchDlExpire : expireTime.format(webDlExpireTimeFormat),
}; };
this.updateCustomViewTextsWithFilter( this.updateCustomViewTextsWithFilter(
'queueManager', 'queueManager',
MciViewIds.queueManager.customRangeStart, MciViewIds.queueManager.customRangeStart,
formatObj, formatObj,
{ filter : Object.keys(formatObj).map(k => '{' + k + '}' ) } { filter : Object.keys(formatObj).map(k => '{' + k + '}' ) }
); );
return cb(null); return cb(null);
} }
); );
} }
displayQueueManagerPage(clearScreen, cb) { displayQueueManagerPage(clearScreen, cb) {
const self = this; const self = this;
async.series( async.series(
[ [
function prepArtAndViewController(callback) { function prepArtAndViewController(callback) {
return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback);
}, },
function prepareQueueDownloadLinks(callback) { function prepareQueueDownloadLinks(callback) {
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
const config = Config(); const config = Config();
async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => { async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => {
FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => { FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => {
if(err) { if(err) {
if(ErrNotEnabled === err.reasonCode) { if(ErrNotEnabled === err.reasonCode) {
return nextFileEntry(err); // we should have caught this prior return nextFileEntry(err); // we should have caught this prior
} }
const expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes'); const expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes');
FileAreaWeb.createAndServeTempDownload( FileAreaWeb.createAndServeTempDownload(
self.client, self.client,
fileEntry, fileEntry,
{ expireTime : expireTime }, { expireTime : expireTime },
(err, url) => { (err, url) => {
if(err) { if(err) {
return nextFileEntry(err); return nextFileEntry(err);
} }
fileEntry.webDlLinkRaw = url; fileEntry.webDlLinkRaw = url;
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url;
fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat); fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat);
return nextFileEntry(null); return nextFileEntry(null);
} }
); );
} else { } else {
fileEntry.webDlLinkRaw = serveItem.url; fileEntry.webDlLinkRaw = serveItem.url;
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url; fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url;
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
return nextFileEntry(null); return nextFileEntry(null);
} }
}); });
}, err => { }, err => {
return callback(err); return callback(err);
}); });
}, },
function populateViews(callback) { function populateViews(callback) {
return self.updateDownloadQueueView(callback); return self.updateDownloadQueueView(callback);
} }
], ],
err => { err => {
if(cb) { if(cb) {
return cb(err); return cb(err);
} }
} }
); );
} }
displayArtAndPrepViewController(name, options, cb) { displayArtAndPrepViewController(name, options, cb) {
const self = this; const self = this;
const config = this.menuConfig.config; const config = this.menuConfig.config;
async.waterfall( async.waterfall(
[ [
function readyAndDisplayArt(callback) { function readyAndDisplayArt(callback) {
if(options.clearScreen) { if(options.clearScreen) {
self.client.term.rawWrite(ansi.resetScreen()); self.client.term.rawWrite(ansi.resetScreen());
} }
theme.displayThemedAsset( theme.displayThemedAsset(
config.art[name], config.art[name],
self.client, self.client,
{ font : self.menuConfig.font, trailingLF : false }, { font : self.menuConfig.font, trailingLF : false },
(err, artData) => { (err, artData) => {
return callback(err, artData); return callback(err, artData);
} }
); );
}, },
function prepeareViewController(artData, callback) { function prepeareViewController(artData, callback) {
if(_.isUndefined(self.viewControllers[name])) { if(_.isUndefined(self.viewControllers[name])) {
const vcOpts = { const vcOpts = {
client : self.client, client : self.client,
formId : FormIds[name], formId : FormIds[name],
}; };
if(!_.isUndefined(options.noInput)) { if(!_.isUndefined(options.noInput)) {
vcOpts.noInput = options.noInput; vcOpts.noInput = options.noInput;
} }
const vc = self.addViewController(name, new ViewController(vcOpts)); const vc = self.addViewController(name, new ViewController(vcOpts));
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds[name], formId : FormIds[name],
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
} }
self.viewControllers[name].setFocus(true); self.viewControllers[name].setFocus(true);
return callback(null); return callback(null);
}, },
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
} }
}; };

File diff suppressed because it is too large Load Diff

View File

@ -42,113 +42,113 @@ const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc.
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'Transfer file', name : 'Transfer file',
desc : 'Sends or receives a file(s)', desc : 'Sends or receives a file(s)',
author : 'NuSkooler', author : 'NuSkooler',
}; };
exports.getModule = class TransferFileModule extends MenuModule { exports.getModule = class TransferFileModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = this.menuConfig.config || {}; this.config = this.menuConfig.config || {};
// //
// Most options can be set via extraArgs or config block // Most options can be set via extraArgs or config block
// //
const config = Config(); const config = Config();
if(options.extraArgs) { if(options.extraArgs) {
if(options.extraArgs.protocol) { if(options.extraArgs.protocol) {
this.protocolConfig = config.fileTransferProtocols[options.extraArgs.protocol]; this.protocolConfig = config.fileTransferProtocols[options.extraArgs.protocol];
} }
if(options.extraArgs.direction) { if(options.extraArgs.direction) {
this.direction = options.extraArgs.direction; this.direction = options.extraArgs.direction;
} }
if(options.extraArgs.sendQueue) { if(options.extraArgs.sendQueue) {
this.sendQueue = options.extraArgs.sendQueue; this.sendQueue = options.extraArgs.sendQueue;
} }
if(options.extraArgs.recvFileName) { if(options.extraArgs.recvFileName) {
this.recvFileName = options.extraArgs.recvFileName; this.recvFileName = options.extraArgs.recvFileName;
} }
if(options.extraArgs.recvDirectory) { if(options.extraArgs.recvDirectory) {
this.recvDirectory = options.extraArgs.recvDirectory; this.recvDirectory = options.extraArgs.recvDirectory;
} }
} else { } else {
if(this.config.protocol) { if(this.config.protocol) {
this.protocolConfig = config.fileTransferProtocols[this.config.protocol]; this.protocolConfig = config.fileTransferProtocols[this.config.protocol];
} }
if(this.config.direction) { if(this.config.direction) {
this.direction = this.config.direction; this.direction = this.config.direction;
} }
if(this.config.sendQueue) { if(this.config.sendQueue) {
this.sendQueue = this.config.sendQueue; this.sendQueue = this.config.sendQueue;
} }
if(this.config.recvFileName) { if(this.config.recvFileName) {
this.recvFileName = this.config.recvFileName; this.recvFileName = this.config.recvFileName;
} }
if(this.config.recvDirectory) { if(this.config.recvDirectory) {
this.recvDirectory = this.config.recvDirectory; this.recvDirectory = this.config.recvDirectory;
} }
} }
this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something* this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something*
this.direction = this.direction || 'send'; this.direction = this.direction || 'send';
this.sendQueue = this.sendQueue || []; this.sendQueue = this.sendQueue || [];
// Ensure sendQueue is an array of objects that contain at least a 'path' member // Ensure sendQueue is an array of objects that contain at least a 'path' member
this.sendQueue = this.sendQueue.map(item => { this.sendQueue = this.sendQueue.map(item => {
if(_.isString(item)) { if(_.isString(item)) {
return { path : item }; return { path : item };
} else { } else {
return item; return item;
} }
}); });
this.sentFileIds = []; this.sentFileIds = [];
} }
isSending() { isSending() {
return ('send' === this.direction); return ('send' === this.direction);
} }
restorePipeAfterExternalProc() { restorePipeAfterExternalProc() {
if(!this.pipeRestored) { if(!this.pipeRestored) {
this.pipeRestored = true; this.pipeRestored = true;
this.client.restoreDataHandler(); this.client.restoreDataHandler();
} }
} }
sendFiles(cb) { sendFiles(cb) {
// assume *sending* can always batch // assume *sending* can always batch
// :TODO: Look into this further // :TODO: Look into this further
const allFiles = this.sendQueue.map(f => f.path); const allFiles = this.sendQueue.map(f => f.path);
this.executeExternalProtocolHandlerForSend(allFiles, err => { this.executeExternalProtocolHandlerForSend(allFiles, err => {
if(err) { if(err) {
this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' ); this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' );
} else { } else {
const sentFiles = []; const sentFiles = [];
this.sendQueue.forEach(f => { this.sendQueue.forEach(f => {
f.sent = true; f.sent = true;
sentFiles.push(f.path); sentFiles.push(f.path);
}); });
this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` );
} }
return cb(err); return cb(err);
}); });
} }
/* /*
sendFiles(cb) { sendFiles(cb) {
// :TODO: built in/native protocol support // :TODO: built in/native protocol support
@ -189,408 +189,408 @@ exports.getModule = class TransferFileModule extends MenuModule {
} }
*/ */
moveFileWithCollisionHandling(src, dst, cb) { moveFileWithCollisionHandling(src, dst, cb) {
// //
// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc.
// in the case of collisions. // in the case of collisions.
// //
const dstPath = paths.dirname(dst); const dstPath = paths.dirname(dst);
const dstFileExt = paths.extname(dst); const dstFileExt = paths.extname(dst);
const dstFileSuffix = paths.basename(dst, dstFileExt); const dstFileSuffix = paths.basename(dst, dstFileExt);
let renameIndex = 0; let renameIndex = 0;
let movedOk = false; let movedOk = false;
let tryDstPath; let tryDstPath;
async.until( async.until(
() => movedOk, // until moved OK () => movedOk, // until moved OK
(cb) => { (cb) => {
if(0 === renameIndex) { if(0 === renameIndex) {
// try originally supplied path first // try originally supplied path first
tryDstPath = dst; tryDstPath = dst;
} else { } else {
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`);
} }
fse.move(src, tryDstPath, err => { fse.move(src, tryDstPath, err => {
if(err) { if(err) {
if('EEXIST' === err.code) { if('EEXIST' === err.code) {
renameIndex += 1; renameIndex += 1;
return cb(null); // keep trying return cb(null); // keep trying
} }
return cb(err); return cb(err);
} }
movedOk = true; movedOk = true;
return cb(null, tryDstPath); return cb(null, tryDstPath);
}); });
}, },
(err, finalPath) => { (err, finalPath) => {
return cb(err, finalPath); return cb(err, finalPath);
} }
); );
} }
recvFiles(cb) { recvFiles(cb) {
this.executeExternalProtocolHandlerForRecv(err => { this.executeExternalProtocolHandlerForRecv(err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
this.recvFilePaths = []; this.recvFilePaths = [];
if(this.recvFileName) { if(this.recvFileName) {
// //
// file name specified - we expect a single file in |this.recvDirectory| // file name specified - we expect a single file in |this.recvDirectory|
// by the name of |this.recvFileName| // by the name of |this.recvFileName|
// //
const recvFullPath = paths.join(this.recvDirectory, this.recvFileName); const recvFullPath = paths.join(this.recvDirectory, this.recvFileName);
fs.stat(recvFullPath, (err, stats) => { fs.stat(recvFullPath, (err, stats) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
if(!stats.isFile()) { if(!stats.isFile()) {
return cb(Errors.Invalid('Expected file entry in recv directory')); return cb(Errors.Invalid('Expected file entry in recv directory'));
} }
this.recvFilePaths.push(recvFullPath); this.recvFilePaths.push(recvFullPath);
return cb(null); return cb(null);
}); });
} else { } else {
// //
// Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already // Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already
// //
fs.readdir(this.recvDirectory, (err, files) => { fs.readdir(this.recvDirectory, (err, files) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
// stat each to grab files only // stat each to grab files only
async.each(files, (fileName, nextFile) => { async.each(files, (fileName, nextFile) => {
const recvFullPath = paths.join(this.recvDirectory, fileName); const recvFullPath = paths.join(this.recvDirectory, fileName);
fs.stat(recvFullPath, (err, stats) => { fs.stat(recvFullPath, (err, stats) => {
if(err) { if(err) {
this.client.log.warn('Failed to stat file', { path : recvFullPath } ); this.client.log.warn('Failed to stat file', { path : recvFullPath } );
return nextFile(null); // just try the next one return nextFile(null); // just try the next one
} }
if(stats.isFile()) { if(stats.isFile()) {
this.recvFilePaths.push(recvFullPath); this.recvFilePaths.push(recvFullPath);
} }
return nextFile(null); return nextFile(null);
}); });
}, () => { }, () => {
return cb(null); return cb(null);
}); });
}); });
} }
}); });
} }
pathWithTerminatingSeparator(path) { pathWithTerminatingSeparator(path) {
if(path && paths.sep !== path.charAt(path.length - 1)) { if(path && paths.sep !== path.charAt(path.length - 1)) {
path = path + paths.sep; path = path + paths.sep;
} }
return path; return path;
} }
prepAndBuildSendArgs(filePaths, cb) { prepAndBuildSendArgs(filePaths, cb) {
const externalArgs = this.protocolConfig.external['sendArgs']; const externalArgs = this.protocolConfig.external['sendArgs'];
async.waterfall( async.waterfall(
[ [
function getTempFileListPath(callback) { function getTempFileListPath(callback) {
const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) ); const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) );
if(!hasFileList) { if(!hasFileList) {
return callback(null, null); return callback(null, null);
} }
temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => { temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => {
if(err) { if(err) {
return callback(err); // failed to create it return callback(err); // failed to create it
} }
fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL)); fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL));
fs.close(tempFileInfo.fd, err => { fs.close(tempFileInfo.fd, err => {
return callback(err, tempFileInfo.path); return callback(err, tempFileInfo.path);
}); });
}); });
}, },
function createArgs(tempFileListPath, callback) { function createArgs(tempFileListPath, callback) {
// initial args: ignore {filePaths} as we must break that into it's own sep array items // initial args: ignore {filePaths} as we must break that into it's own sep array items
const args = externalArgs.map(arg => { const args = externalArgs.map(arg => {
return '{filePaths}' === arg ? arg : stringFormat(arg, { return '{filePaths}' === arg ? arg : stringFormat(arg, {
fileListPath : tempFileListPath || '', fileListPath : tempFileListPath || '',
}); });
}); });
const filePathsPos = args.indexOf('{filePaths}'); const filePathsPos = args.indexOf('{filePaths}');
if(filePathsPos > -1) { if(filePathsPos > -1) {
// replace {filePaths} with 0:n individual entries in |args| // replace {filePaths} with 0:n individual entries in |args|
args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) ); args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) );
} }
return callback(null, args); return callback(null, args);
} }
], ],
(err, args) => { (err, args) => {
return cb(err, args); return cb(err, args);
} }
); );
} }
prepAndBuildRecvArgs(cb) { prepAndBuildRecvArgs(cb) {
const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs'; const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs';
const externalArgs = this.protocolConfig.external[argsKey]; const externalArgs = this.protocolConfig.external[argsKey];
const args = externalArgs.map(arg => stringFormat(arg, { const args = externalArgs.map(arg => stringFormat(arg, {
uploadDir : this.recvDirectory, uploadDir : this.recvDirectory,
fileName : this.recvFileName || '', fileName : this.recvFileName || '',
})); }));
return cb(null, args); return cb(null, args);
} }
executeExternalProtocolHandler(args, cb) { executeExternalProtocolHandler(args, cb) {
const external = this.protocolConfig.external; const external = this.protocolConfig.external;
const cmd = external[`${this.direction}Cmd`]; const cmd = external[`${this.direction}Cmd`];
this.client.log.debug( this.client.log.debug(
{ cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction }, { cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction },
'Executing external protocol' 'Executing external protocol'
); );
const spawnOpts = { const spawnOpts = {
cols : this.client.term.termWidth, cols : this.client.term.termWidth,
rows : this.client.term.termHeight, rows : this.client.term.termHeight,
cwd : this.recvDirectory, cwd : this.recvDirectory,
encoding : null, // don't bork our data! encoding : null, // don't bork our data!
}; };
const externalProc = pty.spawn(cmd, args, spawnOpts); const externalProc = pty.spawn(cmd, args, spawnOpts);
this.client.setTemporaryDirectDataHandler(data => { this.client.setTemporaryDirectDataHandler(data => {
// needed for things like sz/rz // needed for things like sz/rz
if(external.escapeTelnet) { if(external.escapeTelnet) {
const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape
externalProc.write(Buffer.from(tmp, 'binary')); externalProc.write(Buffer.from(tmp, 'binary'));
} else { } else {
externalProc.write(data); externalProc.write(data);
} }
}); });
externalProc.on('data', data => { externalProc.on('data', data => {
// needed for things like sz/rz // needed for things like sz/rz
if(external.escapeTelnet) { if(external.escapeTelnet) {
const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape
this.client.term.rawWrite(Buffer.from(tmp, 'binary')); this.client.term.rawWrite(Buffer.from(tmp, 'binary'));
} else { } else {
this.client.term.rawWrite(data); this.client.term.rawWrite(data);
} }
}); });
externalProc.once('close', () => { externalProc.once('close', () => {
return this.restorePipeAfterExternalProc(); return this.restorePipeAfterExternalProc();
}); });
externalProc.once('exit', (exitCode) => { externalProc.once('exit', (exitCode) => {
this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' ); this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' );
this.restorePipeAfterExternalProc(); this.restorePipeAfterExternalProc();
externalProc.removeAllListeners(); externalProc.removeAllListeners();
return cb(exitCode ? Errors.ExternalProcess(`Process exited with exit code ${exitCode}`, 'EBADEXIT') : null); return cb(exitCode ? Errors.ExternalProcess(`Process exited with exit code ${exitCode}`, 'EBADEXIT') : null);
}); });
} }
executeExternalProtocolHandlerForSend(filePaths, cb) { executeExternalProtocolHandlerForSend(filePaths, cb) {
if(!Array.isArray(filePaths)) { if(!Array.isArray(filePaths)) {
filePaths = [ filePaths ]; filePaths = [ filePaths ];
} }
this.prepAndBuildSendArgs(filePaths, (err, args) => { this.prepAndBuildSendArgs(filePaths, (err, args) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
this.executeExternalProtocolHandler(args, err => { this.executeExternalProtocolHandler(args, err => {
return cb(err); return cb(err);
}); });
}); });
} }
executeExternalProtocolHandlerForRecv(cb) { executeExternalProtocolHandlerForRecv(cb) {
this.prepAndBuildRecvArgs( (err, args) => { this.prepAndBuildRecvArgs( (err, args) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
this.executeExternalProtocolHandler(args, err => { this.executeExternalProtocolHandler(args, err => {
return cb(err); return cb(err);
}); });
}); });
} }
getMenuResult() { getMenuResult() {
if(this.isSending()) { if(this.isSending()) {
return { sentFileIds : this.sentFileIds }; return { sentFileIds : this.sentFileIds };
} else { } else {
return { recvFilePaths : this.recvFilePaths }; return { recvFilePaths : this.recvFilePaths };
} }
} }
updateSendStats(cb) { updateSendStats(cb) {
let downloadBytes = 0; let downloadBytes = 0;
let downloadCount = 0; let downloadCount = 0;
let fileIds = []; let fileIds = [];
async.each(this.sendQueue, (queueItem, next) => { async.each(this.sendQueue, (queueItem, next) => {
if(!queueItem.sent) { if(!queueItem.sent) {
return next(null); return next(null);
} }
if(queueItem.fileId) { if(queueItem.fileId) {
fileIds.push(queueItem.fileId); fileIds.push(queueItem.fileId);
} }
if(_.isNumber(queueItem.byteSize)) { if(_.isNumber(queueItem.byteSize)) {
downloadCount += 1; downloadCount += 1;
downloadBytes += queueItem.byteSize; downloadBytes += queueItem.byteSize;
return next(null); return next(null);
} }
// we just have a path - figure it out // we just have a path - figure it out
fs.stat(queueItem.path, (err, stats) => { fs.stat(queueItem.path, (err, stats) => {
if(err) { if(err) {
this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' ); this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' );
} else { } else {
downloadCount += 1; downloadCount += 1;
downloadBytes += stats.size; downloadBytes += stats.size;
} }
return next(null); return next(null);
}); });
}, () => { }, () => {
// All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks // All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks
StatLog.incrementUserStat(this.client.user, 'dl_total_count', downloadCount); StatLog.incrementUserStat(this.client.user, 'dl_total_count', downloadCount);
StatLog.incrementUserStat(this.client.user, 'dl_total_bytes', downloadBytes); StatLog.incrementUserStat(this.client.user, 'dl_total_bytes', downloadBytes);
StatLog.incrementSystemStat('dl_total_count', downloadCount); StatLog.incrementSystemStat('dl_total_count', downloadCount);
StatLog.incrementSystemStat('dl_total_bytes', downloadBytes); StatLog.incrementSystemStat('dl_total_bytes', downloadBytes);
fileIds.forEach(fileId => { fileIds.forEach(fileId => {
FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1); FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1);
}); });
return cb(null); return cb(null);
}); });
} }
updateRecvStats(cb) { updateRecvStats(cb) {
let uploadBytes = 0; let uploadBytes = 0;
let uploadCount = 0; let uploadCount = 0;
async.each(this.recvFilePaths, (filePath, next) => { async.each(this.recvFilePaths, (filePath, next) => {
// we just have a path - figure it out // we just have a path - figure it out
fs.stat(filePath, (err, stats) => { fs.stat(filePath, (err, stats) => {
if(err) { if(err) {
this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' ); this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' );
} else { } else {
uploadCount += 1; uploadCount += 1;
uploadBytes += stats.size; uploadBytes += stats.size;
} }
return next(null); return next(null);
}); });
}, () => { }, () => {
StatLog.incrementUserStat(this.client.user, 'ul_total_count', uploadCount); StatLog.incrementUserStat(this.client.user, 'ul_total_count', uploadCount);
StatLog.incrementUserStat(this.client.user, 'ul_total_bytes', uploadBytes); StatLog.incrementUserStat(this.client.user, 'ul_total_bytes', uploadBytes);
StatLog.incrementSystemStat('ul_total_count', uploadCount); StatLog.incrementSystemStat('ul_total_count', uploadCount);
StatLog.incrementSystemStat('ul_total_bytes', uploadBytes); StatLog.incrementSystemStat('ul_total_bytes', uploadBytes);
return cb(null); return cb(null);
}); });
} }
initSequence() { initSequence() {
const self = this; const self = this;
// :TODO: break this up to send|recv // :TODO: break this up to send|recv
async.series( async.series(
[ [
function validateConfig(callback) { function validateConfig(callback) {
if(self.isSending()) { if(self.isSending()) {
if(!Array.isArray(self.sendQueue)) { if(!Array.isArray(self.sendQueue)) {
self.sendQueue = [ self.sendQueue ]; self.sendQueue = [ self.sendQueue ];
} }
} }
return callback(null); return callback(null);
}, },
function transferFiles(callback) { function transferFiles(callback) {
if(self.isSending()) { if(self.isSending()) {
self.sendFiles( err => { self.sendFiles( err => {
if(err) { if(err) {
return callback(err); return callback(err);
} }
const sentFileIds = []; const sentFileIds = [];
self.sendQueue.forEach(queueItem => { self.sendQueue.forEach(queueItem => {
if(queueItem.sent && queueItem.fileId) { if(queueItem.sent && queueItem.fileId) {
sentFileIds.push(queueItem.fileId); sentFileIds.push(queueItem.fileId);
} }
}); });
if(sentFileIds.length > 0) { if(sentFileIds.length > 0) {
// remove items we sent from the D/L queue // remove items we sent from the D/L queue
const dlQueue = new DownloadQueue(self.client); const dlQueue = new DownloadQueue(self.client);
const dlFileEntries = dlQueue.removeItems(sentFileIds); const dlFileEntries = dlQueue.removeItems(sentFileIds);
// fire event for downloaded entries // fire event for downloaded entries
Events.emit( Events.emit(
Events.getSystemEvents().UserDownload, Events.getSystemEvents().UserDownload,
{ {
user : self.client.user, user : self.client.user,
files : dlFileEntries files : dlFileEntries
} }
); );
self.sentFileIds = sentFileIds; self.sentFileIds = sentFileIds;
} }
return callback(null); return callback(null);
}); });
} else { } else {
self.recvFiles( err => { self.recvFiles( err => {
return callback(err); return callback(err);
}); });
} }
}, },
function cleanupTempFiles(callback) { function cleanupTempFiles(callback) {
temptmp.cleanup( paths => { temptmp.cleanup( paths => {
Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' );
}); });
return callback(null); return callback(null);
}, },
function updateUserAndSystemStats(callback) { function updateUserAndSystemStats(callback) {
if(self.isSending()) { if(self.isSending()) {
return self.updateSendStats(callback); return self.updateSendStats(callback);
} else { } else {
return self.updateRecvStats(callback); return self.updateRecvStats(callback);
} }
} }
], ],
err => { err => {
if(err) { if(err) {
self.client.log.warn( { error : err.message }, 'File transfer error'); self.client.log.warn( { error : err.message }, 'File transfer error');
} }
return self.prevMenu(); return self.prevMenu();
} }
); );
} }
}; };

View File

@ -12,147 +12,147 @@ const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File transfer protocol selection', name : 'File transfer protocol selection',
desc : 'Select protocol / method for file transfer', desc : 'Select protocol / method for file transfer',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
protList : 1, protList : 1,
}; };
exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = this.menuConfig.config || {}; this.config = this.menuConfig.config || {};
if(options.extraArgs) { if(options.extraArgs) {
if(options.extraArgs.direction) { if(options.extraArgs.direction) {
this.config.direction = options.extraArgs.direction; this.config.direction = options.extraArgs.direction;
} }
} }
this.config.direction = this.config.direction || 'send'; this.config.direction = this.config.direction || 'send';
this.extraArgs = options.extraArgs; this.extraArgs = options.extraArgs;
if(_.has(options, 'lastMenuResult.sentFileIds')) { if(_.has(options, 'lastMenuResult.sentFileIds')) {
this.sentFileIds = options.lastMenuResult.sentFileIds; this.sentFileIds = options.lastMenuResult.sentFileIds;
} }
if(_.has(options, 'lastMenuResult.recvFilePaths')) { if(_.has(options, 'lastMenuResult.recvFilePaths')) {
this.recvFilePaths = options.lastMenuResult.recvFilePaths; this.recvFilePaths = options.lastMenuResult.recvFilePaths;
} }
this.fallbackOnly = options.lastMenuResult ? true : false; this.fallbackOnly = options.lastMenuResult ? true : false;
this.loadAvailProtocols(); this.loadAvailProtocols();
this.menuMethods = { this.menuMethods = {
selectProtocol : (formData, extraArgs, cb) => { selectProtocol : (formData, extraArgs, cb) => {
const protocol = this.protocols[formData.value.protocol]; const protocol = this.protocols[formData.value.protocol];
const finalExtraArgs = this.extraArgs || {}; const finalExtraArgs = this.extraArgs || {};
Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs ); Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs );
const modOpts = { const modOpts = {
extraArgs : finalExtraArgs, extraArgs : finalExtraArgs,
}; };
if('send' === this.config.direction) { if('send' === this.config.direction) {
return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb); return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb);
} else { } else {
return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb); return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb);
} }
}, },
}; };
} }
getMenuResult() { getMenuResult() {
if(this.sentFileIds) { if(this.sentFileIds) {
return { sentFileIds : this.sentFileIds }; return { sentFileIds : this.sentFileIds };
} }
if(this.recvFilePaths) { if(this.recvFilePaths) {
return { recvFilePaths : this.recvFilePaths }; return { recvFilePaths : this.recvFilePaths };
} }
} }
initSequence() { initSequence() {
if(this.sentFileIds || this.recvFilePaths) { if(this.sentFileIds || this.recvFilePaths) {
// nothing to do here; move along (we're just falling through) // nothing to do here; move along (we're just falling through)
this.prevMenu(); this.prevMenu();
} else { } else {
super.initSequence(); super.initSequence();
} }
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu mciMap : mciData.menu
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
}, },
function populateList(callback) { function populateList(callback) {
const protListView = vc.getView(MciViewIds.protList); const protListView = vc.getView(MciViewIds.protList);
const protListFormat = self.config.protListFormat || '{name}'; const protListFormat = self.config.protListFormat || '{name}';
const protListFocusFormat = self.config.protListFocusFormat || protListFormat; const protListFocusFormat = self.config.protListFocusFormat || protListFormat;
protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) ); protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) );
protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) ); protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) );
protListView.redraw(); protListView.redraw();
return callback(null); return callback(null);
} }
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
}); });
} }
loadAvailProtocols() { loadAvailProtocols() {
this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => { this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => {
return { return {
protocol : protocol, protocol : protocol,
name : protInfo.name, name : protInfo.name,
hasBatch : _.has(protInfo, 'external.recvArgs'), hasBatch : _.has(protInfo, 'external.recvArgs'),
hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'), hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'),
sort : protInfo.sort, sort : protInfo.sort,
}; };
}); });
// Filter out batch vs non-batch only protocols // Filter out batch vs non-batch only protocols
if(this.extraArgs.recvFileName) { // non-batch aka non-blind if(this.extraArgs.recvFileName) { // non-batch aka non-blind
this.protocols = this.protocols.filter( prot => prot.hasNonBatch ); this.protocols = this.protocols.filter( prot => prot.hasNonBatch );
} else { } else {
this.protocols = this.protocols.filter( prot => prot.hasBatch ); this.protocols = this.protocols.filter( prot => prot.hasBatch );
} }
// natural sort taking explicit orders into consideration // natural sort taking explicit orders into consideration
this.protocols.sort( (a, b) => { this.protocols.sort( (a, b) => {
if(_.isNumber(a.sort) && _.isNumber(b.sort)) { if(_.isNumber(a.sort) && _.isNumber(b.sort)) {
return a.sort - b.sort; return a.sort - b.sort;
} else { } else {
return a.name.localeCompare(b.name, { sensitivity : false, numeric : true } ); return a.name.localeCompare(b.name, { sensitivity : false, numeric : true } );
} }
}); });
} }
}; };

View File

@ -14,59 +14,59 @@ exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling;
exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator; exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator;
function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) {
operation = operation || 'copy'; operation = operation || 'copy';
const dstPath = paths.dirname(dst); const dstPath = paths.dirname(dst);
const dstFileExt = paths.extname(dst); const dstFileExt = paths.extname(dst);
const dstFileSuffix = paths.basename(dst, dstFileExt); const dstFileSuffix = paths.basename(dst, dstFileExt);
EnigAssert('move' === operation || 'copy' === operation); EnigAssert('move' === operation || 'copy' === operation);
let renameIndex = 0; let renameIndex = 0;
let opOk = false; let opOk = false;
let tryDstPath; let tryDstPath;
function tryOperation(src, dst, callback) { function tryOperation(src, dst, callback) {
if('move' === operation) { if('move' === operation) {
fse.move(src, tryDstPath, err => { fse.move(src, tryDstPath, err => {
return callback(err); return callback(err);
}); });
} else if('copy' === operation) { } else if('copy' === operation) {
fse.copy(src, tryDstPath, { overwrite : false, errorOnExist : true }, err => { fse.copy(src, tryDstPath, { overwrite : false, errorOnExist : true }, err => {
return callback(err); return callback(err);
}); });
} }
} }
async.until( async.until(
() => opOk, // until moved OK () => opOk, // until moved OK
(cb) => { (cb) => {
if(0 === renameIndex) { if(0 === renameIndex) {
// try originally supplied path first // try originally supplied path first
tryDstPath = dst; tryDstPath = dst;
} else { } else {
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`);
} }
tryOperation(src, tryDstPath, err => { tryOperation(src, tryDstPath, err => {
if(err) { if(err) {
// for some reason fs-extra copy doesn't pass err.code // for some reason fs-extra copy doesn't pass err.code
// :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST // :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST
if('EEXIST' === err.code || 'copy' === operation) { if('EEXIST' === err.code || 'copy' === operation) {
renameIndex += 1; renameIndex += 1;
return cb(null); // keep trying return cb(null); // keep trying
} }
return cb(err); return cb(err);
} }
opOk = true; opOk = true;
return cb(null, tryDstPath); return cb(null, tryDstPath);
}); });
}, },
(err, finalPath) => { (err, finalPath) => {
return cb(err, finalPath); return cb(err, finalPath);
} }
); );
} }
// //
@ -74,16 +74,16 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) {
// in the case of collisions. // in the case of collisions.
// //
function moveFileWithCollisionHandling(src, dst, cb) { function moveFileWithCollisionHandling(src, dst, cb) {
return moveOrCopyFileWithCollisionHandling(src, dst, 'move', cb); return moveOrCopyFileWithCollisionHandling(src, dst, 'move', cb);
} }
function copyFileWithCollisionHandling(src, dst, cb) { function copyFileWithCollisionHandling(src, dst, cb) {
return moveOrCopyFileWithCollisionHandling(src, dst, 'copy', cb); return moveOrCopyFileWithCollisionHandling(src, dst, 'copy', cb);
} }
function pathWithTerminatingSeparator(path) { function pathWithTerminatingSeparator(path) {
if(path && paths.sep !== path.charAt(path.length - 1)) { if(path && paths.sep !== path.charAt(path.length - 1)) {
path = path + paths.sep; path = path + paths.sep;
} }
return path; return path;
} }

View File

@ -5,46 +5,46 @@ let _ = require('lodash');
// FNV-1a based on work here: https://github.com/wiedi/node-fnv // FNV-1a based on work here: https://github.com/wiedi/node-fnv
module.exports = class FNV1a { module.exports = class FNV1a {
constructor(data) { constructor(data) {
this.hash = 0x811c9dc5; this.hash = 0x811c9dc5;
if(!_.isUndefined(data)) { if(!_.isUndefined(data)) {
this.update(data); this.update(data);
} }
} }
update(data) { update(data) {
if(_.isNumber(data)) { if(_.isNumber(data)) {
data = data.toString(); data = data.toString();
} }
if(_.isString(data)) { if(_.isString(data)) {
data = Buffer.from(data); data = Buffer.from(data);
} }
if(!Buffer.isBuffer(data)) { if(!Buffer.isBuffer(data)) {
throw new Error('data must be String or Buffer!'); throw new Error('data must be String or Buffer!');
} }
for(let b of data) { for(let b of data) {
this.hash = this.hash ^ b; this.hash = this.hash ^ b;
this.hash += this.hash +=
(this.hash << 24) + (this.hash << 8) + (this.hash << 7) + (this.hash << 24) + (this.hash << 8) + (this.hash << 7) +
(this.hash << 4) + (this.hash << 1); (this.hash << 4) + (this.hash << 1);
} }
return this; return this;
} }
digest(encoding) { digest(encoding) {
encoding = encoding || 'binary'; encoding = encoding || 'binary';
const buf = Buffer.alloc(4); const buf = Buffer.alloc(4);
buf.writeInt32BE(this.hash & 0xffffffff, 0); buf.writeInt32BE(this.hash & 0xffffffff, 0);
return buf.toString(encoding); return buf.toString(encoding);
} }
get value() { get value() {
return this.hash & 0xffffffff; return this.hash & 0xffffffff;
} }
}; };

File diff suppressed because it is too large Load Diff

View File

@ -7,94 +7,94 @@ const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-
const FTN_PATTERN_REGEXP = /^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i; const FTN_PATTERN_REGEXP = /^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i;
module.exports = class Address { module.exports = class Address {
constructor(addr) { constructor(addr) {
if(addr) { if(addr) {
if(_.isObject(addr)) { if(_.isObject(addr)) {
Object.assign(this, addr); Object.assign(this, addr);
} else if(_.isString(addr)) { } else if(_.isString(addr)) {
const temp = Address.fromString(addr); const temp = Address.fromString(addr);
if(temp) { if(temp) {
Object.assign(this, temp); Object.assign(this, temp);
} }
} }
} }
} }
static isValidAddress(addr) { static isValidAddress(addr) {
return addr && addr.isValid(); return addr && addr.isValid();
} }
isValid() { isValid() {
// FTN address is valid if we have at least a net/node // FTN address is valid if we have at least a net/node
return _.isNumber(this.net) && _.isNumber(this.node); return _.isNumber(this.net) && _.isNumber(this.node);
} }
isEqual(other) { isEqual(other) {
if(_.isString(other)) { if(_.isString(other)) {
other = Address.fromString(other); other = Address.fromString(other);
} }
return ( return (
this.net === other.net && this.net === other.net &&
this.node === other.node && this.node === other.node &&
this.zone === other.zone && this.zone === other.zone &&
this.point === other.point && this.point === other.point &&
this.domain === other.domain this.domain === other.domain
); );
} }
getMatchAddr(pattern) { getMatchAddr(pattern) {
const m = FTN_PATTERN_REGEXP.exec(pattern); const m = FTN_PATTERN_REGEXP.exec(pattern);
if(m) { if(m) {
let addr = { }; let addr = { };
if(m[1]) { if(m[1]) {
addr.zone = m[1].slice(0, -1); addr.zone = m[1].slice(0, -1);
if('*' !== addr.zone) { if('*' !== addr.zone) {
addr.zone = parseInt(addr.zone); addr.zone = parseInt(addr.zone);
} }
} else { } else {
addr.zone = '*'; addr.zone = '*';
} }
if(m[2]) { if(m[2]) {
addr.net = m[2]; addr.net = m[2];
if('*' !== addr.net) { if('*' !== addr.net) {
addr.net = parseInt(addr.net); addr.net = parseInt(addr.net);
} }
} else { } else {
addr.net = '*'; addr.net = '*';
} }
if(m[3]) { if(m[3]) {
addr.node = m[3].substr(1); addr.node = m[3].substr(1);
if('*' !== addr.node) { if('*' !== addr.node) {
addr.node = parseInt(addr.node); addr.node = parseInt(addr.node);
} }
} else { } else {
addr.node = '*'; addr.node = '*';
} }
if(m[4]) { if(m[4]) {
addr.point = m[4].substr(1); addr.point = m[4].substr(1);
if('*' !== addr.point) { if('*' !== addr.point) {
addr.point = parseInt(addr.point); addr.point = parseInt(addr.point);
} }
} else { } else {
addr.point = '*'; addr.point = '*';
} }
if(m[5]) { if(m[5]) {
addr.domain = m[5].substr(1); addr.domain = m[5].substr(1);
} else { } else {
addr.domain = '*'; addr.domain = '*';
} }
return addr; return addr;
} }
} }
/* /*
getMatchScore(pattern) { getMatchScore(pattern) {
let score = 0; let score = 0;
const addr = this.getMatchAddr(pattern); const addr = this.getMatchAddr(pattern);
@ -116,92 +116,92 @@ module.exports = class Address {
} }
*/ */
isPatternMatch(pattern) { isPatternMatch(pattern) {
const addr = this.getMatchAddr(pattern); const addr = this.getMatchAddr(pattern);
if(addr) { if(addr) {
return ( return (
('*' === addr.net || this.net === addr.net) && ('*' === addr.net || this.net === addr.net) &&
('*' === addr.node || this.node === addr.node) && ('*' === addr.node || this.node === addr.node) &&
('*' === addr.zone || this.zone === addr.zone) && ('*' === addr.zone || this.zone === addr.zone) &&
('*' === addr.point || this.point === addr.point) && ('*' === addr.point || this.point === addr.point) &&
('*' === addr.domain || this.domain === addr.domain) ('*' === addr.domain || this.domain === addr.domain)
); );
} }
return false; return false;
} }
static fromString(addrStr) { static fromString(addrStr) {
const m = FTN_ADDRESS_REGEXP.exec(addrStr); const m = FTN_ADDRESS_REGEXP.exec(addrStr);
if(m) { if(m) {
// start with a 2D // start with a 2D
let addr = { let addr = {
net : parseInt(m[2]), net : parseInt(m[2]),
node : parseInt(m[3].substr(1)), node : parseInt(m[3].substr(1)),
}; };
// 3D: Addition of zone if present // 3D: Addition of zone if present
if(m[1]) { if(m[1]) {
addr.zone = parseInt(m[1].slice(0, -1)); addr.zone = parseInt(m[1].slice(0, -1));
} }
// 4D if optional point is present // 4D if optional point is present
if(m[4]) { if(m[4]) {
addr.point = parseInt(m[4].substr(1)); addr.point = parseInt(m[4].substr(1));
} }
// 5D with @domain // 5D with @domain
if(m[5]) { if(m[5]) {
addr.domain = m[5].substr(1); addr.domain = m[5].substr(1);
} }
return new Address(addr); return new Address(addr);
} }
} }
toString(dimensions) { toString(dimensions) {
dimensions = dimensions || '5D'; dimensions = dimensions || '5D';
let addrStr = `${this.zone}:${this.net}`; let addrStr = `${this.zone}:${this.net}`;
// allow for e.g. '4D' or 5 // allow for e.g. '4D' or 5
const dim = parseInt(dimensions.toString()[0]); const dim = parseInt(dimensions.toString()[0]);
if(dim >= 3) { if(dim >= 3) {
addrStr += `/${this.node}`; addrStr += `/${this.node}`;
} }
// missing & .0 are equiv for point // missing & .0 are equiv for point
if(dim >= 4 && this.point) { if(dim >= 4 && this.point) {
addrStr += `.${this.point}`; addrStr += `.${this.point}`;
} }
if(5 === dim && this.domain) { if(5 === dim && this.domain) {
addrStr += `@${this.domain.toLowerCase()}`; addrStr += `@${this.domain.toLowerCase()}`;
} }
return addrStr; return addrStr;
} }
static getComparator() { static getComparator() {
return function(left, right) { return function(left, right) {
let c = (left.zone || 0) - (right.zone || 0); let c = (left.zone || 0) - (right.zone || 0);
if(0 !== c) { if(0 !== c) {
return c; return c;
} }
c = (left.net || 0) - (right.net || 0); c = (left.net || 0) - (right.net || 0);
if(0 !== c) { if(0 !== c) {
return c; return c;
} }
c = (left.node || 0) - (right.node || 0); c = (left.node || 0) - (right.node || 0);
if(0 !== c) { if(0 !== c) {
return c; return c;
} }
return (left.domain || '').localeCompare(right.domain || ''); return (left.domain || '').localeCompare(right.domain || '');
}; };
} }
}; };

File diff suppressed because it is too large Load Diff

View File

@ -45,12 +45,12 @@ exports.getQuotePrefix = getQuotePrefix;
// See list here: https://github.com/Mithgol/node-fidonet-jam // See list here: https://github.com/Mithgol/node-fidonet-jam
function stringToNullPaddedBuffer(s, bufLen) { function stringToNullPaddedBuffer(s, bufLen) {
let buffer = Buffer.alloc(bufLen); let buffer = Buffer.alloc(bufLen);
let enc = iconv.encode(s, 'CP437').slice(0, bufLen); let enc = iconv.encode(s, 'CP437').slice(0, bufLen);
for(let i = 0; i < enc.length; ++i) { for(let i = 0; i < enc.length; ++i) {
buffer[i] = enc[i]; buffer[i] = enc[i];
} }
return buffer; return buffer;
} }
// //
@ -58,45 +58,45 @@ function stringToNullPaddedBuffer(s, bufLen) {
// //
// :TODO: Name the next couple methods better - for FTN *packets* // :TODO: Name the next couple methods better - for FTN *packets*
function getDateFromFtnDateTime(dateTime) { function getDateFromFtnDateTime(dateTime) {
// //
// Examples seen in the wild (Working): // Examples seen in the wild (Working):
// "12 Sep 88 18:17:59" // "12 Sep 88 18:17:59"
// "Tue 01 Jan 80 00:00" // "Tue 01 Jan 80 00:00"
// "27 Feb 15 00:00:03" // "27 Feb 15 00:00:03"
// //
// :TODO: Use moment.js here // :TODO: Use moment.js here
return moment(Date.parse(dateTime)); // Date.parse() allows funky formats return moment(Date.parse(dateTime)); // Date.parse() allows funky formats
// return (new Date(Date.parse(dateTime))).toISOString(); // return (new Date(Date.parse(dateTime))).toISOString();
} }
function getDateTimeString(m) { function getDateTimeString(m) {
// //
// From http://ftsc.org/docs/fts-0001.016: // From http://ftsc.org/docs/fts-0001.016:
// DateTime = (* a character string 20 characters long *) // DateTime = (* a character string 20 characters long *)
// (* 01 Jan 86 02:34:56 *) // (* 01 Jan 86 02:34:56 *)
// DayOfMonth " " Month " " Year " " // DayOfMonth " " Month " " Year " "
// " " HH ":" MM ":" SS // " " HH ":" MM ":" SS
// Null // Null
// //
// DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *) // DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *)
// Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" | // Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" |
// "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec" // "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec"
// Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00" // Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00"
// HH = "00" | .. | "23" // HH = "00" | .. | "23"
// MM = "00" | .. | "59" // MM = "00" | .. | "59"
// SS = "00" | .. | "59" // SS = "00" | .. | "59"
// //
if(!moment.isMoment(m)) { if(!moment.isMoment(m)) {
m = moment(m); m = moment(m);
} }
return m.format('DD MMM YY HH:mm:ss'); return m.format('DD MMM YY HH:mm:ss');
} }
function getMessageSerialNumber(messageId) { function getMessageSerialNumber(messageId) {
const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1)); const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1));
const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16); const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16);
return `00000000${hash}`.substr(-8); return `00000000${hash}`.substr(-8);
} }
// //
@ -143,11 +143,11 @@ function getMessageSerialNumber(messageId) {
// format, but that will only help when using newer Mystic versions. // format, but that will only help when using newer Mystic versions.
// //
function getMessageIdentifier(message, address, isNetMail = false) { function getMessageIdentifier(message, address, isNetMail = false) {
const addrStr = new Address(address).toString('5D'); const addrStr = new Address(address).toString('5D');
return isNetMail ? return isNetMail ?
`${addrStr} ${getMessageSerialNumber(message.messageId)}` : `${addrStr} ${getMessageSerialNumber(message.messageId)}` :
`${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}` `${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}`
; ;
} }
// //
@ -158,10 +158,10 @@ function getMessageIdentifier(message, address, isNetMail = false) {
// in which (<os>; <arch>; <nodeVer>) is used instead // in which (<os>; <arch>; <nodeVer>) is used instead
// //
function getProductIdentifier() { function getProductIdentifier() {
const version = getCleanEnigmaVersion(); const version = getCleanEnigmaVersion();
const nodeVer = process.version.substr(1); // remove 'v' prefix const nodeVer = process.version.substr(1); // remove 'v' prefix
return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
} }
// //
@ -171,7 +171,7 @@ function getProductIdentifier() {
// http://ftsc.org/docs/frl-1004.002 // http://ftsc.org/docs/frl-1004.002
// //
function getUTCTimeZoneOffset() { function getUTCTimeZoneOffset() {
return moment().format('ZZ').replace(/\+/, ''); return moment().format('ZZ').replace(/\+/, '');
} }
// //
@ -179,18 +179,18 @@ function getUTCTimeZoneOffset() {
// http://ftsc.org/docs/fsc-0032.001 // http://ftsc.org/docs/fsc-0032.001
// //
function getQuotePrefix(name) { function getQuotePrefix(name) {
let initials; let initials;
const parts = name.split(' '); const parts = name.split(' ');
if(parts.length > 1) { if(parts.length > 1) {
// First & Last initials - (Bryan Ashby -> BA) // First & Last initials - (Bryan Ashby -> BA)
initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase(); initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase();
} else { } else {
// Just use the first two - (NuSkooler -> Nu) // Just use the first two - (NuSkooler -> Nu)
initials = _.capitalize(name.slice(0, 2)); initials = _.capitalize(name.slice(0, 2));
} }
return ` ${initials}> `; return ` ${initials}> `;
} }
// //
@ -198,18 +198,18 @@ function getQuotePrefix(name) {
// http://ftsc.org/docs/fts-0004.001 // http://ftsc.org/docs/fts-0004.001
// //
function getOrigin(address) { function getOrigin(address) {
const config = Config(); const config = Config();
const origin = _.has(config, 'messageNetworks.originLine') ? const origin = _.has(config, 'messageNetworks.originLine') ?
config.messageNetworks.originLine : config.messageNetworks.originLine :
config.general.boardName; config.general.boardName;
const addrStr = new Address(address).toString('5D'); const addrStr = new Address(address).toString('5D');
return ` * Origin: ${origin} (${addrStr})`; return ` * Origin: ${origin} (${addrStr})`;
} }
function getTearLine() { function getTearLine() {
const nodeVer = process.version.substr(1); // remove 'v' prefix const nodeVer = process.version.substr(1); // remove 'v' prefix
return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
} }
// //
@ -217,17 +217,17 @@ function getTearLine() {
// http://ftsc.org/docs/frl-1005.001 // http://ftsc.org/docs/frl-1005.001
// //
function getVia(address) { function getVia(address) {
/* /*
FRL-1005.001 states teh following format: FRL-1005.001 states teh following format:
^AVia: <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone] ^AVia: <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone]
<Program Name> <Version> [Serial Number]<CR> <Program Name> <Version> [Serial Number]<CR>
*/ */
const addrStr = new Address(address).toString('5D'); const addrStr = new Address(address).toString('5D');
const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC'); const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC');
const version = getCleanEnigmaVersion(); const version = getCleanEnigmaVersion();
return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`; return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`;
} }
// //
@ -235,50 +235,50 @@ function getVia(address) {
// http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac // http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac
// //
function getIntl(toAddress, fromAddress) { function getIntl(toAddress, fromAddress) {
// //
// INTL differs from 'standard' kludges in that there is no ':' after "INTL" // INTL differs from 'standard' kludges in that there is no ':' after "INTL"
// //
// "<SOH>"INTL "<destination address>" "<origin address><CR>" // "<SOH>"INTL "<destination address>" "<origin address><CR>"
// "...These addresses shall be given on the form <zone>:<net>/<node>" // "...These addresses shall be given on the form <zone>:<net>/<node>"
// //
return `${toAddress.toString('3D')} ${fromAddress.toString('3D')}`; return `${toAddress.toString('3D')} ${fromAddress.toString('3D')}`;
} }
function getAbbreviatedNetNodeList(netNodes) { function getAbbreviatedNetNodeList(netNodes) {
let abbrList = ''; let abbrList = '';
let currNet; let currNet;
netNodes.forEach(netNode => { netNodes.forEach(netNode => {
if(_.isString(netNode)) { if(_.isString(netNode)) {
netNode = Address.fromString(netNode); netNode = Address.fromString(netNode);
} }
if(currNet !== netNode.net) { if(currNet !== netNode.net) {
abbrList += `${netNode.net}/`; abbrList += `${netNode.net}/`;
currNet = netNode.net; currNet = netNode.net;
} }
abbrList += `${netNode.node} `; abbrList += `${netNode.node} `;
}); });
return abbrList.trim(); // remove trailing space return abbrList.trim(); // remove trailing space
} }
// //
// Parse an abbreviated net/node list commonly used for SEEN-BY and PATH // Parse an abbreviated net/node list commonly used for SEEN-BY and PATH
// //
function parseAbbreviatedNetNodeList(netNodes) { function parseAbbreviatedNetNodeList(netNodes) {
const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g; const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g;
let net; let net;
let m; let m;
let results = []; let results = [];
while(null !== (m = re.exec(netNodes))) { while(null !== (m = re.exec(netNodes))) {
if(m[1] && m[2]) { if(m[1] && m[2]) {
net = parseInt(m[1]); net = parseInt(m[1]);
results.push(new Address( { net : net, node : parseInt(m[2]) } )); results.push(new Address( { net : net, node : parseInt(m[2]) } ));
} else if(net) { } else if(net) {
results.push(new Address( { net : net, node : parseInt(m[3]) } )); results.push(new Address( { net : net, node : parseInt(m[3]) } ));
} }
} }
return results; return results;
} }
// //
@ -295,7 +295,7 @@ function parseAbbreviatedNetNodeList(netNodes) {
// not the "SEEN-BY" prefix itself // not the "SEEN-BY" prefix itself
// //
function getUpdatedSeenByEntries(existingEntries, additions) { function getUpdatedSeenByEntries(existingEntries, additions) {
/* /*
From FTS-0004: From FTS-0004:
"There can be many seen-by lines at the end of Conference "There can be many seen-by lines at the end of Conference
@ -316,37 +316,37 @@ function getUpdatedSeenByEntries(existingEntries, additions) {
this field is not put in place by other Echomail compatible this field is not put in place by other Echomail compatible
programs." programs."
*/ */
existingEntries = existingEntries || []; existingEntries = existingEntries || [];
if(!_.isArray(existingEntries)) { if(!_.isArray(existingEntries)) {
existingEntries = [ existingEntries ]; existingEntries = [ existingEntries ];
} }
if(!_.isString(additions)) { if(!_.isString(additions)) {
additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions)); additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions));
} }
additions = additions.sort(Address.getComparator()); additions = additions.sort(Address.getComparator());
// //
// For now, we'll just append a new SEEN-BY entry // For now, we'll just append a new SEEN-BY entry
// //
// :TODO: we should at least try and update what is already there in a smart way // :TODO: we should at least try and update what is already there in a smart way
existingEntries.push(getAbbreviatedNetNodeList(additions)); existingEntries.push(getAbbreviatedNetNodeList(additions));
return existingEntries; return existingEntries;
} }
function getUpdatedPathEntries(existingEntries, localAddress) { function getUpdatedPathEntries(existingEntries, localAddress) {
// :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line // :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line
existingEntries = existingEntries || []; existingEntries = existingEntries || [];
if(!_.isArray(existingEntries)) { if(!_.isArray(existingEntries)) {
existingEntries = [ existingEntries ]; existingEntries = [ existingEntries ];
} }
existingEntries.push(getAbbreviatedNetNodeList( existingEntries.push(getAbbreviatedNetNodeList(
parseAbbreviatedNetNodeList(localAddress))); parseAbbreviatedNetNodeList(localAddress)));
return existingEntries; return existingEntries;
} }
// //
@ -354,71 +354,71 @@ function getUpdatedPathEntries(existingEntries, localAddress) {
// http://ftsc.org/docs/fts-5003.001 // http://ftsc.org/docs/fts-5003.001
// //
const ENCODING_TO_FTS_5003_001_CHARS = { const ENCODING_TO_FTS_5003_001_CHARS = {
// level 1 - generally should not be used // level 1 - generally should not be used
ascii : [ 'ASCII', 1 ], ascii : [ 'ASCII', 1 ],
'us-ascii' : [ 'ASCII', 1 ], 'us-ascii' : [ 'ASCII', 1 ],
// level 2 - 8 bit, ASCII based // level 2 - 8 bit, ASCII based
cp437 : [ 'CP437', 2 ], cp437 : [ 'CP437', 2 ],
cp850 : [ 'CP850', 2 ], cp850 : [ 'CP850', 2 ],
// level 3 - reserved // level 3 - reserved
// level 4 // level 4
utf8 : [ 'UTF-8', 4 ], utf8 : [ 'UTF-8', 4 ],
'utf-8' : [ 'UTF-8', 4 ], 'utf-8' : [ 'UTF-8', 4 ],
}; };
function getCharacterSetIdentifierByEncoding(encodingName) { function getCharacterSetIdentifierByEncoding(encodingName) {
const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()]; const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()];
return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase(); return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase();
} }
function getEncodingFromCharacterSetIdentifier(chrs) { function getEncodingFromCharacterSetIdentifier(chrs) {
const ident = chrs.split(' ')[0].toUpperCase(); const ident = chrs.split(' ')[0].toUpperCase();
// :TODO: fill in the rest!!! // :TODO: fill in the rest!!!
return { return {
// level 1 // level 1
'ASCII' : 'iso-646-1', 'ASCII' : 'iso-646-1',
'DUTCH' : 'iso-646', 'DUTCH' : 'iso-646',
'FINNISH' : 'iso-646-10', 'FINNISH' : 'iso-646-10',
'FRENCH' : 'iso-646', 'FRENCH' : 'iso-646',
'CANADIAN' : 'iso-646', 'CANADIAN' : 'iso-646',
'GERMAN' : 'iso-646', 'GERMAN' : 'iso-646',
'ITALIAN' : 'iso-646', 'ITALIAN' : 'iso-646',
'NORWEIG' : 'iso-646', 'NORWEIG' : 'iso-646',
'PORTU' : 'iso-646', 'PORTU' : 'iso-646',
'SPANISH' : 'iso-656', 'SPANISH' : 'iso-656',
'SWEDISH' : 'iso-646-10', 'SWEDISH' : 'iso-646-10',
'SWISS' : 'iso-646', 'SWISS' : 'iso-646',
'UK' : 'iso-646', 'UK' : 'iso-646',
'ISO-10' : 'iso-646-10', 'ISO-10' : 'iso-646-10',
// level 2 // level 2
'CP437' : 'cp437', 'CP437' : 'cp437',
'CP850' : 'cp850', 'CP850' : 'cp850',
'CP852' : 'cp852', 'CP852' : 'cp852',
'CP866' : 'cp866', 'CP866' : 'cp866',
'CP848' : 'cp848', 'CP848' : 'cp848',
'CP1250' : 'cp1250', 'CP1250' : 'cp1250',
'CP1251' : 'cp1251', 'CP1251' : 'cp1251',
'CP1252' : 'cp1252', 'CP1252' : 'cp1252',
'CP10000' : 'macroman', 'CP10000' : 'macroman',
'LATIN-1' : 'iso-8859-1', 'LATIN-1' : 'iso-8859-1',
'LATIN-2' : 'iso-8859-2', 'LATIN-2' : 'iso-8859-2',
'LATIN-5' : 'iso-8859-9', 'LATIN-5' : 'iso-8859-9',
'LATIN-9' : 'iso-8859-15', 'LATIN-9' : 'iso-8859-15',
// level 4 // level 4
'UTF-8' : 'utf8', 'UTF-8' : 'utf8',
// deprecated stuff // deprecated stuff
'IBMPC' : 'cp1250', // :TODO: validate 'IBMPC' : 'cp1250', // :TODO: validate
'+7_FIDO' : 'cp866', '+7_FIDO' : 'cp866',
'+7' : 'cp866', '+7' : 'cp866',
'MAC' : 'macroman', // :TODO: validate 'MAC' : 'macroman', // :TODO: validate
}[ident]; }[ident];
} }

View File

@ -15,154 +15,154 @@ exports.HorizontalMenuView = HorizontalMenuView;
// :TODO: Update this to allow scrolling if number of items cannot fit in width (similar to VerticalMenuView) // :TODO: Update this to allow scrolling if number of items cannot fit in width (similar to VerticalMenuView)
function HorizontalMenuView(options) { function HorizontalMenuView(options) {
options.cursor = options.cursor || 'hide'; options.cursor = options.cursor || 'hide';
if(!_.isNumber(options.itemSpacing)) { if(!_.isNumber(options.itemSpacing)) {
options.itemSpacing = 1; options.itemSpacing = 1;
} }
MenuView.call(this, options); MenuView.call(this, options);
this.dimens.height = 1; // always the case this.dimens.height = 1; // always the case
var self = this; var self = this;
this.getSpacer = function() { this.getSpacer = function() {
return new Array(self.itemSpacing + 1).join(' '); return new Array(self.itemSpacing + 1).join(' ');
}; };
this.performAutoScale = function() { this.performAutoScale = function() {
if(self.autoScale.width) { if(self.autoScale.width) {
var spacer = self.getSpacer(); var spacer = self.getSpacer();
var width = self.items.join(spacer).length + (spacer.length * 2); var width = self.items.join(spacer).length + (spacer.length * 2);
assert(width <= self.client.term.termWidth - self.position.col); assert(width <= self.client.term.termWidth - self.position.col);
self.dimens.width = width; self.dimens.width = width;
} }
}; };
this.performAutoScale(); this.performAutoScale();
this.cachePositions = function() { this.cachePositions = function() {
if(this.positionCacheExpired) { if(this.positionCacheExpired) {
var col = self.position.col; var col = self.position.col;
var spacer = self.getSpacer(); var spacer = self.getSpacer();
for(var i = 0; i < self.items.length; ++i) { for(var i = 0; i < self.items.length; ++i) {
self.items[i].col = col; self.items[i].col = col;
col += spacer.length + self.items[i].text.length + spacer.length; col += spacer.length + self.items[i].text.length + spacer.length;
} }
} }
this.positionCacheExpired = false; this.positionCacheExpired = false;
}; };
this.drawItem = function(index) { this.drawItem = function(index) {
assert(!this.positionCacheExpired); assert(!this.positionCacheExpired);
const item = self.items[index]; const item = self.items[index];
if(!item) { if(!item) {
return; return;
} }
let text; let text;
let sgr; let sgr;
if(item.focused && self.hasFocusItems()) { if(item.focused && self.hasFocusItems()) {
const focusItem = self.focusItems[index]; const focusItem = self.focusItems[index];
text = focusItem ? focusItem.text : item.text; text = focusItem ? focusItem.text : item.text;
sgr = ''; sgr = '';
} else if(this.complexItems) { } else if(this.complexItems) {
text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item));
sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR());
} else { } else {
text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle);
sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR());
} }
const drawWidth = strUtil.renderStringLength(text) + (self.getSpacer().length * 2); const drawWidth = strUtil.renderStringLength(text) + (self.getSpacer().length * 2);
self.client.term.write( self.client.term.write(
`${goto(self.position.row, item.col)}${sgr}${strUtil.pad(text, drawWidth, self.fillChar, 'center')}` `${goto(self.position.row, item.col)}${sgr}${strUtil.pad(text, drawWidth, self.fillChar, 'center')}`
); );
}; };
} }
require('util').inherits(HorizontalMenuView, MenuView); require('util').inherits(HorizontalMenuView, MenuView);
HorizontalMenuView.prototype.setHeight = function(height) { HorizontalMenuView.prototype.setHeight = function(height) {
height = parseInt(height, 10); height = parseInt(height, 10);
assert(1 === height); // nothing else allowed here assert(1 === height); // nothing else allowed here
HorizontalMenuView.super_.prototype.setHeight(this, height); HorizontalMenuView.super_.prototype.setHeight(this, height);
}; };
HorizontalMenuView.prototype.redraw = function() { HorizontalMenuView.prototype.redraw = function() {
HorizontalMenuView.super_.prototype.redraw.call(this); HorizontalMenuView.super_.prototype.redraw.call(this);
this.cachePositions(); this.cachePositions();
for(var i = 0; i < this.items.length; ++i) { for(var i = 0; i < this.items.length; ++i) {
this.items[i].focused = this.focusedItemIndex === i; this.items[i].focused = this.focusedItemIndex === i;
this.drawItem(i); this.drawItem(i);
} }
}; };
HorizontalMenuView.prototype.setPosition = function(pos) { HorizontalMenuView.prototype.setPosition = function(pos) {
HorizontalMenuView.super_.prototype.setPosition.call(this, pos); HorizontalMenuView.super_.prototype.setPosition.call(this, pos);
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
HorizontalMenuView.prototype.setFocus = function(focused) { HorizontalMenuView.prototype.setFocus = function(focused) {
HorizontalMenuView.super_.prototype.setFocus.call(this, focused); HorizontalMenuView.super_.prototype.setFocus.call(this, focused);
this.redraw(); this.redraw();
}; };
HorizontalMenuView.prototype.setItems = function(items) { HorizontalMenuView.prototype.setItems = function(items) {
HorizontalMenuView.super_.prototype.setItems.call(this, items); HorizontalMenuView.super_.prototype.setItems.call(this, items);
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
HorizontalMenuView.prototype.focusNext = function() { HorizontalMenuView.prototype.focusNext = function() {
if(this.items.length - 1 === this.focusedItemIndex) { if(this.items.length - 1 === this.focusedItemIndex) {
this.focusedItemIndex = 0; this.focusedItemIndex = 0;
} else { } else {
this.focusedItemIndex++; this.focusedItemIndex++;
} }
// :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes
this.redraw(); this.redraw();
HorizontalMenuView.super_.prototype.focusNext.call(this); HorizontalMenuView.super_.prototype.focusNext.call(this);
}; };
HorizontalMenuView.prototype.focusPrevious = function() { HorizontalMenuView.prototype.focusPrevious = function() {
if(0 === this.focusedItemIndex) { if(0 === this.focusedItemIndex) {
this.focusedItemIndex = this.items.length - 1; this.focusedItemIndex = this.items.length - 1;
} else { } else {
this.focusedItemIndex--; this.focusedItemIndex--;
} }
// :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes
this.redraw(); this.redraw();
HorizontalMenuView.super_.prototype.focusPrevious.call(this); HorizontalMenuView.super_.prototype.focusPrevious.call(this);
}; };
HorizontalMenuView.prototype.onKeyPress = function(ch, key) { HorizontalMenuView.prototype.onKeyPress = function(ch, key) {
if(key) { if(key) {
if(this.isKeyMapped('left', key.name)) { if(this.isKeyMapped('left', key.name)) {
this.focusPrevious(); this.focusPrevious();
} else if(this.isKeyMapped('right', key.name)) { } else if(this.isKeyMapped('right', key.name)) {
this.focusNext(); this.focusNext();
} }
} }
HorizontalMenuView.super_.prototype.onKeyPress.call(this, ch, key); HorizontalMenuView.super_.prototype.onKeyPress.call(this, ch, key);
}; };
HorizontalMenuView.prototype.getData = function() { HorizontalMenuView.prototype.getData = function() {
const item = this.getItem(this.focusedItemIndex); const item = this.getItem(this.focusedItemIndex);
return _.isString(item.data) ? item.data : this.focusedItemIndex; return _.isString(item.data) ? item.data : this.focusedItemIndex;
}; };

View File

@ -9,69 +9,69 @@ const stylizeString = require('./string_util.js').stylizeString;
const _ = require('lodash'); const _ = require('lodash');
module.exports = class KeyEntryView extends View { module.exports = class KeyEntryView extends View {
constructor(options) { constructor(options) {
options.acceptsFocus = valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = valueWithDefault(options.acceptsInput, true); options.acceptsInput = valueWithDefault(options.acceptsInput, true);
super(options); super(options);
this.eatTabKey = options.eatTabKey || true; this.eatTabKey = options.eatTabKey || true;
this.caseInsensitive = options.caseInsensitive || true; this.caseInsensitive = options.caseInsensitive || true;
if(Array.isArray(options.keys)) { if(Array.isArray(options.keys)) {
if(this.caseInsensitive) { if(this.caseInsensitive) {
this.keys = options.keys.map( k => k.toUpperCase() ); this.keys = options.keys.map( k => k.toUpperCase() );
} else { } else {
this.keys = options.keys; this.keys = options.keys;
} }
} }
} }
onKeyPress(ch, key) { onKeyPress(ch, key) {
const drawKey = ch; const drawKey = ch;
if(ch && this.caseInsensitive) { if(ch && this.caseInsensitive) {
ch = ch.toUpperCase(); ch = ch.toUpperCase();
} }
if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) { if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) {
this.redraw(); // sets position this.redraw(); // sets position
this.client.term.write(stylizeString(ch, this.textStyle)); this.client.term.write(stylizeString(ch, this.textStyle));
} }
this.keyEntered = ch || key.name; this.keyEntered = ch || key.name;
if(key && 'tab' === key.name && !this.eatTabKey) { if(key && 'tab' === key.name && !this.eatTabKey) {
return this.emit('action', 'next', key); return this.emit('action', 'next', key);
} }
this.emit('action', 'accept'); this.emit('action', 'accept');
// NOTE: we don't call super here. KeyEntryView is a special snowflake. // NOTE: we don't call super here. KeyEntryView is a special snowflake.
} }
setPropertyValue(propName, propValue) { setPropertyValue(propName, propValue) {
switch(propName) { switch(propName) {
case 'eatTabKey' : case 'eatTabKey' :
if(_.isBoolean(propValue)) { if(_.isBoolean(propValue)) {
this.eatTabKey = propValue; this.eatTabKey = propValue;
} }
break; break;
case 'caseInsensitive' : case 'caseInsensitive' :
if(_.isBoolean(propValue)) { if(_.isBoolean(propValue)) {
this.caseInsensitive = propValue; this.caseInsensitive = propValue;
} }
break; break;
case 'keys' : case 'keys' :
if(Array.isArray(propValue)) { if(Array.isArray(propValue)) {
this.keys = propValue; this.keys = propValue;
} }
break; break;
} }
super.setPropertyValue(propName, propValue); super.setPropertyValue(propName, propValue);
} }
getData() { return this.keyEntered; } getData() { return this.keyEntered; }
}; };

View File

@ -24,128 +24,128 @@ const _ = require('lodash');
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'Last Callers', name : 'Last Callers',
desc : 'Last callers to the system', desc : 'Last callers to the system',
author : 'NuSkooler', author : 'NuSkooler',
packageName : 'codes.l33t.enigma.lastcallers' packageName : 'codes.l33t.enigma.lastcallers'
}; };
const MciCodeIds = { const MciCodeIds = {
CallerList : 1, CallerList : 1,
}; };
exports.getModule = class LastCallersModule extends MenuModule { exports.getModule = class LastCallersModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
let loginHistory; let loginHistory;
let callersView; let callersView;
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu, mciMap : mciData.menu,
noInput : true, noInput : true,
}; };
vc.loadFromMenuConfig(loadOpts, callback); vc.loadFromMenuConfig(loadOpts, callback);
}, },
function fetchHistory(callback) { function fetchHistory(callback) {
callersView = vc.getView(MciCodeIds.CallerList); callersView = vc.getView(MciCodeIds.CallerList);
// fetch up // fetch up
StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => { StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => {
loginHistory = lh; loginHistory = lh;
if(self.menuConfig.config.hideSysOpLogin) { if(self.menuConfig.config.hideSysOpLogin) {
const noOpLoginHistory = loginHistory.filter(lh => { const noOpLoginHistory = loginHistory.filter(lh => {
return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId
}); });
// //
// If we have enough items to display, or hideSysOpLogin is set to 'always', // If we have enough items to display, or hideSysOpLogin is set to 'always',
// then set loginHistory to our filtered list. Else, we'll leave it be. // then set loginHistory to our filtered list. Else, we'll leave it be.
// //
if(noOpLoginHistory.length >= callersView.dimens.height || 'always' === self.menuConfig.config.hideSysOpLogin) { if(noOpLoginHistory.length >= callersView.dimens.height || 'always' === self.menuConfig.config.hideSysOpLogin) {
loginHistory = noOpLoginHistory; loginHistory = noOpLoginHistory;
} }
} }
// //
// Finally, we need to trim up the list to the needed size // Finally, we need to trim up the list to the needed size
// //
loginHistory = loginHistory.slice(0, callersView.dimens.height); loginHistory = loginHistory.slice(0, callersView.dimens.height);
return callback(err); return callback(err);
}); });
}, },
function getUserNamesAndProperties(callback) { function getUserNamesAndProperties(callback) {
const getPropOpts = { const getPropOpts = {
names : [ 'location', 'affiliation' ] names : [ 'location', 'affiliation' ]
}; };
const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD';
async.each( async.each(
loginHistory, loginHistory,
(item, next) => { (item, next) => {
item.userId = parseInt(item.log_value); item.userId = parseInt(item.log_value);
item.ts = moment(item.timestamp).format(dateTimeFormat); item.ts = moment(item.timestamp).format(dateTimeFormat);
User.getUserName(item.userId, (err, userName) => { User.getUserName(item.userId, (err, userName) => {
if(err) { if(err) {
item.deleted = true; item.deleted = true;
return next(null); return next(null);
} else { } else {
item.userName = userName || 'N/A'; item.userName = userName || 'N/A';
User.loadProperties(item.userId, getPropOpts, (err, props) => { User.loadProperties(item.userId, getPropOpts, (err, props) => {
if(!err && props) { if(!err && props) {
item.location = props.location || 'N/A'; item.location = props.location || 'N/A';
item.affiliation = item.affils = (props.affiliation || 'N/A'); item.affiliation = item.affils = (props.affiliation || 'N/A');
} else { } else {
item.location = 'N/A'; item.location = 'N/A';
item.affiliation = item.affils = 'N/A'; item.affiliation = item.affils = 'N/A';
} }
return next(null); return next(null);
}); });
} }
}); });
}, },
err => { err => {
loginHistory = loginHistory.filter(lh => true !== lh.deleted); loginHistory = loginHistory.filter(lh => true !== lh.deleted);
return callback(err); return callback(err);
} }
); );
}, },
function populateList(callback) { function populateList(callback) {
const listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affiliation} - {ts}'; const listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affiliation} - {ts}';
callersView.setItems(_.map(loginHistory, ce => stringFormat(listFormat, ce) ) ); callersView.setItems(_.map(loginHistory, ce => stringFormat(listFormat, ce) ) );
callersView.redraw(); callersView.redraw();
return callback(null); return callback(null);
} }
], ],
(err) => { (err) => {
if(err) { if(err) {
self.client.log.error( { error : err.toString() }, 'Error loading last callers'); self.client.log.error( { error : err.toString() }, 'Error loading last callers');
} }
cb(err); cb(err);
} }
); );
}); });
} }
}; };

View File

@ -14,51 +14,51 @@ exports.shutdown = shutdown;
exports.getServer = getServer; exports.getServer = getServer;
function startup(cb) { function startup(cb) {
return startListening(cb); return startListening(cb);
} }
function shutdown(cb) { function shutdown(cb) {
return cb(null); return cb(null);
} }
function getServer(packageName) { function getServer(packageName) {
return listeningServers[packageName]; return listeningServers[packageName];
} }
function startListening(cb) { function startListening(cb) {
const moduleUtil = require('./module_util.js'); // late load so we get Config const moduleUtil = require('./module_util.js'); // late load so we get Config
async.each( [ 'login', 'content' ], (category, next) => { async.each( [ 'login', 'content' ], (category, next) => {
moduleUtil.loadModulesForCategory(`${category}Servers`, (err, module) => { moduleUtil.loadModulesForCategory(`${category}Servers`, (err, module) => {
// :TODO: use enig error here! // :TODO: use enig error here!
if(err) { if(err) {
if('EENIGMODDISABLED' === err.code) { if('EENIGMODDISABLED' === err.code) {
logger.log.debug(err.message); logger.log.debug(err.message);
} else { } else {
logger.log.info( { err : err }, 'Failed loading module'); logger.log.info( { err : err }, 'Failed loading module');
} }
return; return;
} }
const moduleInst = new module.getModule(); const moduleInst = new module.getModule();
try { try {
moduleInst.createServer(); moduleInst.createServer();
if(!moduleInst.listen()) { if(!moduleInst.listen()) {
throw new Error('Failed listening'); throw new Error('Failed listening');
} }
listeningServers[module.moduleInfo.packageName] = { listeningServers[module.moduleInfo.packageName] = {
instance : moduleInst, instance : moduleInst,
info : module.moduleInfo, info : module.moduleInfo,
}; };
} catch(e) { } catch(e) {
logger.log.error(e, 'Exception caught creating server!'); logger.log.error(e, 'Exception caught creating server!');
} }
}, err => { }, err => {
return next(err); return next(err);
}); });
}, err => { }, err => {
return cb(err); return cb(err);
}); });
} }

View File

@ -9,66 +9,66 @@ const _ = require('lodash');
module.exports = class Log { module.exports = class Log {
static init() { static init() {
const Config = require('./config.js').get(); const Config = require('./config.js').get();
const logPath = Config.paths.logs; const logPath = Config.paths.logs;
const err = this.checkLogPath(logPath); const err = this.checkLogPath(logPath);
if(err) { if(err) {
console.error(err.message); // eslint-disable-line no-console console.error(err.message); // eslint-disable-line no-console
return process.exit(); return process.exit();
} }
const logStreams = []; const logStreams = [];
if(_.isObject(Config.logging.rotatingFile)) { if(_.isObject(Config.logging.rotatingFile)) {
Config.logging.rotatingFile.path = paths.join(logPath, Config.logging.rotatingFile.fileName); Config.logging.rotatingFile.path = paths.join(logPath, Config.logging.rotatingFile.fileName);
logStreams.push(Config.logging.rotatingFile); logStreams.push(Config.logging.rotatingFile);
} }
const serializers = { const serializers = {
err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc. err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc.
}; };
// try to remove sensitive info by default, e.g. 'password' fields // try to remove sensitive info by default, e.g. 'password' fields
[ 'formData', 'formValue' ].forEach(keyName => { [ 'formData', 'formValue' ].forEach(keyName => {
serializers[keyName] = (fd) => Log.hideSensitive(fd); serializers[keyName] = (fd) => Log.hideSensitive(fd);
}); });
this.log = bunyan.createLogger({ this.log = bunyan.createLogger({
name : 'ENiGMA½ BBS', name : 'ENiGMA½ BBS',
streams : logStreams, streams : logStreams,
serializers : serializers, serializers : serializers,
}); });
} }
static checkLogPath(logPath) { static checkLogPath(logPath) {
try { try {
if(!fs.statSync(logPath).isDirectory()) { if(!fs.statSync(logPath).isDirectory()) {
return new Error(`${logPath} is not a directory`); return new Error(`${logPath} is not a directory`);
} }
return null; return null;
} catch(e) { } catch(e) {
if('ENOENT' === e.code) { if('ENOENT' === e.code) {
return new Error(`${logPath} does not exist`); return new Error(`${logPath} does not exist`);
} }
return e; return e;
} }
} }
static hideSensitive(obj) { static hideSensitive(obj) {
try { try {
// //
// Use a regexp -- we don't know how nested fields we want to seek and destroy may be // Use a regexp -- we don't know how nested fields we want to seek and destroy may be
// //
return JSON.parse( return JSON.parse(
JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => { JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => {
return `"${valueName}":"********"`; return `"${valueName}":"********"`;
}) })
); );
} catch(e) { } catch(e) {
// be safe and return empty obj! // be safe and return empty obj!
return {}; return {};
} }
} }
}; };

View File

@ -11,77 +11,77 @@ const clientConns = require('./client_connections.js');
const _ = require('lodash'); const _ = require('lodash');
module.exports = class LoginServerModule extends ServerModule { module.exports = class LoginServerModule extends ServerModule {
constructor() { constructor() {
super(); super();
} }
// :TODO: we need to max connections -- e.g. from config 'maxConnections' // :TODO: we need to max connections -- e.g. from config 'maxConnections'
prepareClient(client, cb) { prepareClient(client, cb) {
const theme = require('./theme.js'); const theme = require('./theme.js');
// //
// Choose initial theme before we have user context // Choose initial theme before we have user context
// //
if('*' === conf.config.preLoginTheme) { if('*' === conf.config.preLoginTheme) {
client.user.properties.theme_id = theme.getRandomTheme() || ''; client.user.properties.theme_id = theme.getRandomTheme() || '';
} else { } else {
client.user.properties.theme_id = conf.config.preLoginTheme; client.user.properties.theme_id = conf.config.preLoginTheme;
} }
theme.setClientTheme(client, client.user.properties.theme_id); theme.setClientTheme(client, client.user.properties.theme_id);
return cb(null); // note: currently useless to use cb here - but this may change...again... return cb(null); // note: currently useless to use cb here - but this may change...again...
} }
handleNewClient(client, clientSock, modInfo) { handleNewClient(client, clientSock, modInfo) {
// //
// Start tracking the client. We'll assign it an ID which is // Start tracking the client. We'll assign it an ID which is
// just the index in our connections array. // just the index in our connections array.
// //
if(_.isUndefined(client.session)) { if(_.isUndefined(client.session)) {
client.session = {}; client.session = {};
} }
client.session.serverName = modInfo.name; client.session.serverName = modInfo.name;
client.session.isSecure = _.isBoolean(client.isSecure) ? client.isSecure : (modInfo.isSecure || false); client.session.isSecure = _.isBoolean(client.isSecure) ? client.isSecure : (modInfo.isSecure || false);
clientConns.addNewClient(client, clientSock); clientConns.addNewClient(client, clientSock);
client.on('ready', readyOptions => { client.on('ready', readyOptions => {
client.startIdleMonitor(); client.startIdleMonitor();
// Go to module -- use default error handler // Go to module -- use default error handler
this.prepareClient(client, () => { this.prepareClient(client, () => {
require('./connect.js').connectEntry(client, readyOptions.firstMenu); require('./connect.js').connectEntry(client, readyOptions.firstMenu);
}); });
}); });
client.on('end', () => { client.on('end', () => {
clientConns.removeClient(client); clientConns.removeClient(client);
}); });
client.on('error', err => { client.on('error', err => {
logger.log.info({ clientId : client.session.id }, 'Connection error: %s' % err.message); logger.log.info({ clientId : client.session.id }, 'Connection error: %s' % err.message);
}); });
client.on('close', err => { client.on('close', err => {
const logFunc = err ? logger.log.info : logger.log.debug; const logFunc = err ? logger.log.info : logger.log.debug;
logFunc( { clientId : client.session.id }, 'Connection closed'); logFunc( { clientId : client.session.id }, 'Connection closed');
clientConns.removeClient(client); clientConns.removeClient(client);
}); });
client.on('idle timeout', () => { client.on('idle timeout', () => {
client.log.info('User idle timeout expired'); client.log.info('User idle timeout expired');
client.menuStack.goto('idleLogoff', err => { client.menuStack.goto('idleLogoff', err => {
if(err) { if(err) {
// likely just doesn't exist // likely just doesn't exist
client.term.write('\nIdle timeout expired. Goodbye!\n'); client.term.write('\nIdle timeout expired. Goodbye!\n');
client.end(); client.end();
} }
}); });
}); });
} }
}; };

View File

@ -8,29 +8,29 @@ var _ = require('lodash');
module.exports = MailPacket; module.exports = MailPacket;
function MailPacket(options) { function MailPacket(options) {
events.EventEmitter.call(this); events.EventEmitter.call(this);
// map of network name -> address obj ( { zone, net, node, point, domain } ) // map of network name -> address obj ( { zone, net, node, point, domain } )
this.nodeAddresses = options.nodeAddresses || {}; this.nodeAddresses = options.nodeAddresses || {};
} }
require('util').inherits(MailPacket, events.EventEmitter); require('util').inherits(MailPacket, events.EventEmitter);
MailPacket.prototype.read = function(options) { MailPacket.prototype.read = function(options) {
// //
// options.packetPath | opts.packetBuffer: supplies a path-to-file // options.packetPath | opts.packetBuffer: supplies a path-to-file
// or a buffer containing packet data // or a buffer containing packet data
// //
// emits 'message' event per message read // emits 'message' event per message read
// //
assert(_.isString(options.packetPath) || Buffer.isBuffer(options.packetBuffer)); assert(_.isString(options.packetPath) || Buffer.isBuffer(options.packetBuffer));
}; };
MailPacket.prototype.write = function(options) { MailPacket.prototype.write = function(options) {
// //
// options.messages[]: array of message(s) to create packets from // options.messages[]: array of message(s) to create packets from
// //
// emits 'packet' event per packet constructed // emits 'packet' event per packet constructed
// //
assert(_.isArray(options.messages)); assert(_.isArray(options.messages));
}; };

View File

@ -22,60 +22,60 @@ const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))
Bar <baz@foobar.net> { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' } Bar <baz@foobar.net> { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' }
*/ */
function getAddressedToInfo(input) { function getAddressedToInfo(input) {
input = input.trim(); input = input.trim();
const firstAtPos = input.indexOf('@'); const firstAtPos = input.indexOf('@');
if(firstAtPos < 0) { if(firstAtPos < 0) {
let addr = Address.fromString(input); let addr = Address.fromString(input);
if(Address.isValidAddress(addr)) { if(Address.isValidAddress(addr)) {
return { flavor : Message.AddressFlavor.FTN, remote : input }; return { flavor : Message.AddressFlavor.FTN, remote : input };
} }
const lessThanPos = input.indexOf('<'); const lessThanPos = input.indexOf('<');
if(lessThanPos < 0) { if(lessThanPos < 0) {
return { name : input, flavor : Message.AddressFlavor.Local }; return { name : input, flavor : Message.AddressFlavor.Local };
} }
const greaterThanPos = input.indexOf('>'); const greaterThanPos = input.indexOf('>');
if(greaterThanPos < lessThanPos) { if(greaterThanPos < lessThanPos) {
return { name : input, flavor : Message.AddressFlavor.Local }; return { name : input, flavor : Message.AddressFlavor.Local };
} }
addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos)); addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos));
if(Address.isValidAddress(addr)) { if(Address.isValidAddress(addr)) {
return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() }; return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() };
} }
return { name : input, flavor : Message.AddressFlavor.Local }; return { name : input, flavor : Message.AddressFlavor.Local };
} }
const lessThanPos = input.indexOf('<'); const lessThanPos = input.indexOf('<');
const greaterThanPos = input.indexOf('>'); const greaterThanPos = input.indexOf('>');
if(lessThanPos > 0 && greaterThanPos > lessThanPos) { if(lessThanPos > 0 && greaterThanPos > lessThanPos) {
const addr = input.slice(lessThanPos + 1, greaterThanPos); const addr = input.slice(lessThanPos + 1, greaterThanPos);
const m = addr.match(EMAIL_REGEX); const m = addr.match(EMAIL_REGEX);
if(m) { if(m) {
return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.Email, remote : addr }; return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.Email, remote : addr };
} }
return { name : input, flavor : Message.AddressFlavor.Local }; return { name : input, flavor : Message.AddressFlavor.Local };
} }
let m = input.match(EMAIL_REGEX); let m = input.match(EMAIL_REGEX);
if(m) { if(m) {
return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input }; return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input };
} }
let addr = Address.fromString(input); // 5D? let addr = Address.fromString(input); // 5D?
if(Address.isValidAddress(addr)) { if(Address.isValidAddress(addr)) {
return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ; return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ;
} }
addr = Address.fromString(input.slice(firstAtPos + 1).trim()); addr = Address.fromString(input.slice(firstAtPos + 1).trim());
if(Address.isValidAddress(addr)) { if(Address.isValidAddress(addr)) {
return { name : input.slice(0, firstAtPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() }; return { name : input.slice(0, firstAtPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() };
} }
return { name : input, flavor : Message.AddressFlavor.Local }; return { name : input, flavor : Message.AddressFlavor.Local };
} }

View File

@ -28,181 +28,181 @@ exports.MaskEditTextView = MaskEditTextView;
// //
function MaskEditTextView(options) { function MaskEditTextView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
options.resizable = false; options.resizable = false;
TextView.call(this, options); TextView.call(this, options);
this.cursorPos = { x : 0 }; this.cursorPos = { x : 0 };
this.patternArrayPos = 0; this.patternArrayPos = 0;
var self = this; var self = this;
this.maskPattern = options.maskPattern || ''; this.maskPattern = options.maskPattern || '';
this.clientBackspace = function() { this.clientBackspace = function() {
var fillCharSGR = this.getStyleSGR(3) || this.getSGR(); var fillCharSGR = this.getStyleSGR(3) || this.getSGR();
this.client.term.write('\b' + fillCharSGR + this.fillChar + '\b' + this.getFocusSGR()); this.client.term.write('\b' + fillCharSGR + this.fillChar + '\b' + this.getFocusSGR());
}; };
this.drawText = function(s) { this.drawText = function(s) {
var textToDraw = strUtil.stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); var textToDraw = strUtil.stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle);
assert(textToDraw.length <= self.patternArray.length); assert(textToDraw.length <= self.patternArray.length);
// draw out the text we have so far // draw out the text we have so far
var i = 0; var i = 0;
var t = 0; var t = 0;
while(i < self.patternArray.length) { while(i < self.patternArray.length) {
if(_.isRegExp(self.patternArray[i])) { if(_.isRegExp(self.patternArray[i])) {
if(t < textToDraw.length) { if(t < textToDraw.length) {
self.client.term.write((self.hasFocus ? self.getFocusSGR() : self.getSGR()) + textToDraw[t]); self.client.term.write((self.hasFocus ? self.getFocusSGR() : self.getSGR()) + textToDraw[t]);
t++; t++;
} else { } else {
self.client.term.write((self.getStyleSGR(3) || '') + self.fillChar); self.client.term.write((self.getStyleSGR(3) || '') + self.fillChar);
} }
} else { } else {
var styleSgr = this.hasFocus ? (self.getStyleSGR(2) || '') : (self.getStyleSGR(1) || ''); var styleSgr = this.hasFocus ? (self.getStyleSGR(2) || '') : (self.getStyleSGR(1) || '');
self.client.term.write(styleSgr + self.maskPattern[i]); self.client.term.write(styleSgr + self.maskPattern[i]);
} }
i++; i++;
} }
}; };
this.buildPattern = function() { this.buildPattern = function() {
self.patternArray = []; self.patternArray = [];
self.maxLength = 0; self.maxLength = 0;
for(var i = 0; i < self.maskPattern.length; i++) { for(var i = 0; i < self.maskPattern.length; i++) {
// :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark! // :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark!
if(self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) { if(self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) {
self.patternArray.push(MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]); self.patternArray.push(MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]);
++self.maxLength; ++self.maxLength;
} else { } else {
self.patternArray.push(self.maskPattern[i]); self.patternArray.push(self.maskPattern[i]);
} }
} }
}; };
this.getEndOfTextColumn = function() { this.getEndOfTextColumn = function() {
return this.position.col + this.patternArrayPos; return this.position.col + this.patternArrayPos;
}; };
this.buildPattern(); this.buildPattern();
} }
require('util').inherits(MaskEditTextView, TextView); require('util').inherits(MaskEditTextView, TextView);
MaskEditTextView.maskPatternCharacterRegEx = { MaskEditTextView.maskPatternCharacterRegEx = {
'#' : /[0-9]/, // Numeric '#' : /[0-9]/, // Numeric
'A' : /[a-zA-Z]/, // Alpha 'A' : /[a-zA-Z]/, // Alpha
'@' : /[0-9a-zA-Z]/, // Alphanumeric '@' : /[0-9a-zA-Z]/, // Alphanumeric
'&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255 '&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255
}; };
MaskEditTextView.prototype.setText = function(text) { MaskEditTextView.prototype.setText = function(text) {
MaskEditTextView.super_.prototype.setText.call(this, text); MaskEditTextView.super_.prototype.setText.call(this, text);
if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText() if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText()
this.patternArrayPos = this.patternArray.length; this.patternArrayPos = this.patternArray.length;
} }
}; };
MaskEditTextView.prototype.setMaskPattern = function(pattern) { MaskEditTextView.prototype.setMaskPattern = function(pattern) {
this.dimens.width = pattern.length; this.dimens.width = pattern.length;
this.maskPattern = pattern; this.maskPattern = pattern;
this.buildPattern(); this.buildPattern();
}; };
MaskEditTextView.prototype.onKeyPress = function(ch, key) { MaskEditTextView.prototype.onKeyPress = function(ch, key) {
if(key) { if(key) {
if(this.isKeyMapped('backspace', key.name)) { if(this.isKeyMapped('backspace', key.name)) {
if(this.text.length > 0) { if(this.text.length > 0) {
this.patternArrayPos--; this.patternArrayPos--;
assert(this.patternArrayPos >= 0); assert(this.patternArrayPos >= 0);
if(_.isRegExp(this.patternArray[this.patternArrayPos])) { if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
this.text = this.text.substr(0, this.text.length - 1); this.text = this.text.substr(0, this.text.length - 1);
this.clientBackspace(); this.clientBackspace();
} else { } else {
while(this.patternArrayPos > 0) { while(this.patternArrayPos > 0) {
if(_.isRegExp(this.patternArray[this.patternArrayPos])) { if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
this.text = this.text.substr(0, this.text.length - 1); this.text = this.text.substr(0, this.text.length - 1);
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1)); this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1));
this.clientBackspace(); this.clientBackspace();
break; break;
} }
this.patternArrayPos--; this.patternArrayPos--;
} }
} }
} }
return; return;
} else if(this.isKeyMapped('clearLine', key.name)) { } else if(this.isKeyMapped('clearLine', key.name)) {
this.text = ''; this.text = '';
this.patternArrayPos = 0; this.patternArrayPos = 0;
this.setFocus(true); // redraw + adjust cursor this.setFocus(true); // redraw + adjust cursor
return; return;
} }
} }
if(ch && strUtil.isPrintable(ch)) { if(ch && strUtil.isPrintable(ch)) {
if(this.text.length < this.maxLength) { if(this.text.length < this.maxLength) {
ch = strUtil.stylizeString(ch, this.textStyle); ch = strUtil.stylizeString(ch, this.textStyle);
if(!ch.match(this.patternArray[this.patternArrayPos])) { if(!ch.match(this.patternArray[this.patternArrayPos])) {
return; return;
} }
this.text += ch; this.text += ch;
this.patternArrayPos++; this.patternArrayPos++;
while(this.patternArrayPos < this.patternArray.length && while(this.patternArrayPos < this.patternArray.length &&
!_.isRegExp(this.patternArray[this.patternArrayPos])) !_.isRegExp(this.patternArray[this.patternArrayPos]))
{ {
this.patternArrayPos++; this.patternArrayPos++;
} }
this.redraw(); this.redraw();
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn())); this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn()));
} }
} }
MaskEditTextView.super_.prototype.onKeyPress.call(this, ch, key); MaskEditTextView.super_.prototype.onKeyPress.call(this, ch, key);
}; };
MaskEditTextView.prototype.setPropertyValue = function(propName, value) { MaskEditTextView.prototype.setPropertyValue = function(propName, value) {
switch(propName) { switch(propName) {
case 'maskPattern' : this.setMaskPattern(value); break; case 'maskPattern' : this.setMaskPattern(value); break;
} }
MaskEditTextView.super_.prototype.setPropertyValue.call(this, propName, value); MaskEditTextView.super_.prototype.setPropertyValue.call(this, propName, value);
}; };
MaskEditTextView.prototype.getData = function() { MaskEditTextView.prototype.getData = function() {
var rawData = MaskEditTextView.super_.prototype.getData.call(this); var rawData = MaskEditTextView.super_.prototype.getData.call(this);
if(!rawData || 0 === rawData.length) { if(!rawData || 0 === rawData.length) {
return rawData; return rawData;
} }
var data = ''; var data = '';
assert(rawData.length <= this.patternArray.length); assert(rawData.length <= this.patternArray.length);
var p = 0; var p = 0;
for(var i = 0; i < this.patternArray.length; ++i) { for(var i = 0; i < this.patternArray.length; ++i) {
if(_.isRegExp(this.patternArray[i])) { if(_.isRegExp(this.patternArray[i])) {
data += rawData[p++]; data += rawData[p++];
} else { } else {
data += this.patternArray[i]; data += this.patternArray[i];
} }
} }
return data; return data;
}; };

View File

@ -22,186 +22,186 @@ const _ = require('lodash');
exports.MCIViewFactory = MCIViewFactory; exports.MCIViewFactory = MCIViewFactory;
function MCIViewFactory(client) { function MCIViewFactory(client) {
this.client = client; this.client = client;
} }
MCIViewFactory.UserViewCodes = [ MCIViewFactory.UserViewCodes = [
'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE', 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE',
// //
// XY is a special MCI code that allows finding positions // XY is a special MCI code that allows finding positions
// and counts for key lookup, but does not explicitly // and counts for key lookup, but does not explicitly
// represent a visible View on it's own // represent a visible View on it's own
// //
'XY', 'XY',
]; ];
MCIViewFactory.prototype.createFromMCI = function(mci) { MCIViewFactory.prototype.createFromMCI = function(mci) {
assert(mci.code); assert(mci.code);
assert(mci.id > 0); assert(mci.id > 0);
assert(mci.position); assert(mci.position);
var view; var view;
var options = { var options = {
client : this.client, client : this.client,
id : mci.id, id : mci.id,
ansiSGR : mci.SGR, ansiSGR : mci.SGR,
ansiFocusSGR : mci.focusSGR, ansiFocusSGR : mci.focusSGR,
position : { row : mci.position[0], col : mci.position[1] }, position : { row : mci.position[0], col : mci.position[1] },
}; };
// :TODO: These should use setPropertyValue()! // :TODO: These should use setPropertyValue()!
function setOption(pos, name) { function setOption(pos, name) {
if(mci.args.length > pos && mci.args[pos].length > 0) { if(mci.args.length > pos && mci.args[pos].length > 0) {
options[name] = mci.args[pos]; options[name] = mci.args[pos];
} }
} }
function setWidth(pos) { function setWidth(pos) {
if(mci.args.length > pos && mci.args[pos].length > 0) { if(mci.args.length > pos && mci.args[pos].length > 0) {
if(!_.isObject(options.dimens)) { if(!_.isObject(options.dimens)) {
options.dimens = {}; options.dimens = {};
} }
options.dimens.width = parseInt(mci.args[pos], 10); options.dimens.width = parseInt(mci.args[pos], 10);
} }
} }
function setFocusOption(pos, name) { function setFocusOption(pos, name) {
if(mci.focusArgs && mci.focusArgs.length > pos && mci.focusArgs[pos].length > 0) { if(mci.focusArgs && mci.focusArgs.length > pos && mci.focusArgs[pos].length > 0) {
options[name] = mci.focusArgs[pos]; options[name] = mci.focusArgs[pos];
} }
} }
// //
// Note: Keep this in sync with UserViewCodes above! // Note: Keep this in sync with UserViewCodes above!
// //
switch(mci.code) { switch(mci.code) {
// Text Label (Text View) // Text Label (Text View)
case 'TL' : case 'TL' :
setOption(0, 'textStyle'); setOption(0, 'textStyle');
setOption(1, 'justify'); setOption(1, 'justify');
setWidth(2); setWidth(2);
view = new TextView(options); view = new TextView(options);
break; break;
// Edit Text // Edit Text
case 'ET' : case 'ET' :
setWidth(0); setWidth(0);
setOption(1, 'textStyle'); setOption(1, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new EditTextView(options); view = new EditTextView(options);
break; break;
// Masked Edit Text // Masked Edit Text
case 'ME' : case 'ME' :
setOption(0, 'textStyle'); setOption(0, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new MaskEditTextView(options); view = new MaskEditTextView(options);
break; break;
// Multi Line Edit Text // Multi Line Edit Text
case 'MT' : case 'MT' :
// :TODO: apply params // :TODO: apply params
view = new MultiLineEditTextView(options); view = new MultiLineEditTextView(options);
break; break;
// Pre-defined Label (Text View) // Pre-defined Label (Text View)
// :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove // :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove
case 'PL' : case 'PL' :
if(mci.args.length > 0) { if(mci.args.length > 0) {
options.text = getPredefinedMCIValue(this.client, mci.args[0]); options.text = getPredefinedMCIValue(this.client, mci.args[0]);
if(options.text) { if(options.text) {
setOption(1, 'textStyle'); setOption(1, 'textStyle');
setOption(2, 'justify'); setOption(2, 'justify');
setWidth(3); setWidth(3);
view = new TextView(options); view = new TextView(options);
} }
} }
break; break;
// Button // Button
case 'BT' : case 'BT' :
if(mci.args.length > 0) { if(mci.args.length > 0) {
options.dimens = { width : parseInt(mci.args[0], 10) }; options.dimens = { width : parseInt(mci.args[0], 10) };
} }
setOption(1, 'textStyle'); setOption(1, 'textStyle');
setOption(2, 'justify'); setOption(2, 'justify');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new ButtonView(options); view = new ButtonView(options);
break; break;
// Vertial Menu // Vertial Menu
case 'VM' : case 'VM' :
setOption(0, 'itemSpacing'); setOption(0, 'itemSpacing');
setOption(1, 'justify'); setOption(1, 'justify');
setOption(2, 'textStyle'); setOption(2, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new VerticalMenuView(options); view = new VerticalMenuView(options);
break; break;
// Horizontal Menu // Horizontal Menu
case 'HM' : case 'HM' :
setOption(0, 'itemSpacing'); setOption(0, 'itemSpacing');
setOption(1, 'textStyle'); setOption(1, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new HorizontalMenuView(options); view = new HorizontalMenuView(options);
break; break;
case 'SM' : case 'SM' :
setOption(0, 'textStyle'); setOption(0, 'textStyle');
setOption(1, 'justify'); setOption(1, 'justify');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new SpinnerMenuView(options); view = new SpinnerMenuView(options);
break; break;
case 'TM' : case 'TM' :
if(mci.args.length > 0) { if(mci.args.length > 0) {
var styleSG1 = { fg : parseInt(mci.args[0], 10) }; var styleSG1 = { fg : parseInt(mci.args[0], 10) };
if(mci.args.length > 1) { if(mci.args.length > 1) {
styleSG1.bg = parseInt(mci.args[1], 10); styleSG1.bg = parseInt(mci.args[1], 10);
} }
options.styleSG1 = ansi.getSGRFromGraphicRendition(styleSG1, true); options.styleSG1 = ansi.getSGRFromGraphicRendition(styleSG1, true);
} }
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new ToggleMenuView(options); view = new ToggleMenuView(options);
break; break;
case 'KE' : case 'KE' :
view = new KeyEntryView(options); view = new KeyEntryView(options);
break; break;
default : default :
options.text = getPredefinedMCIValue(this.client, mci.code); options.text = getPredefinedMCIValue(this.client, mci.code);
if(_.isString(options.text)) { if(_.isString(options.text)) {
setWidth(0); setWidth(0);
setOption(1, 'textStyle'); setOption(1, 'textStyle');
setOption(2, 'justify'); setOption(2, 'justify');
view = new TextView(options); view = new TextView(options);
} }
break; break;
} }
if(view) { if(view) {
view.mciCode = mci.code; view.mciCode = mci.code;
} }
return view; return view;
}; };

View File

@ -19,358 +19,358 @@ const _ = require('lodash');
exports.MenuModule = class MenuModule extends PluginModule { exports.MenuModule = class MenuModule extends PluginModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.menuName = options.menuName; this.menuName = options.menuName;
this.menuConfig = options.menuConfig; this.menuConfig = options.menuConfig;
this.client = options.client; this.client = options.client;
this.menuConfig.options = options.menuConfig.options || {}; this.menuConfig.options = options.menuConfig.options || {};
this.menuMethods = {}; // methods called from @method's this.menuMethods = {}; // methods called from @method's
this.menuConfig.config = this.menuConfig.config || {}; this.menuConfig.config = this.menuConfig.config || {};
this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls; this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls;
this.viewControllers = {}; this.viewControllers = {};
} }
enter() { enter() {
this.initSequence(); this.initSequence();
} }
leave() { leave() {
this.detachViewControllers(); this.detachViewControllers();
} }
initSequence() { initSequence() {
const self = this; const self = this;
const mciData = {}; const mciData = {};
let pausePosition; let pausePosition;
async.series( async.series(
[ [
function beforeDisplayArt(callback) { function beforeDisplayArt(callback) {
self.beforeArt(callback); self.beforeArt(callback);
}, },
function displayMenuArt(callback) { function displayMenuArt(callback) {
if(!_.isString(self.menuConfig.art)) { if(!_.isString(self.menuConfig.art)) {
return callback(null); return callback(null);
} }
self.displayAsset( self.displayAsset(
self.menuConfig.art, self.menuConfig.art,
self.menuConfig.options, self.menuConfig.options,
(err, artData) => { (err, artData) => {
if(err) { if(err) {
self.client.log.trace('Could not display art', { art : self.menuConfig.art, reason : err.message } ); self.client.log.trace('Could not display art', { art : self.menuConfig.art, reason : err.message } );
} else { } else {
mciData.menu = artData.mciMap; mciData.menu = artData.mciMap;
} }
return callback(null); // any errors are non-fatal return callback(null); // any errors are non-fatal
} }
); );
}, },
function moveToPromptLocation(callback) { function moveToPromptLocation(callback) {
if(self.menuConfig.prompt) { if(self.menuConfig.prompt) {
// :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements // :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements
} }
return callback(null); return callback(null);
}, },
function displayPromptArt(callback) { function displayPromptArt(callback) {
if(!_.isString(self.menuConfig.prompt)) { if(!_.isString(self.menuConfig.prompt)) {
return callback(null); return callback(null);
} }
if(!_.isObject(self.menuConfig.promptConfig)) { if(!_.isObject(self.menuConfig.promptConfig)) {
return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found')); return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found'));
} }
self.displayAsset( self.displayAsset(
self.menuConfig.promptConfig.art, self.menuConfig.promptConfig.art,
self.menuConfig.options, self.menuConfig.options,
(err, artData) => { (err, artData) => {
if(artData) { if(artData) {
mciData.prompt = artData.mciMap; mciData.prompt = artData.mciMap;
} }
return callback(err); // pass err here; prompts *must* have art return callback(err); // pass err here; prompts *must* have art
} }
); );
}, },
function recordCursorPosition(callback) { function recordCursorPosition(callback) {
if(!self.shouldPause()) { if(!self.shouldPause()) {
return callback(null); // cursor position not needed return callback(null); // cursor position not needed
} }
self.client.once('cursor position report', pos => { self.client.once('cursor position report', pos => {
pausePosition = { row : pos[0], col : 1 }; pausePosition = { row : pos[0], col : 1 };
self.client.log.trace('After art position recorded', pausePosition ); self.client.log.trace('After art position recorded', pausePosition );
return callback(null); return callback(null);
}); });
self.client.term.rawWrite(ansi.queryPos()); self.client.term.rawWrite(ansi.queryPos());
}, },
function afterArtDisplayed(callback) { function afterArtDisplayed(callback) {
return self.mciReady(mciData, callback); return self.mciReady(mciData, callback);
}, },
function displayPauseIfRequested(callback) { function displayPauseIfRequested(callback) {
if(!self.shouldPause()) { if(!self.shouldPause()) {
return callback(null); return callback(null);
} }
return self.pausePrompt(pausePosition, callback); return self.pausePrompt(pausePosition, callback);
}, },
function finishAndNext(callback) { function finishAndNext(callback) {
self.finishedLoading(); self.finishedLoading();
return self.autoNextMenu(callback); return self.autoNextMenu(callback);
} }
], ],
err => { err => {
if(err) { if(err) {
self.client.log.warn('Error during init sequence', { error : err.message } ); self.client.log.warn('Error during init sequence', { error : err.message } );
return self.prevMenu( () => { /* dummy */ } ); return self.prevMenu( () => { /* dummy */ } );
} }
} }
); );
} }
beforeArt(cb) { beforeArt(cb) {
if(_.isNumber(this.menuConfig.options.baudRate)) { if(_.isNumber(this.menuConfig.options.baudRate)) {
// :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here // :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here
this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate)); this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate));
} }
if(this.cls) { if(this.cls) {
this.client.term.rawWrite(ansi.resetScreen()); this.client.term.rawWrite(ansi.resetScreen());
} }
return cb(null); return cb(null);
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
// available for sub-classes // available for sub-classes
return cb(null); return cb(null);
} }
finishedLoading() { finishedLoading() {
// nothing in base // nothing in base
} }
getSaveState() { getSaveState() {
// nothing in base // nothing in base
} }
restoreSavedState(/*savedState*/) { restoreSavedState(/*savedState*/) {
// nothing in base // nothing in base
} }
getMenuResult() { getMenuResult() {
// default to the formData that was provided @ a submit, if any // default to the formData that was provided @ a submit, if any
return this.submitFormData; return this.submitFormData;
} }
nextMenu(cb) { nextMenu(cb) {
if(!this.haveNext()) { if(!this.haveNext()) {
return this.prevMenu(cb); // no next, go to prev return this.prevMenu(cb); // no next, go to prev
} }
return this.client.menuStack.next(cb); return this.client.menuStack.next(cb);
} }
prevMenu(cb) { prevMenu(cb) {
return this.client.menuStack.prev(cb); return this.client.menuStack.prev(cb);
} }
gotoMenu(name, options, cb) { gotoMenu(name, options, cb) {
return this.client.menuStack.goto(name, options, cb); return this.client.menuStack.goto(name, options, cb);
} }
addViewController(name, vc) { addViewController(name, vc) {
assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`); assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`);
this.viewControllers[name] = vc; this.viewControllers[name] = vc;
return vc; return vc;
} }
detachViewControllers() { detachViewControllers() {
Object.keys(this.viewControllers).forEach( name => { Object.keys(this.viewControllers).forEach( name => {
this.viewControllers[name].detachClientEvents(); this.viewControllers[name].detachClientEvents();
}); });
} }
shouldPause() { shouldPause() {
return ('end' === this.menuConfig.options.pause || true === this.menuConfig.options.pause); return ('end' === this.menuConfig.options.pause || true === this.menuConfig.options.pause);
} }
hasNextTimeout() { hasNextTimeout() {
return _.isNumber(this.menuConfig.options.nextTimeout); return _.isNumber(this.menuConfig.options.nextTimeout);
} }
haveNext() { haveNext() {
return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next)); return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next));
} }
autoNextMenu(cb) { autoNextMenu(cb) {
const self = this; const self = this;
function gotoNextMenu() { function gotoNextMenu() {
if(self.haveNext()) { if(self.haveNext()) {
return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb); return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb);
} else { } else {
return self.prevMenu(cb); return self.prevMenu(cb);
} }
} }
if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) { if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) {
if(this.hasNextTimeout()) { if(this.hasNextTimeout()) {
setTimeout( () => { setTimeout( () => {
return gotoNextMenu(); return gotoNextMenu();
}, this.menuConfig.options.nextTimeout); }, this.menuConfig.options.nextTimeout);
} else { } else {
return gotoNextMenu(); return gotoNextMenu();
} }
} }
} }
standardMCIReadyHandler(mciData, cb) { standardMCIReadyHandler(mciData, cb) {
// //
// A quick rundown: // A quick rundown:
// * We may have mciData.menu, mciData.prompt, or both. // * We may have mciData.menu, mciData.prompt, or both.
// * Prompt form is favored over menu form if both are present. // * Prompt form is favored over menu form if both are present.
// * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve) // * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve)
// //
const self = this; const self = this;
async.series( async.series(
[ [
function addViewControllers(callback) { function addViewControllers(callback) {
_.forEach(mciData, (mciMap, name) => { _.forEach(mciData, (mciMap, name) => {
assert('menu' === name || 'prompt' === name); assert('menu' === name || 'prompt' === name);
self.addViewController(name, new ViewController( { client : self.client } ) ); self.addViewController(name, new ViewController( { client : self.client } ) );
}); });
return callback(null); return callback(null);
}, },
function createMenu(callback) { function createMenu(callback) {
if(!self.viewControllers.menu) { if(!self.viewControllers.menu) {
return callback(null); return callback(null);
} }
const menuLoadOpts = { const menuLoadOpts = {
mciMap : mciData.menu, mciMap : mciData.menu,
callingMenu : self, callingMenu : self,
withoutForm : _.isObject(mciData.prompt), withoutForm : _.isObject(mciData.prompt),
}; };
self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => { self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => {
return callback(err); return callback(err);
}); });
}, },
function createPrompt(callback) { function createPrompt(callback) {
if(!self.viewControllers.prompt) { if(!self.viewControllers.prompt) {
return callback(null); return callback(null);
} }
const promptLoadOpts = { const promptLoadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.prompt, mciMap : mciData.prompt,
}; };
self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => { self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => {
return callback(err); return callback(err);
}); });
} }
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
} }
displayAsset(name, options, cb) { displayAsset(name, options, cb) {
if(_.isFunction(options)) { if(_.isFunction(options)) {
cb = options; cb = options;
options = {}; options = {};
} }
if(options.clearScreen) { if(options.clearScreen) {
this.client.term.rawWrite(ansi.resetScreen()); this.client.term.rawWrite(ansi.resetScreen());
} }
return theme.displayThemedAsset( return theme.displayThemedAsset(
name, name,
this.client, this.client,
Object.assign( { font : this.menuConfig.config.font }, options ), Object.assign( { font : this.menuConfig.config.font }, options ),
(err, artData) => { (err, artData) => {
if(cb) { if(cb) {
return cb(err, artData); return cb(err, artData);
} }
} }
); );
} }
prepViewController(name, formId, mciMap, cb) { prepViewController(name, formId, mciMap, cb) {
if(_.isUndefined(this.viewControllers[name])) { if(_.isUndefined(this.viewControllers[name])) {
const vcOpts = { const vcOpts = {
client : this.client, client : this.client,
formId : formId, formId : formId,
}; };
const vc = this.addViewController(name, new ViewController(vcOpts)); const vc = this.addViewController(name, new ViewController(vcOpts));
const loadOpts = { const loadOpts = {
callingMenu : this, callingMenu : this,
mciMap : mciMap, mciMap : mciMap,
formId : formId, formId : formId,
}; };
return vc.loadFromMenuConfig(loadOpts, err => { return vc.loadFromMenuConfig(loadOpts, err => {
return cb(err, vc); return cb(err, vc);
}); });
} }
this.viewControllers[name].setFocus(true); this.viewControllers[name].setFocus(true);
return cb(null, this.viewControllers[name]); return cb(null, this.viewControllers[name]);
} }
prepViewControllerWithArt(name, formId, options, cb) { prepViewControllerWithArt(name, formId, options, cb) {
this.displayAsset( this.displayAsset(
this.menuConfig.config.art[name], this.menuConfig.config.art[name],
options, options,
(err, artData) => { (err, artData) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
return this.prepViewController(name, formId, artData.mciMap, cb); return this.prepViewController(name, formId, artData.mciMap, cb);
} }
); );
} }
optionalMoveToPosition(position) { optionalMoveToPosition(position) {
if(position) { if(position) {
position.x = position.row || position.x || 1; position.x = position.row || position.x || 1;
position.y = position.col || position.y || 1; position.y = position.col || position.y || 1;
this.client.term.rawWrite(ansi.goto(position.x, position.y)); this.client.term.rawWrite(ansi.goto(position.x, position.y));
} }
} }
pausePrompt(position, cb) { pausePrompt(position, cb) {
if(!cb && _.isFunction(position)) { if(!cb && _.isFunction(position)) {
cb = position; cb = position;
position = null; position = null;
} }
this.optionalMoveToPosition(position); this.optionalMoveToPosition(position);
return theme.displayThemedPause(this.client, cb); return theme.displayThemedPause(this.client, cb);
} }
/* /*
:TODO: this needs quite a bit of work - but would be nice: promptForInput(..., (err, formData) => ... ) :TODO: this needs quite a bit of work - but would be nice: promptForInput(..., (err, formData) => ... )
promptForInput(formName, name, options, cb) { promptForInput(formName, name, options, cb) {
if(!cb && _.isFunction(options)) { if(!cb && _.isFunction(options)) {
@ -386,55 +386,55 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
*/ */
setViewText(formName, mciId, text, appendMultiLine) { setViewText(formName, mciId, text, appendMultiLine) {
const view = this.viewControllers[formName].getView(mciId); const view = this.viewControllers[formName].getView(mciId);
if(!view) { if(!view) {
return; return;
} }
if(appendMultiLine && (view instanceof MultiLineEditTextView)) { if(appendMultiLine && (view instanceof MultiLineEditTextView)) {
view.addText(text); view.addText(text);
} else { } else {
view.setText(text); view.setText(text);
} }
} }
updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) { updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) {
options = options || {}; options = options || {};
let textView; let textView;
let customMciId = startId; let customMciId = startId;
const config = this.menuConfig.config; const config = this.menuConfig.config;
const endId = options.endId || 99; // we'll fail to get a view before 99 const endId = options.endId || 99; // we'll fail to get a view before 99
while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) { while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) {
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10"
const format = config[key]; const format = config[key];
if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) { if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) {
const text = stringFormat(format, fmtObj); const text = stringFormat(format, fmtObj);
if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) { if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) {
textView.addText(text); textView.addText(text);
} else { } else {
textView.setText(text); textView.setText(text);
} }
} }
++customMciId; ++customMciId;
} }
} }
refreshPredefinedMciViewsByCode(formName, mciCodes) { refreshPredefinedMciViewsByCode(formName, mciCodes) {
const form = _.get(this, [ 'viewControllers', formName] ); const form = _.get(this, [ 'viewControllers', formName] );
if(form) { if(form) {
form.getViewsByMciCode(mciCodes).forEach(v => { form.getViewsByMciCode(mciCodes).forEach(v => {
if(!v.setText) { if(!v.setText) {
return; return;
} }
v.setText(getPredefinedMCIValue(this.client, v.mciCode)); v.setText(getPredefinedMCIValue(this.client, v.mciCode));
}); });
} }
} }
}; };

View File

@ -12,180 +12,180 @@ const assert = require('assert');
// :TODO: Stack is backwards.... top should be most recent! :) // :TODO: Stack is backwards.... top should be most recent! :)
module.exports = class MenuStack { module.exports = class MenuStack {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
this.stack = []; this.stack = [];
} }
push(moduleInfo) { push(moduleInfo) {
return this.stack.push(moduleInfo); return this.stack.push(moduleInfo);
} }
pop() { pop() {
return this.stack.pop(); return this.stack.pop();
} }
peekPrev() { peekPrev() {
if(this.stackSize > 1) { if(this.stackSize > 1) {
return this.stack[this.stack.length - 2]; return this.stack[this.stack.length - 2];
} }
} }
top() { top() {
if(this.stackSize > 0) { if(this.stackSize > 0) {
return this.stack[this.stack.length - 1]; return this.stack[this.stack.length - 1];
} }
} }
get stackSize() { get stackSize() {
return this.stack.length; return this.stack.length;
} }
get currentModule() { get currentModule() {
const top = this.top(); const top = this.top();
if(top) { if(top) {
return top.instance; return top.instance;
} }
} }
next(cb) { next(cb) {
const currentModuleInfo = this.top(); const currentModuleInfo = this.top();
assert(currentModuleInfo, 'Empty menu stack!'); assert(currentModuleInfo, 'Empty menu stack!');
const menuConfig = currentModuleInfo.instance.menuConfig; const menuConfig = currentModuleInfo.instance.menuConfig;
const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next'); const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next');
if(!nextMenu) { if(!nextMenu) {
return cb(Array.isArray(menuConfig.next) ? return cb(Array.isArray(menuConfig.next) ?
Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH') : Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH') :
Errors.MenuStack('Invalid or missing "next" member in menu config', 'BADNEXT') Errors.MenuStack('Invalid or missing "next" member in menu config', 'BADNEXT')
); );
} }
if(nextMenu === currentModuleInfo.name) { if(nextMenu === currentModuleInfo.name) {
return cb(Errors.MenuStack('Menu config "next" specifies current menu', 'ALREADYTHERE')); return cb(Errors.MenuStack('Menu config "next" specifies current menu', 'ALREADYTHERE'));
} }
this.goto(nextMenu, { }, cb); this.goto(nextMenu, { }, cb);
} }
prev(cb) { prev(cb) {
const menuResult = this.top().instance.getMenuResult(); const menuResult = this.top().instance.getMenuResult();
// :TODO: leave() should really take a cb... // :TODO: leave() should really take a cb...
this.pop().instance.leave(); // leave & remove current this.pop().instance.leave(); // leave & remove current
const previousModuleInfo = this.pop(); // get previous const previousModuleInfo = this.pop(); // get previous
if(previousModuleInfo) { if(previousModuleInfo) {
const opts = { const opts = {
extraArgs : previousModuleInfo.extraArgs, extraArgs : previousModuleInfo.extraArgs,
savedState : previousModuleInfo.savedState, savedState : previousModuleInfo.savedState,
lastMenuResult : menuResult, lastMenuResult : menuResult,
}; };
return this.goto(previousModuleInfo.name, opts, cb); return this.goto(previousModuleInfo.name, opts, cb);
} }
return cb(Errors.MenuStack('No previous menu available', 'NOPREV')); return cb(Errors.MenuStack('No previous menu available', 'NOPREV'));
} }
goto(name, options, cb) { goto(name, options, cb) {
const currentModuleInfo = this.top(); const currentModuleInfo = this.top();
if(!cb && _.isFunction(options)) { if(!cb && _.isFunction(options)) {
cb = options; cb = options;
options = {}; options = {};
} }
options = options || {}; options = options || {};
const self = this; const self = this;
if(currentModuleInfo && name === currentModuleInfo.name) { if(currentModuleInfo && name === currentModuleInfo.name) {
if(cb) { if(cb) {
cb(Errors.MenuStack('Already at supplied menu', 'ALREADYTHERE')); cb(Errors.MenuStack('Already at supplied menu', 'ALREADYTHERE'));
} }
return; return;
} }
const loadOpts = { const loadOpts = {
name : name, name : name,
client : self.client, client : self.client,
}; };
if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) { if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) {
loadOpts.extraArgs = currentModuleInfo.extraArgs; loadOpts.extraArgs = currentModuleInfo.extraArgs;
} else { } else {
loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value'); loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value');
} }
loadOpts.lastMenuResult = options.lastMenuResult; loadOpts.lastMenuResult = options.lastMenuResult;
loadMenu(loadOpts, (err, modInst) => { loadMenu(loadOpts, (err, modInst) => {
if(err) { if(err) {
// :TODO: probably should just require a cb... // :TODO: probably should just require a cb...
const errCb = cb || self.client.defaultHandlerMissingMod(); const errCb = cb || self.client.defaultHandlerMissingMod();
errCb(err); errCb(err);
} else { } else {
self.client.log.debug( { menuName : name }, 'Goto menu module'); self.client.log.debug( { menuName : name }, 'Goto menu module');
// //
// If menuFlags were supplied in menu.hjson, they should win over // If menuFlags were supplied in menu.hjson, they should win over
// anything supplied in code. // anything supplied in code.
// //
let menuFlags; let menuFlags;
if(0 === modInst.menuConfig.options.menuFlags.length) { if(0 === modInst.menuConfig.options.menuFlags.length) {
menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : []; menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : [];
} else { } else {
menuFlags = modInst.menuConfig.options.menuFlags; menuFlags = modInst.menuConfig.options.menuFlags;
// in code we can ask to merge in // in code we can ask to merge in
if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) { if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) {
menuFlags = _.uniq(menuFlags.concat(options.menuFlags)); menuFlags = _.uniq(menuFlags.concat(options.menuFlags));
} }
} }
if(currentModuleInfo) { if(currentModuleInfo) {
// save stack state // save stack state
currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState(); currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState();
currentModuleInfo.instance.leave(); currentModuleInfo.instance.leave();
if(currentModuleInfo.menuFlags.includes('noHistory')) { if(currentModuleInfo.menuFlags.includes('noHistory')) {
this.pop(); this.pop();
} }
if(menuFlags.includes('popParent')) { if(menuFlags.includes('popParent')) {
this.pop().instance.leave(); // leave & remove current this.pop().instance.leave(); // leave & remove current
} }
} }
self.push({ self.push({
name : name, name : name,
instance : modInst, instance : modInst,
extraArgs : loadOpts.extraArgs, extraArgs : loadOpts.extraArgs,
menuFlags : menuFlags, menuFlags : menuFlags,
}); });
// restore previous state if requested // restore previous state if requested
if(options.savedState) { if(options.savedState) {
modInst.restoreSavedState(options.savedState); modInst.restoreSavedState(options.savedState);
} }
const stackEntries = self.stack.map(stackEntry => { const stackEntries = self.stack.map(stackEntry => {
let name = stackEntry.name; let name = stackEntry.name;
if(stackEntry.instance.menuConfig.options.menuFlags.length > 0) { if(stackEntry.instance.menuConfig.options.menuFlags.length > 0) {
name += ` (${stackEntry.instance.menuConfig.options.menuFlags.join(', ')})`; name += ` (${stackEntry.instance.menuConfig.options.menuFlags.join(', ')})`;
} }
return name; return name;
}); });
self.client.log.trace( { stack : stackEntries }, 'Updated menu stack' ); self.client.log.trace( { stack : stackEntries }, 'Updated menu stack' );
modInst.enter(); modInst.enter();
if(cb) { if(cb) {
cb(null); cb(null);
} }
} }
}); });
} }
}; };

View File

@ -19,243 +19,243 @@ exports.handleAction = handleAction;
exports.handleNext = handleNext; exports.handleNext = handleNext;
function getMenuConfig(client, name, cb) { function getMenuConfig(client, name, cb) {
var menuConfig; var menuConfig;
async.waterfall( async.waterfall(
[ [
function locateMenuConfig(callback) { function locateMenuConfig(callback) {
if(_.has(client.currentTheme, [ 'menus', name ])) { if(_.has(client.currentTheme, [ 'menus', name ])) {
menuConfig = client.currentTheme.menus[name]; menuConfig = client.currentTheme.menus[name];
callback(null); callback(null);
} else { } else {
callback(new Error('No menu entry for \'' + name + '\'')); callback(new Error('No menu entry for \'' + name + '\''));
} }
}, },
function locatePromptConfig(callback) { function locatePromptConfig(callback) {
if(_.isString(menuConfig.prompt)) { if(_.isString(menuConfig.prompt)) {
if(_.has(client.currentTheme, [ 'prompts', menuConfig.prompt ])) { if(_.has(client.currentTheme, [ 'prompts', menuConfig.prompt ])) {
menuConfig.promptConfig = client.currentTheme.prompts[menuConfig.prompt]; menuConfig.promptConfig = client.currentTheme.prompts[menuConfig.prompt];
callback(null); callback(null);
} else { } else {
callback(new Error('No prompt entry for \'' + menuConfig.prompt + '\'')); callback(new Error('No prompt entry for \'' + menuConfig.prompt + '\''));
} }
} else { } else {
callback(null); callback(null);
} }
} }
], ],
function complete(err) { function complete(err) {
cb(err, menuConfig); cb(err, menuConfig);
} }
); );
} }
function loadMenu(options, cb) { function loadMenu(options, cb) {
assert(_.isObject(options)); assert(_.isObject(options));
assert(_.isString(options.name)); assert(_.isString(options.name));
assert(_.isObject(options.client)); assert(_.isObject(options.client));
async.waterfall( async.waterfall(
[ [
function getMenuConfiguration(callback) { function getMenuConfiguration(callback) {
getMenuConfig(options.client, options.name, (err, menuConfig) => { getMenuConfig(options.client, options.name, (err, menuConfig) => {
return callback(err, menuConfig); return callback(err, menuConfig);
}); });
}, },
function loadMenuModule(menuConfig, callback) { function loadMenuModule(menuConfig, callback) {
menuConfig.options = menuConfig.options || {}; menuConfig.options = menuConfig.options || {};
menuConfig.options.menuFlags = menuConfig.options.menuFlags || []; menuConfig.options.menuFlags = menuConfig.options.menuFlags || [];
if(!Array.isArray(menuConfig.options.menuFlags)) { if(!Array.isArray(menuConfig.options.menuFlags)) {
menuConfig.options.menuFlags = [ menuConfig.options.menuFlags ]; menuConfig.options.menuFlags = [ menuConfig.options.menuFlags ];
} }
const modAsset = asset.getModuleAsset(menuConfig.module); const modAsset = asset.getModuleAsset(menuConfig.module);
const modSupplied = null !== modAsset; const modSupplied = null !== modAsset;
const modLoadOpts = { const modLoadOpts = {
name : modSupplied ? modAsset.asset : 'standard_menu', name : modSupplied ? modAsset.asset : 'standard_menu',
path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config().paths.mods, path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config().paths.mods,
category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods', category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods',
}; };
moduleUtil.loadModuleEx(modLoadOpts, (err, mod) => { moduleUtil.loadModuleEx(modLoadOpts, (err, mod) => {
const modData = { const modData = {
name : modLoadOpts.name, name : modLoadOpts.name,
config : menuConfig, config : menuConfig,
mod : mod, mod : mod,
}; };
return callback(err, modData); return callback(err, modData);
}); });
}, },
function createModuleInstance(modData, callback) { function createModuleInstance(modData, callback) {
Log.trace( Log.trace(
{ moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo }, { moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo },
'Creating menu module instance'); 'Creating menu module instance');
let moduleInstance; let moduleInstance;
try { try {
moduleInstance = new modData.mod.getModule({ moduleInstance = new modData.mod.getModule({
menuName : options.name, menuName : options.name,
menuConfig : modData.config, menuConfig : modData.config,
extraArgs : options.extraArgs, extraArgs : options.extraArgs,
client : options.client, client : options.client,
lastMenuResult : options.lastMenuResult, lastMenuResult : options.lastMenuResult,
}); });
} catch(e) { } catch(e) {
return callback(e); return callback(e);
} }
return callback(null, moduleInstance); return callback(null, moduleInstance);
} }
], ],
(err, modInst) => { (err, modInst) => {
return cb(err, modInst); return cb(err, modInst);
} }
); );
} }
function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) {
assert(_.isObject(menuConfig)); assert(_.isObject(menuConfig));
if(!_.isObject(menuConfig.form)) { if(!_.isObject(menuConfig.form)) {
cb(new Error('Invalid or missing \'form\' member for menu')); cb(new Error('Invalid or missing \'form\' member for menu'));
return; return;
} }
if(!_.isObject(menuConfig.form[formId])) { if(!_.isObject(menuConfig.form[formId])) {
cb(new Error('No form found for formId ' + formId)); cb(new Error('No form found for formId ' + formId));
return; return;
} }
const formForId = menuConfig.form[formId]; const formForId = menuConfig.form[formId];
const mciReqKey = _.filter(_.map(_.sortBy(mciMap, 'code'), 'code'), (mci) => { const mciReqKey = _.filter(_.map(_.sortBy(mciMap, 'code'), 'code'), (mci) => {
return MCIViewFactory.UserViewCodes.indexOf(mci) > -1; return MCIViewFactory.UserViewCodes.indexOf(mci) > -1;
}).join(''); }).join('');
Log.trace( { mciKey : mciReqKey }, 'Looking for MCI configuration key'); Log.trace( { mciKey : mciReqKey }, 'Looking for MCI configuration key');
// //
// Exact, explicit match? // Exact, explicit match?
// //
if(_.isObject(formForId[mciReqKey])) { if(_.isObject(formForId[mciReqKey])) {
Log.trace( { mciKey : mciReqKey }, 'Using exact configuration key match'); Log.trace( { mciKey : mciReqKey }, 'Using exact configuration key match');
cb(null, formForId[mciReqKey]); cb(null, formForId[mciReqKey]);
return; return;
} }
// //
// Generic match // Generic match
// //
if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) { if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) {
Log.trace('Using generic configuration'); Log.trace('Using generic configuration');
return cb(null, formForId); return cb(null, formForId);
} }
cb(new Error('No matching form configuration found for key \'' + mciReqKey + '\'')); cb(new Error('No matching form configuration found for key \'' + mciReqKey + '\''));
} }
// :TODO: Most of this should be moved elsewhere .... DRY... // :TODO: Most of this should be moved elsewhere .... DRY...
function callModuleMenuMethod(client, asset, path, formData, extraArgs, cb) { function callModuleMenuMethod(client, asset, path, formData, extraArgs, cb) {
if('' === paths.extname(path)) { if('' === paths.extname(path)) {
path += '.js'; path += '.js';
} }
try { try {
client.log.trace( client.log.trace(
{ path : path, methodName : asset.asset, formData : formData, extraArgs : extraArgs }, { path : path, methodName : asset.asset, formData : formData, extraArgs : extraArgs },
'Calling menu method'); 'Calling menu method');
const methodMod = require(path); const methodMod = require(path);
return methodMod[asset.asset](client.currentMenuModule, formData || { }, extraArgs, cb); return methodMod[asset.asset](client.currentMenuModule, formData || { }, extraArgs, cb);
} catch(e) { } catch(e) {
client.log.error( { error : e.toString(), methodName : asset.asset }, 'Failed to execute asset method'); client.log.error( { error : e.toString(), methodName : asset.asset }, 'Failed to execute asset method');
return cb(e); return cb(e);
} }
} }
function handleAction(client, formData, conf, cb) { function handleAction(client, formData, conf, cb) {
assert(_.isObject(conf)); assert(_.isObject(conf));
assert(_.isString(conf.action)); assert(_.isString(conf.action));
const actionAsset = asset.parseAsset(conf.action); const actionAsset = asset.parseAsset(conf.action);
assert(_.isObject(actionAsset)); assert(_.isObject(actionAsset));
switch(actionAsset.type) { switch(actionAsset.type) {
case 'method' : case 'method' :
case 'systemMethod' : case 'systemMethod' :
if(_.isString(actionAsset.location)) { if(_.isString(actionAsset.location)) {
return callModuleMenuMethod( return callModuleMenuMethod(
client, client,
actionAsset, actionAsset,
paths.join(Config().paths.mods, actionAsset.location), paths.join(Config().paths.mods, actionAsset.location),
formData, formData,
conf.extraArgs, conf.extraArgs,
cb); cb);
} else if('systemMethod' === actionAsset.type) { } else if('systemMethod' === actionAsset.type) {
// :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. () // :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. ()
// :TODO: Probably better as system_method.js // :TODO: Probably better as system_method.js
return callModuleMenuMethod( return callModuleMenuMethod(
client, client,
actionAsset, actionAsset,
paths.join(__dirname, 'system_menu_method.js'), paths.join(__dirname, 'system_menu_method.js'),
formData, formData,
conf.extraArgs, conf.extraArgs,
cb); cb);
} else { } else {
// local to current module // local to current module
const currentModule = client.currentMenuModule; const currentModule = client.currentMenuModule;
if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) { if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) {
return currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs, cb); return currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs, cb);
} }
const err = new Error('Method does not exist'); const err = new Error('Method does not exist');
client.log.warn( { method : actionAsset.asset }, err.message); client.log.warn( { method : actionAsset.asset }, err.message);
return cb(err); return cb(err);
} }
case 'menu' : case 'menu' :
return client.currentMenuModule.gotoMenu(actionAsset.asset, { formData : formData, extraArgs : conf.extraArgs }, cb ); return client.currentMenuModule.gotoMenu(actionAsset.asset, { formData : formData, extraArgs : conf.extraArgs }, cb );
} }
} }
function handleNext(client, nextSpec, conf, cb) { function handleNext(client, nextSpec, conf, cb) {
nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); // handle any conditionals nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); // handle any conditionals
const nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu'); const nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu');
// :TODO: getAssetWithShorthand() can return undefined - handle it! // :TODO: getAssetWithShorthand() can return undefined - handle it!
conf = conf || {}; conf = conf || {};
const extraArgs = conf.extraArgs || {}; const extraArgs = conf.extraArgs || {};
// :TODO: DRY this with handleAction() // :TODO: DRY this with handleAction()
switch(nextAsset.type) { switch(nextAsset.type) {
case 'method' : case 'method' :
case 'systemMethod' : case 'systemMethod' :
if(_.isString(nextAsset.location)) { if(_.isString(nextAsset.location)) {
return callModuleMenuMethod(client, nextAsset, paths.join(Config().paths.mods, nextAsset.location), {}, extraArgs, cb); return callModuleMenuMethod(client, nextAsset, paths.join(Config().paths.mods, nextAsset.location), {}, extraArgs, cb);
} else if('systemMethod' === nextAsset.type) { } else if('systemMethod' === nextAsset.type) {
// :TODO: see other notes about system_menu_method.js here // :TODO: see other notes about system_menu_method.js here
return callModuleMenuMethod(client, nextAsset, paths.join(__dirname, 'system_menu_method.js'), {}, extraArgs, cb); return callModuleMenuMethod(client, nextAsset, paths.join(__dirname, 'system_menu_method.js'), {}, extraArgs, cb);
} else { } else {
// local to current module // local to current module
const currentModule = client.currentMenuModule; const currentModule = client.currentMenuModule;
if(_.isFunction(currentModule.menuMethods[nextAsset.asset])) { if(_.isFunction(currentModule.menuMethods[nextAsset.asset])) {
const formData = {}; // we don't have any const formData = {}; // we don't have any
return currentModule.menuMethods[nextAsset.asset]( formData, extraArgs, cb ); return currentModule.menuMethods[nextAsset.asset]( formData, extraArgs, cb );
} }
const err = new Error('Method does not exist'); const err = new Error('Method does not exist');
client.log.warn( { method : nextAsset.asset }, err.message); client.log.warn( { method : nextAsset.asset }, err.message);
return cb(err); return cb(err);
} }
case 'menu' : case 'menu' :
return client.currentMenuModule.gotoMenu(nextAsset.asset, { extraArgs : extraArgs }, cb ); return client.currentMenuModule.gotoMenu(nextAsset.asset, { extraArgs : extraArgs }, cb );
} }
const err = new Error('Invalid asset type for "next"'); const err = new Error('Invalid asset type for "next"');
client.log.error( { nextSpec : nextSpec }, err.message); client.log.error( { nextSpec : nextSpec }, err.message);
return cb(err); return cb(err);
} }

View File

@ -14,264 +14,264 @@ const _ = require('lodash');
exports.MenuView = MenuView; exports.MenuView = MenuView;
function MenuView(options) { function MenuView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
View.call(this, options); View.call(this, options);
this.disablePipe = options.disablePipe || false; this.disablePipe = options.disablePipe || false;
const self = this; const self = this;
if(options.items) { if(options.items) {
this.setItems(options.items); this.setItems(options.items);
} else { } else {
this.items = []; this.items = [];
} }
this.renderCache = {}; this.renderCache = {};
this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true);
this.setHotKeys(options.hotKeys); this.setHotKeys(options.hotKeys);
this.focusedItemIndex = options.focusedItemIndex || 0; this.focusedItemIndex = options.focusedItemIndex || 0;
this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0;
this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0;
// :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization
this.focusPrefix = options.focusPrefix || ''; this.focusPrefix = options.focusPrefix || '';
this.focusSuffix = options.focusSuffix || ''; this.focusSuffix = options.focusSuffix || '';
this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1);
this.justify = options.justify || 'none'; this.justify = options.justify || 'none';
this.hasFocusItems = function() { this.hasFocusItems = function() {
return !_.isUndefined(self.focusItems); return !_.isUndefined(self.focusItems);
}; };
this.getHotKeyItemIndex = function(ch) { this.getHotKeyItemIndex = function(ch) {
if(ch && self.hotKeys) { if(ch && self.hotKeys) {
const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch];
if(_.isNumber(keyIndex)) { if(_.isNumber(keyIndex)) {
return keyIndex; return keyIndex;
} }
} }
return -1; return -1;
}; };
this.emitIndexUpdate = function() { this.emitIndexUpdate = function() {
self.emit('index update', self.focusedItemIndex); self.emit('index update', self.focusedItemIndex);
} };
} }
util.inherits(MenuView, View); util.inherits(MenuView, View);
MenuView.prototype.setItems = function(items) { MenuView.prototype.setItems = function(items) {
if(Array.isArray(items)) { if(Array.isArray(items)) {
this.sorted = false; this.sorted = false;
this.renderCache = {}; this.renderCache = {};
// //
// Items can be an array of strings or an array of objects. // Items can be an array of strings or an array of objects.
// //
// In the case of objects, items are considered complex and // In the case of objects, items are considered complex and
// may have one or more members that can later be formatted // may have one or more members that can later be formatted
// against. The default member is 'text'. The member 'data' // against. The default member is 'text'. The member 'data'
// may be overridden to provide a form value other than the // may be overridden to provide a form value other than the
// item's index. // item's index.
// //
// Items can be formatted with 'itemFormat' and 'focusItemFormat' // Items can be formatted with 'itemFormat' and 'focusItemFormat'
// //
let text; let text;
let stringItem; let stringItem;
this.items = items.map(item => { this.items = items.map(item => {
stringItem = _.isString(item); stringItem = _.isString(item);
if(stringItem) { if(stringItem) {
text = item; text = item;
} else { } else {
text = item.text || ''; text = item.text || '';
this.complexItems = true; this.complexItems = true;
} }
text = this.disablePipe ? text : pipeToAnsi(text, this.client); text = this.disablePipe ? text : pipeToAnsi(text, this.client);
return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others
}); });
if(this.complexItems) { if(this.complexItems) {
this.itemFormat = this.itemFormat || '{text}'; this.itemFormat = this.itemFormat || '{text}';
} }
} }
}; };
MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) {
const item = this.renderCache[index]; const item = this.renderCache[index];
return item && item[focusItem ? 'focus' : 'standard']; return item && item[focusItem ? 'focus' : 'standard'];
}; };
MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) {
this.renderCache[index] = this.renderCache[index] || {}; this.renderCache[index] = this.renderCache[index] || {};
this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered;
}; };
MenuView.prototype.setSort = function(sort) { MenuView.prototype.setSort = function(sort) {
if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) {
return; return;
} }
const key = true === sort ? 'text' : sort; const key = true === sort ? 'text' : sort;
if('text' !== sort && !this.complexItems) { if('text' !== sort && !this.complexItems) {
return; // need a valid sort key return; // need a valid sort key
} }
this.items.sort( (a, b) => { this.items.sort( (a, b) => {
const a1 = a[key]; const a1 = a[key];
const b1 = b[key]; const b1 = b[key];
if(!a1) { if(!a1) {
return -1; return -1;
} }
if(!b1) { if(!b1) {
return 1; return 1;
} }
return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); return a1.localeCompare( b1, { sensitivity : false, numeric : true } );
}); });
this.sorted = true; this.sorted = true;
}; };
MenuView.prototype.removeItem = function(index) { MenuView.prototype.removeItem = function(index) {
this.sorted = false; this.sorted = false;
this.items.splice(index, 1); this.items.splice(index, 1);
if(this.focusItems) { if(this.focusItems) {
this.focusItems.splice(index, 1); this.focusItems.splice(index, 1);
} }
if(this.focusedItemIndex >= index) { if(this.focusedItemIndex >= index) {
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0);
} }
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
MenuView.prototype.getCount = function() { MenuView.prototype.getCount = function() {
return this.items.length; return this.items.length;
}; };
MenuView.prototype.getItems = function() { MenuView.prototype.getItems = function() {
if(this.complexItems) { if(this.complexItems) {
return this.items; return this.items;
} }
return this.items.map( item => { return this.items.map( item => {
return item.text; return item.text;
}); });
}; };
MenuView.prototype.getItem = function(index) { MenuView.prototype.getItem = function(index) {
if(this.complexItems) { if(this.complexItems) {
return this.items[index]; return this.items[index];
} }
return this.items[index].text; return this.items[index].text;
}; };
MenuView.prototype.focusNext = function() { MenuView.prototype.focusNext = function() {
this.emitIndexUpdate(); this.emitIndexUpdate();
}; };
MenuView.prototype.focusPrevious = function() { MenuView.prototype.focusPrevious = function() {
this.emitIndexUpdate(); this.emitIndexUpdate();
}; };
MenuView.prototype.focusNextPageItem = function() { MenuView.prototype.focusNextPageItem = function() {
this.emitIndexUpdate(); this.emitIndexUpdate();
}; };
MenuView.prototype.focusPreviousPageItem = function() { MenuView.prototype.focusPreviousPageItem = function() {
this.emitIndexUpdate(); this.emitIndexUpdate();
}; };
MenuView.prototype.focusFirst = function() { MenuView.prototype.focusFirst = function() {
this.emitIndexUpdate(); this.emitIndexUpdate();
}; };
MenuView.prototype.focusLast = function() { MenuView.prototype.focusLast = function() {
this.emitIndexUpdate(); this.emitIndexUpdate();
}; };
MenuView.prototype.setFocusItemIndex = function(index) { MenuView.prototype.setFocusItemIndex = function(index) {
this.focusedItemIndex = index; this.focusedItemIndex = index;
}; };
MenuView.prototype.onKeyPress = function(ch, key) { MenuView.prototype.onKeyPress = function(ch, key) {
const itemIndex = this.getHotKeyItemIndex(ch); const itemIndex = this.getHotKeyItemIndex(ch);
if(itemIndex >= 0) { if(itemIndex >= 0) {
this.setFocusItemIndex(itemIndex); this.setFocusItemIndex(itemIndex);
if(true === this.hotKeySubmit) { if(true === this.hotKeySubmit) {
this.emit('action', 'accept'); this.emit('action', 'accept');
} }
} }
MenuView.super_.prototype.onKeyPress.call(this, ch, key); MenuView.super_.prototype.onKeyPress.call(this, ch, key);
}; };
MenuView.prototype.setFocusItems = function(items) { MenuView.prototype.setFocusItems = function(items) {
const self = this; const self = this;
if(items) { if(items) {
this.focusItems = []; this.focusItems = [];
items.forEach( itemText => { items.forEach( itemText => {
this.focusItems.push( this.focusItems.push(
{ {
text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client)
} }
); );
}); });
} }
}; };
MenuView.prototype.setItemSpacing = function(itemSpacing) { MenuView.prototype.setItemSpacing = function(itemSpacing) {
itemSpacing = parseInt(itemSpacing); itemSpacing = parseInt(itemSpacing);
assert(_.isNumber(itemSpacing)); assert(_.isNumber(itemSpacing));
this.itemSpacing = itemSpacing; this.itemSpacing = itemSpacing;
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
MenuView.prototype.setPropertyValue = function(propName, value) { MenuView.prototype.setPropertyValue = function(propName, value) {
switch(propName) { switch(propName) {
case 'itemSpacing' : this.setItemSpacing(value); break; case 'itemSpacing' : this.setItemSpacing(value); break;
case 'items' : this.setItems(value); break; case 'items' : this.setItems(value); break;
case 'focusItems' : this.setFocusItems(value); break; case 'focusItems' : this.setFocusItems(value); break;
case 'hotKeys' : this.setHotKeys(value); break; case 'hotKeys' : this.setHotKeys(value); break;
case 'hotKeySubmit' : this.hotKeySubmit = value; break; case 'hotKeySubmit' : this.hotKeySubmit = value; break;
case 'justify' : this.justify = value; break; case 'justify' : this.justify = value; break;
case 'focusItemIndex' : this.focusedItemIndex = value; break; case 'focusItemIndex' : this.focusedItemIndex = value; break;
case 'itemFormat' : case 'itemFormat' :
case 'focusItemFormat' : case 'focusItemFormat' :
this[propName] = value; this[propName] = value;
break; break;
case 'sort' : this.setSort(value); break; case 'sort' : this.setSort(value); break;
} }
MenuView.super_.prototype.setPropertyValue.call(this, propName, value); MenuView.super_.prototype.setPropertyValue.call(this, propName, value);
}; };
MenuView.prototype.setHotKeys = function(hotKeys) { MenuView.prototype.setHotKeys = function(hotKeys) {
if(_.isObject(hotKeys)) { if(_.isObject(hotKeys)) {
if(this.caseInsensitiveHotKeys) { if(this.caseInsensitiveHotKeys) {
this.hotKeys = {}; this.hotKeys = {};
for(var key in hotKeys) { for(var key in hotKeys) {
this.hotKeys[key.toLowerCase()] = hotKeys[key]; this.hotKeys[key.toLowerCase()] = hotKeys[key];
} }
} else { } else {
this.hotKeys = hotKeys; this.hotKeys = hotKeys;
} }
} }
}; };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,9 @@
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const { const {
getSortedAvailMessageConferences, getSortedAvailMessageConferences,
getAvailableMessageAreasByConfTag, getAvailableMessageAreasByConfTag,
getSortedAvailMessageAreasByConfTag, getSortedAvailMessageAreasByConfTag,
} = require('./message_area.js'); } = require('./message_area.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const Message = require('./message.js'); const Message = require('./message.js');
@ -15,134 +15,134 @@ const Message = require('./message.js');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Base Search', name : 'Message Base Search',
desc : 'Module for quickly searching the message base', desc : 'Module for quickly searching the message base',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
search : { search : {
searchTerms : 1, searchTerms : 1,
search : 2, search : 2,
conf : 3, conf : 3,
area : 4, area : 4,
to : 5, to : 5,
from : 6, from : 6,
advSearch : 7, advSearch : 7,
} }
}; };
exports.getModule = class MessageBaseSearch extends MenuModule { exports.getModule = class MessageBaseSearch extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.menuMethods = { this.menuMethods = {
search : (formData, extraArgs, cb) => { search : (formData, extraArgs, cb) => {
return this.searchNow(formData, cb); return this.searchNow(formData, cb);
} }
}; };
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
this.prepViewController('search', 0, mciData.menu, (err, vc) => { this.prepViewController('search', 0, mciData.menu, (err, vc) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const confView = vc.getView(MciViewIds.search.conf); const confView = vc.getView(MciViewIds.search.conf);
const areaView = vc.getView(MciViewIds.search.area); const areaView = vc.getView(MciViewIds.search.area);
if(!confView || !areaView) { if(!confView || !areaView) {
return cb(Errors.DoesNotExist('Missing one or more required views')); return cb(Errors.DoesNotExist('Missing one or more required views'));
} }
const availConfs = [ { text : '-ALL-', data : '' } ].concat( const availConfs = [ { text : '-ALL-', data : '' } ].concat(
getSortedAvailMessageConferences(this.client).map(conf => Object.assign(conf, { text : conf.conf.name, data : conf.confTag } )) || [] getSortedAvailMessageConferences(this.client).map(conf => Object.assign(conf, { text : conf.conf.name, data : conf.confTag } )) || []
); );
let availAreas = [ { text : '-ALL-', data : '' } ]; // note: will populate if conf changes from ALL let availAreas = [ { text : '-ALL-', data : '' } ]; // note: will populate if conf changes from ALL
confView.setItems(availConfs); confView.setItems(availConfs);
areaView.setItems(availAreas); areaView.setItems(availAreas);
confView.setFocusItemIndex(0); confView.setFocusItemIndex(0);
areaView.setFocusItemIndex(0); areaView.setFocusItemIndex(0);
confView.on('index update', idx => { confView.on('index update', idx => {
availAreas = [ { text : '-ALL-', data : '' } ].concat( availAreas = [ { text : '-ALL-', data : '' } ].concat(
getSortedAvailMessageAreasByConfTag(availConfs[idx].confTag, { client : this.client }).map( getSortedAvailMessageAreasByConfTag(availConfs[idx].confTag, { client : this.client }).map(
area => Object.assign(area, { text : area.area.name, data : area.areaTag } ) area => Object.assign(area, { text : area.area.name, data : area.areaTag } )
) )
); );
areaView.setItems(availAreas); areaView.setItems(availAreas);
areaView.setFocusItemIndex(0); areaView.setFocusItemIndex(0);
}); });
vc.switchFocus(MciViewIds.search.searchTerms); vc.switchFocus(MciViewIds.search.searchTerms);
return cb(null); return cb(null);
}); });
}); });
} }
searchNow(formData, cb) { searchNow(formData, cb) {
const isAdvanced = formData.submitId === MciViewIds.search.advSearch; const isAdvanced = formData.submitId === MciViewIds.search.advSearch;
const value = formData.value; const value = formData.value;
const filter = { const filter = {
resultType : 'messageList', resultType : 'messageList',
sort : 'modTimestamp', sort : 'modTimestamp',
terms : value.searchTerms, terms : value.searchTerms,
//extraFields : [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], //extraFields : [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ],
limit : 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned limit : 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned
}; };
if(isAdvanced) { if(isAdvanced) {
filter.toUserName = value.toUserName; filter.toUserName = value.toUserName;
filter.fromUserName = value.fromUserName; filter.fromUserName = value.fromUserName;
if(value.confTag && !value.areaTag) { if(value.confTag && !value.areaTag) {
// areaTag may be a string or array of strings // areaTag may be a string or array of strings
// getAvailableMessageAreasByConfTag() returns a obj - we only need tags // getAvailableMessageAreasByConfTag() returns a obj - we only need tags
filter.areaTag = _.map( filter.areaTag = _.map(
getAvailableMessageAreasByConfTag(value.confTag, { client : this.client } ), getAvailableMessageAreasByConfTag(value.confTag, { client : this.client } ),
(area, areaTag) => areaTag (area, areaTag) => areaTag
); );
} else if(value.areaTag) { } else if(value.areaTag) {
filter.areaTag = value.areaTag; // specific conf + area filter.areaTag = value.areaTag; // specific conf + area
} }
} }
Message.findMessages(filter, (err, messageList) => { Message.findMessages(filter, (err, messageList) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
if(0 === messageList.length) { if(0 === messageList.length) {
return this.gotoMenu( return this.gotoMenu(
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
{ menuFlags : [ 'popParent' ] }, { menuFlags : [ 'popParent' ] },
cb cb
); );
} }
const menuOpts = { const menuOpts = {
extraArgs : { extraArgs : {
messageList, messageList,
noUpdateLastReadId : true noUpdateLastReadId : true
}, },
menuFlags : [ 'popParent' ], menuFlags : [ 'popParent' ],
}; };
return this.gotoMenu( return this.gotoMenu(
this.menuConfig.config.messageListMenu || 'messageAreaMessageList', this.menuConfig.config.messageListMenu || 'messageAreaMessageList',
menuOpts, menuOpts,
cb cb
); );
}); });
} }
}; };

View File

@ -10,33 +10,33 @@ exports.startup = startup;
exports.resolveMimeType = resolveMimeType; exports.resolveMimeType = resolveMimeType;
function startup(cb) { function startup(cb) {
// //
// Add in types (not yet) supported by mime-db -- and therefor, mime-types // Add in types (not yet) supported by mime-db -- and therefor, mime-types
// //
const ADDITIONAL_EXT_MIMETYPES = { const ADDITIONAL_EXT_MIMETYPES = {
ans : 'text/x-ansi', ans : 'text/x-ansi',
gz : 'application/gzip', // not in mime-types 2.1.15 :( gz : 'application/gzip', // not in mime-types 2.1.15 :(
lzx : 'application/x-lzx', // :TODO: submit to mime-types lzx : 'application/x-lzx', // :TODO: submit to mime-types
}; };
_.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => { _.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => {
// don't override any entries // don't override any entries
if(!_.isString(mimeTypes.types[ext])) { if(!_.isString(mimeTypes.types[ext])) {
mimeTypes[ext] = mimeType; mimeTypes[ext] = mimeType;
} }
if(!mimeTypes.extensions[mimeType]) { if(!mimeTypes.extensions[mimeType]) {
mimeTypes.extensions[mimeType] = [ ext ]; mimeTypes.extensions[mimeType] = [ ext ];
} }
}); });
return cb(null); return cb(null);
} }
function resolveMimeType(query) { function resolveMimeType(query) {
if(mimeTypes.extensions[query]) { if(mimeTypes.extensions[query]) {
return query; // alreaed a mime-type return query; // alreaed a mime-type
} }
return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined
} }

View File

@ -14,39 +14,39 @@ exports.getCleanEnigmaVersion = getCleanEnigmaVersion;
exports.getEnigmaUserAgent = getEnigmaUserAgent; exports.getEnigmaUserAgent = getEnigmaUserAgent;
function isProduction() { function isProduction() {
var env = process.env.NODE_ENV || 'dev'; var env = process.env.NODE_ENV || 'dev';
return 'production' === env; return 'production' === env;
} }
function isDevelopment() { function isDevelopment() {
return (!(isProduction())); return (!(isProduction()));
} }
function valueWithDefault(val, defVal) { function valueWithDefault(val, defVal) {
return (typeof val !== 'undefined' ? val : defVal); return (typeof val !== 'undefined' ? val : defVal);
} }
function resolvePath(path) { function resolvePath(path) {
if(path.substr(0, 2) === '~/') { if(path.substr(0, 2) === '~/') {
var mswCombined = process.env.HOMEDRIVE + process.env.HOMEPATH; var mswCombined = process.env.HOMEDRIVE + process.env.HOMEPATH;
path = (process.env.HOME || mswCombined || process.env.HOMEPATH || process.env.HOMEDIR || process.cwd()) + path.substr(1); path = (process.env.HOME || mswCombined || process.env.HOMEPATH || process.env.HOMEDIR || process.cwd()) + path.substr(1);
} }
return paths.resolve(path); return paths.resolve(path);
} }
function getCleanEnigmaVersion() { function getCleanEnigmaVersion() {
return packageJson.version return packageJson.version
.replace(/-/g, '.') .replace(/-/g, '.')
.replace(/alpha/,'a') .replace(/alpha/,'a')
.replace(/beta/,'b') .replace(/beta/,'b')
; ;
} }
// See also ftn_util.js getTearLine() & getProductIdentifier() // See also ftn_util.js getTearLine() & getProductIdentifier()
function getEnigmaUserAgent() { function getEnigmaUserAgent() {
// can't have 1/2 or ½ in User-Agent according to RFC 1945 :( // can't have 1/2 or ½ in User-Agent according to RFC 1945 :(
const version = getCleanEnigmaVersion(); const version = getCleanEnigmaVersion();
const nodeVer = process.version.substr(1); // remove 'v' prefix const nodeVer = process.version.substr(1); // remove 'v' prefix
return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
} }

View File

@ -7,28 +7,28 @@ const { get } = require('lodash');
exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { exports.MessageAreaConfTempSwitcher = Sup => class extends Sup {
tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) { tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) {
messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag); messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag);
if(!messageAreaTag) { if(!messageAreaTag) {
return; // nothing to do! return; // nothing to do!
} }
if(recordPrevious) { if(recordPrevious) {
this.prevMessageConfAndArea = { this.prevMessageConfAndArea = {
confTag : this.client.user.properties.message_conf_tag, confTag : this.client.user.properties.message_conf_tag,
areaTag : this.client.user.properties.message_area_tag, areaTag : this.client.user.properties.message_area_tag,
}; };
} }
if(!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) { if(!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) {
this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch'); this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch');
} }
} }
tempMessageConfAndAreaRestore() { tempMessageConfAndAreaRestore() {
if(this.prevMessageConfAndArea) { if(this.prevMessageConfAndArea) {
this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag; this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag;
this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag; this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag;
} }
} }
}; };

View File

@ -18,93 +18,93 @@ exports.loadModulesForCategory = loadModulesForCategory;
exports.getModulePaths = getModulePaths; exports.getModulePaths = getModulePaths;
function loadModuleEx(options, cb) { function loadModuleEx(options, cb) {
assert(_.isObject(options)); assert(_.isObject(options));
assert(_.isString(options.name)); assert(_.isString(options.name));
assert(_.isString(options.path)); assert(_.isString(options.path));
const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null; const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null;
if(_.isObject(modConfig) && false === modConfig.enabled) { if(_.isObject(modConfig) && false === modConfig.enabled) {
const err = new Error(`Module "${options.name}" is disabled`); const err = new Error(`Module "${options.name}" is disabled`);
err.code = 'EENIGMODDISABLED'; err.code = 'EENIGMODDISABLED';
return cb(err); return cb(err);
} }
// //
// Modules are allowed to live in /path/to/<moduleName>/<moduleName>.js or // Modules are allowed to live in /path/to/<moduleName>/<moduleName>.js or
// simply in /path/to/<moduleName>.js. This allows for more advanced modules // simply in /path/to/<moduleName>.js. This allows for more advanced modules
// to have their own containing folder, package.json & dependencies, etc. // to have their own containing folder, package.json & dependencies, etc.
// //
let mod; let mod;
let modPath = paths.join(options.path, `${options.name}.js`); // general case first let modPath = paths.join(options.path, `${options.name}.js`); // general case first
try { try {
mod = require(modPath); mod = require(modPath);
} catch(e) { } catch(e) {
if('MODULE_NOT_FOUND' === e.code) { if('MODULE_NOT_FOUND' === e.code) {
modPath = paths.join(options.path, options.name, `${options.name}.js`); modPath = paths.join(options.path, options.name, `${options.name}.js`);
try { try {
mod = require(modPath); mod = require(modPath);
} catch(e) { } catch(e) {
return cb(e); return cb(e);
} }
} else { } else {
return cb(e); return cb(e);
} }
} }
if(!_.isObject(mod.moduleInfo)) { if(!_.isObject(mod.moduleInfo)) {
return cb(new Error('Module is missing "moduleInfo" section')); return cb(new Error('Module is missing "moduleInfo" section'));
} }
if(!_.isFunction(mod.getModule)) { if(!_.isFunction(mod.getModule)) {
return cb(new Error('Invalid or missing "getModule" method for module!')); return cb(new Error('Invalid or missing "getModule" method for module!'));
} }
return cb(null, mod); return cb(null, mod);
} }
function loadModule(name, category, cb) { function loadModule(name, category, cb) {
const path = Config().paths[category]; const path = Config().paths[category];
if(!_.isString(path)) { if(!_.isString(path)) {
return cb(new Error(`Not sure where to look for "${name}" of category "${category}"`)); return cb(new Error(`Not sure where to look for "${name}" of category "${category}"`));
} }
loadModuleEx( { name : name, path : path, category : category }, function loaded(err, mod) { loadModuleEx( { name : name, path : path, category : category }, function loaded(err, mod) {
return cb(err, mod); return cb(err, mod);
}); });
} }
function loadModulesForCategory(category, iterator, complete) { function loadModulesForCategory(category, iterator, complete) {
fs.readdir(Config().paths[category], (err, files) => { fs.readdir(Config().paths[category], (err, files) => {
if(err) { if(err) {
return iterator(err); return iterator(err);
} }
const jsModules = files.filter(file => { const jsModules = files.filter(file => {
return '.js' === paths.extname(file); return '.js' === paths.extname(file);
}); });
async.each(jsModules, (file, next) => { async.each(jsModules, (file, next) => {
loadModule(paths.basename(file, '.js'), category, (err, mod) => { loadModule(paths.basename(file, '.js'), category, (err, mod) => {
iterator(err, mod); iterator(err, mod);
return next(); return next();
}); });
}, err => { }, err => {
if(complete) { if(complete) {
return complete(err); return complete(err);
} }
}); });
}); });
} }
function getModulePaths() { function getModulePaths() {
const config = Config(); const config = Config();
return [ return [
config.paths.mods, config.paths.mods,
config.paths.loginServers, config.paths.loginServers,
config.paths.contentServers, config.paths.contentServers,
config.paths.scannerTossers, config.paths.scannerTossers,
]; ];
} }

View File

@ -15,9 +15,9 @@ const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Area List', name : 'Message Area List',
desc : 'Module for listing / choosing message areas', desc : 'Module for listing / choosing message areas',
author : 'NuSkooler', author : 'NuSkooler',
}; };
/* /*
@ -35,73 +35,73 @@ exports.moduleInfo = {
*/ */
const MciViewIds = { const MciViewIds = {
AreaList : 1, AreaList : 1,
SelAreaInfo1 : 2, SelAreaInfo1 : 2,
SelAreaInfo2 : 3, SelAreaInfo2 : 3,
}; };
exports.getModule = class MessageAreaListModule extends MenuModule { exports.getModule = class MessageAreaListModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag( this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag(
this.client.user.properties.message_conf_tag, this.client.user.properties.message_conf_tag,
{ client : this.client } { client : this.client }
); );
const self = this; const self = this;
this.menuMethods = { this.menuMethods = {
changeArea : function(formData, extraArgs, cb) { changeArea : function(formData, extraArgs, cb) {
if(1 === formData.submitId) { if(1 === formData.submitId) {
let area = self.messageAreas[formData.value.area]; let area = self.messageAreas[formData.value.area];
const areaTag = area.areaTag; const areaTag = area.areaTag;
area = area.area; // what we want is actually embedded area = area.area; // what we want is actually embedded
messageArea.changeMessageArea(self.client, areaTag, err => { messageArea.changeMessageArea(self.client, areaTag, err => {
if(err) { if(err) {
self.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`); self.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`);
self.prevMenuOnTimeout(1000, cb); self.prevMenuOnTimeout(1000, cb);
} else { } else {
if(_.isString(area.art)) { if(_.isString(area.art)) {
const dispOptions = { const dispOptions = {
client : self.client, client : self.client,
name : area.art, name : area.art,
}; };
self.client.term.rawWrite(resetScreen()); self.client.term.rawWrite(resetScreen());
displayThemeArt(dispOptions, () => { displayThemeArt(dispOptions, () => {
// pause by default, unless explicitly told not to // pause by default, unless explicitly told not to
if(_.has(area, 'options.pause') && false === area.options.pause) { if(_.has(area, 'options.pause') && false === area.options.pause) {
return self.prevMenuOnTimeout(1000, cb); return self.prevMenuOnTimeout(1000, cb);
} else { } else {
self.pausePrompt( () => { self.pausePrompt( () => {
return self.prevMenu(cb); return self.prevMenu(cb);
}); });
} }
}); });
} else { } else {
return self.prevMenu(cb); return self.prevMenu(cb);
} }
} }
}); });
} else { } else {
return cb(null); return cb(null);
} }
} }
}; };
} }
prevMenuOnTimeout(timeout, cb) { prevMenuOnTimeout(timeout, cb) {
setTimeout( () => { setTimeout( () => {
return this.prevMenu(cb); return this.prevMenu(cb);
}, timeout); }, timeout);
} }
// :TODO: these concepts have been replaced with the {someKey} style formatting - update me! // :TODO: these concepts have been replaced with the {someKey} style formatting - update me!
updateGeneralAreaInfoViews(areaIndex) { updateGeneralAreaInfoViews(areaIndex) {
/* /*
const areaInfo = self.messageAreas[areaIndex]; const areaInfo = self.messageAreas[areaIndex];
[ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => { [ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => {
@ -111,71 +111,71 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
} }
}); });
*/ */
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu, mciMap : mciData.menu,
formId : 0, formId : 0,
}; };
vc.loadFromMenuConfig(loadOpts, function startingViewReady(err) { vc.loadFromMenuConfig(loadOpts, function startingViewReady(err) {
callback(err); callback(err);
}); });
}, },
function populateAreaListView(callback) { function populateAreaListView(callback) {
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
const areaListView = vc.getView(MciViewIds.AreaList); const areaListView = vc.getView(MciViewIds.AreaList);
if(!areaListView) { if(!areaListView) {
return callback(Errors.MissingMci('A MenuView compatible MCI code is required')); return callback(Errors.MissingMci('A MenuView compatible MCI code is required'));
} }
let i = 1; let i = 1;
areaListView.setItems(_.map(self.messageAreas, v => { areaListView.setItems(_.map(self.messageAreas, v => {
return stringFormat(listFormat, { return stringFormat(listFormat, {
index : i++, index : i++,
areaTag : v.area.areaTag, areaTag : v.area.areaTag,
name : v.area.name, name : v.area.name,
desc : v.area.desc, desc : v.area.desc,
}); });
})); }));
i = 1; i = 1;
areaListView.setFocusItems(_.map(self.messageAreas, v => { areaListView.setFocusItems(_.map(self.messageAreas, v => {
return stringFormat(focusListFormat, { return stringFormat(focusListFormat, {
index : i++, index : i++,
areaTag : v.area.areaTag, areaTag : v.area.areaTag,
name : v.area.name, name : v.area.name,
desc : v.area.desc, desc : v.area.desc,
}); });
})); }));
areaListView.on('index update', areaIndex => { areaListView.on('index update', areaIndex => {
self.updateGeneralAreaInfoViews(areaIndex); self.updateGeneralAreaInfoViews(areaIndex);
}); });
areaListView.redraw(); areaListView.redraw();
callback(null); callback(null);
} }
], ],
function complete(err) { function complete(err) {
return cb(err); return cb(err);
} }
); );
}); });
} }
}; };

View File

@ -8,60 +8,60 @@ const _ = require('lodash');
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Area Post', name : 'Message Area Post',
desc : 'Module for posting a new message to an area', desc : 'Module for posting a new message to an area',
author : 'NuSkooler', author : 'NuSkooler',
}; };
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
constructor(options) { constructor(options) {
super(options); super(options);
const self = this; const self = this;
// we're posting, so always start with 'edit' mode // we're posting, so always start with 'edit' mode
this.editorMode = 'edit'; this.editorMode = 'edit';
this.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) { this.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) {
var msg; var msg;
async.series( async.series(
[ [
function getMessageObject(callback) { function getMessageObject(callback) {
self.getMessage(function gotMsg(err, msgObj) { self.getMessage(function gotMsg(err, msgObj) {
msg = msgObj; msg = msgObj;
return callback(err); return callback(err);
}); });
}, },
function saveMessage(callback) { function saveMessage(callback) {
return persistMessage(msg, callback); return persistMessage(msg, callback);
}, },
function updateStats(callback) { function updateStats(callback) {
self.updateUserStats(callback); self.updateUserStats(callback);
} }
], ],
function complete(err) { function complete(err) {
if(err) { if(err) {
// :TODO:... sooooo now what? // :TODO:... sooooo now what?
} else { } else {
// note: not logging 'from' here as it's part of client.log.xxxx() // note: not logging 'from' here as it's part of client.log.xxxx()
self.client.log.info( self.client.log.info(
{ to : msg.toUserName, subject : msg.subject, uuid : msg.uuid }, { to : msg.toUserName, subject : msg.subject, uuid : msg.uuid },
'Message persisted' 'Message persisted'
); );
} }
return self.nextMenu(cb); return self.nextMenu(cb);
} }
); );
}; };
} }
enter() { enter() {
if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) { if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) {
this.messageAreaTag = this.client.user.properties.message_area_tag; this.messageAreaTag = this.client.user.properties.message_area_tag;
} }
super.enter(); super.enter();
} }
}; };

View File

@ -6,13 +6,13 @@ var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
exports.getModule = AreaReplyFSEModule; exports.getModule = AreaReplyFSEModule;
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Area Reply', name : 'Message Area Reply',
desc : 'Module for replying to an area message', desc : 'Module for replying to an area message',
author : 'NuSkooler', author : 'NuSkooler',
}; };
function AreaReplyFSEModule(options) { function AreaReplyFSEModule(options) {
FullScreenEditorModule.call(this, options); FullScreenEditorModule.call(this, options);
} }
require('util').inherits(AreaReplyFSEModule, FullScreenEditorModule); require('util').inherits(AreaReplyFSEModule, FullScreenEditorModule);

View File

@ -9,137 +9,137 @@ const Message = require('./message.js');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Area View', name : 'Message Area View',
desc : 'Module for viewing an area message', desc : 'Module for viewing an area message',
author : 'NuSkooler', author : 'NuSkooler',
}; };
exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.editorType = 'area'; this.editorType = 'area';
this.editorMode = 'view'; this.editorMode = 'view';
if(_.isObject(options.extraArgs)) { if(_.isObject(options.extraArgs)) {
this.messageList = options.extraArgs.messageList; this.messageList = options.extraArgs.messageList;
this.messageIndex = options.extraArgs.messageIndex; this.messageIndex = options.extraArgs.messageIndex;
this.lastMessageNextExit = options.extraArgs.lastMessageNextExit; this.lastMessageNextExit = options.extraArgs.lastMessageNextExit;
} }
this.messageList = this.messageList || []; this.messageList = this.messageList || [];
this.messageIndex = this.messageIndex || 0; this.messageIndex = this.messageIndex || 0;
this.messageTotal = this.messageList.length; this.messageTotal = this.messageList.length;
if(this.messageList.length > 0) { if(this.messageList.length > 0) {
this.messageAreaTag = this.messageList[this.messageIndex].areaTag; this.messageAreaTag = this.messageList[this.messageIndex].areaTag;
} }
const self = this; const self = this;
// assign *additional* menuMethods // assign *additional* menuMethods
Object.assign(this.menuMethods, { Object.assign(this.menuMethods, {
nextMessage : (formData, extraArgs, cb) => { nextMessage : (formData, extraArgs, cb) => {
if(self.messageIndex + 1 < self.messageList.length) { if(self.messageIndex + 1 < self.messageList.length) {
self.messageIndex++; self.messageIndex++;
this.messageAreaTag = this.messageList[this.messageIndex].areaTag; this.messageAreaTag = this.messageList[this.messageIndex].areaTag;
this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with
return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb);
} }
// auto-exit if no more to go? // auto-exit if no more to go?
if(self.lastMessageNextExit) { if(self.lastMessageNextExit) {
self.lastMessageReached = true; self.lastMessageReached = true;
return self.prevMenu(cb); return self.prevMenu(cb);
} }
return cb(null); return cb(null);
}, },
prevMessage : (formData, extraArgs, cb) => { prevMessage : (formData, extraArgs, cb) => {
if(self.messageIndex > 0) { if(self.messageIndex > 0) {
self.messageIndex--; self.messageIndex--;
this.messageAreaTag = this.messageList[this.messageIndex].areaTag; this.messageAreaTag = this.messageList[this.messageIndex].areaTag;
this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with
return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb);
} }
return cb(null); return cb(null);
}, },
movementKeyPressed : (formData, extraArgs, cb) => { movementKeyPressed : (formData, extraArgs, cb) => {
const bodyView = self.viewControllers.body.getView(1); // :TODO: use const here vs magic # const bodyView = self.viewControllers.body.getView(1); // :TODO: use const here vs magic #
// :TODO: Create methods for up/down vs using keyPressXXXXX // :TODO: Create methods for up/down vs using keyPressXXXXX
switch(formData.key.name) { switch(formData.key.name) {
case 'down arrow' : bodyView.scrollDocumentUp(); break; case 'down arrow' : bodyView.scrollDocumentUp(); break;
case 'up arrow' : bodyView.scrollDocumentDown(); break; case 'up arrow' : bodyView.scrollDocumentDown(); break;
case 'page up' : bodyView.keyPressPageUp(); break; case 'page up' : bodyView.keyPressPageUp(); break;
case 'page down' : bodyView.keyPressPageDown(); break; case 'page down' : bodyView.keyPressPageDown(); break;
} }
// :TODO: need to stop down/page down if doing so would push the last // :TODO: need to stop down/page down if doing so would push the last
// visible page off the screen at all .... this should be handled by MLTEV though... // visible page off the screen at all .... this should be handled by MLTEV though...
return cb(null); return cb(null);
}, },
replyMessage : (formData, extraArgs, cb) => { replyMessage : (formData, extraArgs, cb) => {
if(_.isString(extraArgs.menu)) { if(_.isString(extraArgs.menu)) {
const modOpts = { const modOpts = {
extraArgs : { extraArgs : {
messageAreaTag : self.messageAreaTag, messageAreaTag : self.messageAreaTag,
replyToMessage : self.message, replyToMessage : self.message,
} }
}; };
return self.gotoMenu(extraArgs.menu, modOpts, cb); return self.gotoMenu(extraArgs.menu, modOpts, cb);
} }
self.client.log(extraArgs, 'Missing extraArgs.menu'); self.client.log(extraArgs, 'Missing extraArgs.menu');
return cb(null); return cb(null);
} }
}); });
} }
loadMessageByUuid(uuid, cb) { loadMessageByUuid(uuid, cb) {
const msg = new Message(); const msg = new Message();
msg.load( { uuid : uuid, user : this.client.user }, () => { msg.load( { uuid : uuid, user : this.client.user }, () => {
this.setMessage(msg); this.setMessage(msg);
if(cb) { if(cb) {
return cb(null); return cb(null);
} }
}); });
} }
finishedLoading() { finishedLoading() {
this.loadMessageByUuid(this.messageList[this.messageIndex].messageUuid); this.loadMessageByUuid(this.messageList[this.messageIndex].messageUuid);
} }
getSaveState() { getSaveState() {
return { return {
messageList : this.messageList, messageList : this.messageList,
messageIndex : this.messageIndex, messageIndex : this.messageIndex,
messageTotal : this.messageList.length, messageTotal : this.messageList.length,
}; };
} }
restoreSavedState(savedState) { restoreSavedState(savedState) {
this.messageList = savedState.messageList; this.messageList = savedState.messageList;
this.messageIndex = savedState.messageIndex; this.messageIndex = savedState.messageIndex;
this.messageTotal = savedState.messageTotal; this.messageTotal = savedState.messageTotal;
} }
getMenuResult() { getMenuResult() {
return { return {
messageIndex : this.messageIndex, messageIndex : this.messageIndex,
lastMessageReached : this.lastMessageReached, lastMessageReached : this.lastMessageReached,
}; };
} }
}; };

View File

@ -14,135 +14,135 @@ const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Conference List', name : 'Message Conference List',
desc : 'Module for listing / choosing message conferences', desc : 'Module for listing / choosing message conferences',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
ConfList : 1, ConfList : 1,
// :TODO: // :TODO:
// # areas in conf .... see Obv/2, iNiQ, ... // # areas in conf .... see Obv/2, iNiQ, ...
// //
}; };
exports.getModule = class MessageConfListModule extends MenuModule { exports.getModule = class MessageConfListModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.messageConfs = messageArea.getSortedAvailMessageConferences(this.client); this.messageConfs = messageArea.getSortedAvailMessageConferences(this.client);
const self = this; const self = this;
this.menuMethods = { this.menuMethods = {
changeConference : function(formData, extraArgs, cb) { changeConference : function(formData, extraArgs, cb) {
if(1 === formData.submitId) { if(1 === formData.submitId) {
let conf = self.messageConfs[formData.value.conf]; let conf = self.messageConfs[formData.value.conf];
const confTag = conf.confTag; const confTag = conf.confTag;
conf = conf.conf; // what we want is embedded conf = conf.conf; // what we want is embedded
messageArea.changeMessageConference(self.client, confTag, err => { messageArea.changeMessageConference(self.client, confTag, err => {
if(err) { if(err) {
self.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`); self.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`);
setTimeout( () => { setTimeout( () => {
return self.prevMenu(cb); return self.prevMenu(cb);
}, 1000); }, 1000);
} else { } else {
if(_.isString(conf.art)) { if(_.isString(conf.art)) {
const dispOptions = { const dispOptions = {
client : self.client, client : self.client,
name : conf.art, name : conf.art,
}; };
self.client.term.rawWrite(resetScreen()); self.client.term.rawWrite(resetScreen());
displayThemeArt(dispOptions, () => { displayThemeArt(dispOptions, () => {
// pause by default, unless explicitly told not to // pause by default, unless explicitly told not to
if(_.has(conf, 'options.pause') && false === conf.options.pause) { if(_.has(conf, 'options.pause') && false === conf.options.pause) {
return self.prevMenuOnTimeout(1000, cb); return self.prevMenuOnTimeout(1000, cb);
} else { } else {
self.pausePrompt( () => { self.pausePrompt( () => {
return self.prevMenu(cb); return self.prevMenu(cb);
}); });
} }
}); });
} else { } else {
return self.prevMenu(cb); return self.prevMenu(cb);
} }
} }
}); });
} else { } else {
return cb(null); return cb(null);
} }
} }
}; };
} }
prevMenuOnTimeout(timeout, cb) { prevMenuOnTimeout(timeout, cb) {
setTimeout( () => { setTimeout( () => {
return this.prevMenu(cb); return this.prevMenu(cb);
}, timeout); }, timeout);
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
let loadOpts = { let loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu, mciMap : mciData.menu,
formId : 0, formId : 0,
}; };
vc.loadFromMenuConfig(loadOpts, callback); vc.loadFromMenuConfig(loadOpts, callback);
}, },
function populateConfListView(callback) { function populateConfListView(callback) {
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
const confListView = vc.getView(MciViewIds.ConfList); const confListView = vc.getView(MciViewIds.ConfList);
let i = 1; let i = 1;
confListView.setItems(_.map(self.messageConfs, v => { confListView.setItems(_.map(self.messageConfs, v => {
return stringFormat(listFormat, { return stringFormat(listFormat, {
index : i++, index : i++,
confTag : v.conf.confTag, confTag : v.conf.confTag,
name : v.conf.name, name : v.conf.name,
desc : v.conf.desc, desc : v.conf.desc,
}); });
})); }));
i = 1; i = 1;
confListView.setFocusItems(_.map(self.messageConfs, v => { confListView.setFocusItems(_.map(self.messageConfs, v => {
return stringFormat(focusListFormat, { return stringFormat(focusListFormat, {
index : i++, index : i++,
confTag : v.conf.confTag, confTag : v.conf.confTag,
name : v.conf.name, name : v.conf.name,
desc : v.conf.desc, desc : v.conf.desc,
}); });
})); }));
confListView.redraw(); confListView.redraw();
callback(null); callback(null);
}, },
function populateTextViews(callback) { function populateTextViews(callback) {
// :TODO: populate other avail MCI, e.g. current conf name // :TODO: populate other avail MCI, e.g. current conf name
callback(null); callback(null);
} }
], ],
function complete(err) { function complete(err) {
cb(err); cb(err);
} }
); );
}); });
} }
}; };

View File

@ -30,229 +30,229 @@ const moment = require('moment');
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message List', name : 'Message List',
desc : 'Module for listing/browsing available messages', desc : 'Module for listing/browsing available messages',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
msgList : 1, // VM1 msgList : 1, // VM1
msgInfo1 : 2, // TL2 msgInfo1 : 2, // TL2
}; };
exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(MenuModule) { exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(MenuModule) {
constructor(options) { constructor(options) {
super(options); super(options);
// :TODO: consider this pattern in base MenuModule - clean up code all over // :TODO: consider this pattern in base MenuModule - clean up code all over
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs);
this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false); this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false);
this.menuMethods = { this.menuMethods = {
selectMessage : (formData, extraArgs, cb) => { selectMessage : (formData, extraArgs, cb) => {
if(MciViewIds.msgList === formData.submitId) { if(MciViewIds.msgList === formData.submitId) {
this.initialFocusIndex = formData.value.message; this.initialFocusIndex = formData.value.message;
const modOpts = { const modOpts = {
extraArgs : { extraArgs : {
messageAreaTag : this.getSelectedAreaTag(formData.value.message),// this.config.messageAreaTag, messageAreaTag : this.getSelectedAreaTag(formData.value.message),// this.config.messageAreaTag,
messageList : this.config.messageList, messageList : this.config.messageList,
messageIndex : formData.value.message, messageIndex : formData.value.message,
lastMessageNextExit : true, lastMessageNextExit : true,
} }
}; };
if(_.isBoolean(this.config.noUpdateLastReadId)) { if(_.isBoolean(this.config.noUpdateLastReadId)) {
modOpts.extraArgs.noUpdateLastReadId = this.config.noUpdateLastReadId; modOpts.extraArgs.noUpdateLastReadId = this.config.noUpdateLastReadId;
} }
// //
// Provide a serializer so we don't dump *huge* bits of information to the log // Provide a serializer so we don't dump *huge* bits of information to the log
// due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189 // due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189
// //
const self = this; const self = this;
modOpts.extraArgs.toJSON = function() { modOpts.extraArgs.toJSON = function() {
const logMsgList = (self.config.messageList.length <= 4) ? const logMsgList = (self.config.messageList.length <= 4) ?
self.config.messageList : self.config.messageList :
self.config.messageList.slice(0, 2).concat(self.config.messageList.slice(-2)); self.config.messageList.slice(0, 2).concat(self.config.messageList.slice(-2));
return { return {
// note |this| is scope of toJSON()! // note |this| is scope of toJSON()!
messageAreaTag : this.messageAreaTag, messageAreaTag : this.messageAreaTag,
apprevMessageList : logMsgList, apprevMessageList : logMsgList,
messageCount : this.messageList.length, messageCount : this.messageList.length,
messageIndex : this.messageIndex, messageIndex : this.messageIndex,
}; };
}; };
return this.gotoMenu(this.config.menuViewPost || 'messageAreaViewPost', modOpts, cb); return this.gotoMenu(this.config.menuViewPost || 'messageAreaViewPost', modOpts, cb);
} else { } else {
return cb(null); return cb(null);
} }
}, },
fullExit : (formData, extraArgs, cb) => { fullExit : (formData, extraArgs, cb) => {
this.menuResult = { fullExit : true }; this.menuResult = { fullExit : true };
return this.prevMenu(cb); return this.prevMenu(cb);
} }
}; };
} }
getSelectedAreaTag(listIndex) { getSelectedAreaTag(listIndex) {
return this.config.messageList[listIndex].areaTag || this.config.messageAreaTag; return this.config.messageList[listIndex].areaTag || this.config.messageAreaTag;
} }
enter() { enter() {
if(this.lastMessageReachedExit) { if(this.lastMessageReachedExit) {
return this.prevMenu(); return this.prevMenu();
} }
super.enter(); super.enter();
// //
// Config can specify |messageAreaTag| else it comes from // Config can specify |messageAreaTag| else it comes from
// the user's current area. If |messageList| is supplied, // the user's current area. If |messageList| is supplied,
// each item is expected to contain |areaTag|, so we use that // each item is expected to contain |areaTag|, so we use that
// instead in those cases. // instead in those cases.
// //
if(!Array.isArray(this.config.messageList)) { if(!Array.isArray(this.config.messageList)) {
if(this.config.messageAreaTag) { if(this.config.messageAreaTag) {
this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag); this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag);
} else { } else {
this.config.messageAreaTag = this.client.user.properties.message_area_tag; this.config.messageAreaTag = this.client.user.properties.message_area_tag;
} }
} }
} }
leave() { leave() {
this.tempMessageConfAndAreaRestore(); this.tempMessageConfAndAreaRestore();
super.leave(); super.leave();
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
let configProvidedMessageList = false; let configProvidedMessageList = false;
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu mciMap : mciData.menu
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
}, },
function fetchMessagesInArea(callback) { function fetchMessagesInArea(callback) {
// //
// Config can supply messages else we'll need to populate the list now // Config can supply messages else we'll need to populate the list now
// //
if(_.isArray(self.config.messageList)) { if(_.isArray(self.config.messageList)) {
configProvidedMessageList = true; configProvidedMessageList = true;
return callback(0 === self.config.messageList.length ? new Error('No messages in area') : null); return callback(0 === self.config.messageList.length ? new Error('No messages in area') : null);
} }
messageArea.getMessageListForArea(self.client, self.config.messageAreaTag, function msgs(err, msgList) { messageArea.getMessageListForArea(self.client, self.config.messageAreaTag, function msgs(err, msgList) {
if(!msgList || 0 === msgList.length) { if(!msgList || 0 === msgList.length) {
return callback(new Error('No messages in area')); return callback(new Error('No messages in area'));
} }
self.config.messageList = msgList; self.config.messageList = msgList;
return callback(err); return callback(err);
}); });
}, },
function getLastReadMesageId(callback) { function getLastReadMesageId(callback) {
// messageList entries can contain |isNew| if they want to be considered new // messageList entries can contain |isNew| if they want to be considered new
if(configProvidedMessageList) { if(configProvidedMessageList) {
self.lastReadId = 0; self.lastReadId = 0;
return callback(null); return callback(null);
} }
messageArea.getMessageAreaLastReadId(self.client.user.userId, self.config.messageAreaTag, function lastRead(err, lastReadId) { messageArea.getMessageAreaLastReadId(self.client.user.userId, self.config.messageAreaTag, function lastRead(err, lastReadId) {
self.lastReadId = lastReadId || 0; self.lastReadId = lastReadId || 0;
return callback(null); // ignore any errors, e.g. missing value return callback(null); // ignore any errors, e.g. missing value
}); });
}, },
function updateMessageListObjects(callback) { function updateMessageListObjects(callback) {
const dateTimeFormat = self.menuConfig.config.dateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat(); const dateTimeFormat = self.menuConfig.config.dateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat();
const newIndicator = self.menuConfig.config.newIndicator || '*'; const newIndicator = self.menuConfig.config.newIndicator || '*';
const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues
let msgNum = 1; let msgNum = 1;
self.config.messageList.forEach( (listItem, index) => { self.config.messageList.forEach( (listItem, index) => {
listItem.msgNum = msgNum++; listItem.msgNum = msgNum++;
listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat); listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat);
const isNew = _.isBoolean(listItem.isNew) ? listItem.isNew : listItem.messageId > self.lastReadId; const isNew = _.isBoolean(listItem.isNew) ? listItem.isNew : listItem.messageId > self.lastReadId;
listItem.newIndicator = isNew ? newIndicator : regIndicator; listItem.newIndicator = isNew ? newIndicator : regIndicator;
if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) { if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) {
self.initialFocusIndex = index; self.initialFocusIndex = index;
} }
listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text
}); });
return callback(null); return callback(null);
}, },
function populateList(callback) { function populateList(callback) {
const msgListView = vc.getView(MciViewIds.msgList); const msgListView = vc.getView(MciViewIds.msgList);
// :TODO: replace with standard custom info MCI - msgNumSelected, msgNumTotal, areaName, areaDesc, confName, confDesc, ... // :TODO: replace with standard custom info MCI - msgNumSelected, msgNumTotal, areaName, areaDesc, confName, confDesc, ...
const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}';
msgListView.setItems(self.config.messageList); msgListView.setItems(self.config.messageList);
msgListView.on('index update', idx => { msgListView.on('index update', idx => {
self.setViewText( self.setViewText(
'allViews', 'allViews',
MciViewIds.msgInfo1, MciViewIds.msgInfo1,
stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.config.messageList.length } )); stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.config.messageList.length } ));
}); });
if(self.initialFocusIndex > 0) { if(self.initialFocusIndex > 0) {
// note: causes redraw() // note: causes redraw()
msgListView.setFocusItemIndex(self.initialFocusIndex); msgListView.setFocusItemIndex(self.initialFocusIndex);
} else { } else {
msgListView.redraw(); msgListView.redraw();
} }
return callback(null); return callback(null);
}, },
function drawOtherViews(callback) { function drawOtherViews(callback) {
const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}';
self.setViewText( self.setViewText(
'allViews', 'allViews',
MciViewIds.msgInfo1, MciViewIds.msgInfo1,
stringFormat(messageInfo1Format, { msgNumSelected : self.initialFocusIndex + 1, msgNumTotal : self.config.messageList.length } )); stringFormat(messageInfo1Format, { msgNumSelected : self.initialFocusIndex + 1, msgNumTotal : self.config.messageList.length } ));
return callback(null); return callback(null);
}, },
], ],
err => { err => {
if(err) { if(err) {
self.client.log.error( { error : err.message }, 'Error loading message list'); self.client.log.error( { error : err.message }, 'Error loading message list');
} }
return cb(err); return cb(err);
} }
); );
}); });
} }
getSaveState() { getSaveState() {
return { initialFocusIndex : this.initialFocusIndex }; return { initialFocusIndex : this.initialFocusIndex };
} }
restoreSavedState(savedState) { restoreSavedState(savedState) {
if(savedState) { if(savedState) {
this.initialFocusIndex = savedState.initialFocusIndex; this.initialFocusIndex = savedState.initialFocusIndex;
} }
} }
getMenuResult() { getMenuResult() {
return this.menuResult; return this.menuResult;
} }
}; };

View File

@ -14,53 +14,53 @@ exports.recordMessage = recordMessage;
let msgNetworkModules = []; let msgNetworkModules = [];
function startup(cb) { function startup(cb) {
async.series( async.series(
[ [
function loadModules(callback) { function loadModules(callback) {
loadModulesForCategory('scannerTossers', (err, module) => { loadModulesForCategory('scannerTossers', (err, module) => {
if(!err) { if(!err) {
const modInst = new module.getModule(); const modInst = new module.getModule();
modInst.startup(err => { modInst.startup(err => {
if(!err) { if(!err) {
msgNetworkModules.push(modInst); msgNetworkModules.push(modInst);
} }
}); });
} }
}, err => { }, err => {
callback(err); callback(err);
}); });
} }
], ],
cb cb
); );
} }
function shutdown(cb) { function shutdown(cb) {
async.each( async.each(
msgNetworkModules, msgNetworkModules,
(msgNetModule, next) => { (msgNetModule, next) => {
msgNetModule.shutdown( () => { msgNetModule.shutdown( () => {
return next(); return next();
}); });
}, },
() => { () => {
msgNetworkModules = []; msgNetworkModules = [];
return cb(null); return cb(null);
} }
); );
} }
function recordMessage(message, cb) { function recordMessage(message, cb) {
// //
// Give all message network modules (scanner/tossers) // Give all message network modules (scanner/tossers)
// a chance to do something with |message|. Any or all can // a chance to do something with |message|. Any or all can
// choose to ignore it. // choose to ignore it.
// //
async.each(msgNetworkModules, (modInst, next) => { async.each(msgNetworkModules, (modInst, next) => {
modInst.record(message); modInst.record(message);
next(); next();
}, err => { }, err => {
cb(err); cb(err);
}); });
} }

View File

@ -7,17 +7,17 @@ var PluginModule = require('./plugin_module.js').PluginModule;
exports.MessageScanTossModule = MessageScanTossModule; exports.MessageScanTossModule = MessageScanTossModule;
function MessageScanTossModule() { function MessageScanTossModule() {
PluginModule.call(this); PluginModule.call(this);
} }
require('util').inherits(MessageScanTossModule, PluginModule); require('util').inherits(MessageScanTossModule, PluginModule);
MessageScanTossModule.prototype.startup = function(cb) { MessageScanTossModule.prototype.startup = function(cb) {
return cb(null); return cb(null);
}; };
MessageScanTossModule.prototype.shutdown = function(cb) { MessageScanTossModule.prototype.shutdown = function(cb) {
return cb(null); return cb(null);
}; };
MessageScanTossModule.prototype.record = function(/*message*/) { MessageScanTossModule.prototype.record = function(/*message*/) {

File diff suppressed because it is too large Load Diff

View File

@ -16,9 +16,9 @@ const _ = require('lodash');
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'New Scan', name : 'New Scan',
desc : 'Performs a new scan against various areas of the system', desc : 'Performs a new scan against various areas of the system',
author : 'NuSkooler', author : 'NuSkooler',
}; };
/* /*
@ -30,239 +30,239 @@ exports.moduleInfo = {
*/ */
const MciCodeIds = { const MciCodeIds = {
ScanStatusLabel : 1, // TL1 ScanStatusLabel : 1, // TL1
ScanStatusList : 2, // VM2 (appends) ScanStatusList : 2, // VM2 (appends)
}; };
const Steps = { const Steps = {
MessageConfs : 'messageConferences', MessageConfs : 'messageConferences',
FileBase : 'fileBase', FileBase : 'fileBase',
Finished : 'finished', Finished : 'finished',
}; };
exports.getModule = class NewScanModule extends MenuModule { exports.getModule = class NewScanModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.newScanFullExit = _.get(options, 'lastMenuResult.fullExit', false); this.newScanFullExit = _.get(options, 'lastMenuResult.fullExit', false);
this.currentStep = Steps.MessageConfs; this.currentStep = Steps.MessageConfs;
this.currentScanAux = {}; this.currentScanAux = {};
// :TODO: Make this conf/area specific: // :TODO: Make this conf/area specific:
const config = this.menuConfig.config; const config = this.menuConfig.config;
this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...';
this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new'; this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new';
this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found'; this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found';
this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan'; this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan';
} }
updateScanStatus(statusText) { updateScanStatus(statusText) {
this.setViewText('allViews', MciCodeIds.ScanStatusLabel, statusText); this.setViewText('allViews', MciCodeIds.ScanStatusLabel, statusText);
} }
newScanMessageConference(cb) { newScanMessageConference(cb) {
// lazy init // lazy init
if(!this.sortedMessageConfs) { if(!this.sortedMessageConfs) {
const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc. const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc.
this.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(this.client, getAvailOpts), (v, k) => { this.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(this.client, getAvailOpts), (v, k) => {
return { return {
confTag : k, confTag : k,
conf : v, conf : v,
}; };
}); });
// //
// Sort conferences by name, other than 'system_internal' which should // Sort conferences by name, other than 'system_internal' which should
// always come first such that we display private mails/etc. before // always come first such that we display private mails/etc. before
// other conferences & areas // other conferences & areas
// //
this.sortedMessageConfs.sort((a, b) => { this.sortedMessageConfs.sort((a, b) => {
if('system_internal' === a.confTag) { if('system_internal' === a.confTag) {
return -1; return -1;
} else { } else {
return a.conf.name.localeCompare(b.conf.name, { sensitivity : false, numeric : true } ); return a.conf.name.localeCompare(b.conf.name, { sensitivity : false, numeric : true } );
} }
}); });
this.currentScanAux.conf = this.currentScanAux.conf || 0; this.currentScanAux.conf = this.currentScanAux.conf || 0;
this.currentScanAux.area = this.currentScanAux.area || 0; this.currentScanAux.area = this.currentScanAux.area || 0;
} }
const currentConf = this.sortedMessageConfs[this.currentScanAux.conf]; const currentConf = this.sortedMessageConfs[this.currentScanAux.conf];
this.newScanMessageArea(currentConf, () => { this.newScanMessageArea(currentConf, () => {
if(this.sortedMessageConfs.length > this.currentScanAux.conf + 1) { if(this.sortedMessageConfs.length > this.currentScanAux.conf + 1) {
this.currentScanAux.conf += 1; this.currentScanAux.conf += 1;
this.currentScanAux.area = 0; this.currentScanAux.area = 0;
return this.newScanMessageConference(cb); // recursive to next conf return this.newScanMessageConference(cb); // recursive to next conf
} }
this.updateScanStatus(this.scanCompleteMsg); this.updateScanStatus(this.scanCompleteMsg);
return cb(Errors.DoesNotExist('No more conferences')); return cb(Errors.DoesNotExist('No more conferences'));
}); });
} }
newScanMessageArea(conf, cb) { newScanMessageArea(conf, cb) {
// :TODO: it would be nice to cache this - must be done by conf! // :TODO: it would be nice to cache this - must be done by conf!
const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ); const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } );
const currentArea = sortedAreas[this.currentScanAux.area]; const currentArea = sortedAreas[this.currentScanAux.area];
// //
// Scan and update index until we find something. If results are found, // Scan and update index until we find something. If results are found,
// we'll goto the list module & show them. // we'll goto the list module & show them.
// //
const self = this; const self = this;
async.waterfall( async.waterfall(
[ [
function checkAndUpdateIndex(callback) { function checkAndUpdateIndex(callback) {
// Advance to next area if possible // Advance to next area if possible
if(sortedAreas.length >= self.currentScanAux.area + 1) { if(sortedAreas.length >= self.currentScanAux.area + 1) {
self.currentScanAux.area += 1; self.currentScanAux.area += 1;
return callback(null); return callback(null);
} else { } else {
self.updateScanStatus(self.scanCompleteMsg); self.updateScanStatus(self.scanCompleteMsg);
return callback(Errors.DoesNotExist('No more areas')); // this will stop our scan return callback(Errors.DoesNotExist('No more areas')); // this will stop our scan
} }
}, },
function updateStatusScanStarted(callback) { function updateStatusScanStarted(callback) {
self.updateScanStatus(stringFormat(self.scanStartFmt, { self.updateScanStatus(stringFormat(self.scanStartFmt, {
confName : conf.conf.name, confName : conf.conf.name,
confDesc : conf.conf.desc, confDesc : conf.conf.desc,
areaName : currentArea.area.name, areaName : currentArea.area.name,
areaDesc : currentArea.area.desc areaDesc : currentArea.area.desc
})); }));
return callback(null); return callback(null);
}, },
function getNewMessagesCountInArea(callback) { function getNewMessagesCountInArea(callback) {
msgArea.getNewMessageCountInAreaForUser( msgArea.getNewMessageCountInAreaForUser(
self.client.user.userId, currentArea.areaTag, (err, newMessageCount) => { self.client.user.userId, currentArea.areaTag, (err, newMessageCount) => {
callback(err, newMessageCount); callback(err, newMessageCount);
} }
); );
}, },
function displayMessageList(newMessageCount) { function displayMessageList(newMessageCount) {
if(newMessageCount <= 0) { if(newMessageCount <= 0) {
return self.newScanMessageArea(conf, cb); // next area, if any return self.newScanMessageArea(conf, cb); // next area, if any
} }
const nextModuleOpts = { const nextModuleOpts = {
extraArgs: { extraArgs: {
messageAreaTag : currentArea.areaTag, messageAreaTag : currentArea.areaTag,
} }
}; };
return self.gotoMenu(self.menuConfig.config.newScanMessageList || 'newScanMessageList', nextModuleOpts); return self.gotoMenu(self.menuConfig.config.newScanMessageList || 'newScanMessageList', nextModuleOpts);
} }
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
} }
newScanFileBase(cb) { newScanFileBase(cb) {
// :TODO: add in steps // :TODO: add in steps
const filterCriteria = { const filterCriteria = {
newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user), newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user),
areaTag : getAvailableFileAreaTags(this.client), areaTag : getAvailableFileAreaTags(this.client),
order : 'ascending', // oldest first order : 'ascending', // oldest first
}; };
FileEntry.findFiles( FileEntry.findFiles(
filterCriteria, filterCriteria,
(err, fileIds) => { (err, fileIds) => {
if(err || 0 === fileIds.length) { if(err || 0 === fileIds.length) {
return cb(err ? err : Errors.DoesNotExist('No more new files')); return cb(err ? err : Errors.DoesNotExist('No more new files'));
} }
FileBaseFilters.setFileBaseLastViewedFileIdForUser( this.client.user, fileIds[fileIds.length - 1] ); FileBaseFilters.setFileBaseLastViewedFileIdForUser( this.client.user, fileIds[fileIds.length - 1] );
const menuOpts = { const menuOpts = {
extraArgs : { extraArgs : {
fileList : fileIds, fileList : fileIds,
}, },
}; };
return this.gotoMenu(this.menuConfig.config.newScanFileBaseList || 'newScanFileBaseList', menuOpts); return this.gotoMenu(this.menuConfig.config.newScanFileBaseList || 'newScanFileBaseList', menuOpts);
} }
); );
} }
getSaveState() { getSaveState() {
return { return {
currentStep : this.currentStep, currentStep : this.currentStep,
currentScanAux : this.currentScanAux, currentScanAux : this.currentScanAux,
}; };
} }
restoreSavedState(savedState) { restoreSavedState(savedState) {
this.currentStep = savedState.currentStep; this.currentStep = savedState.currentStep;
this.currentScanAux = savedState.currentScanAux; this.currentScanAux = savedState.currentScanAux;
} }
performScanCurrentStep(cb) { performScanCurrentStep(cb) {
switch(this.currentStep) { switch(this.currentStep) {
case Steps.MessageConfs : case Steps.MessageConfs :
this.newScanMessageConference( () => { this.newScanMessageConference( () => {
this.currentStep = Steps.FileBase; this.currentStep = Steps.FileBase;
return this.performScanCurrentStep(cb); return this.performScanCurrentStep(cb);
}); });
break; break;
case Steps.FileBase : case Steps.FileBase :
this.newScanFileBase( () => { this.newScanFileBase( () => {
this.currentStep = Steps.Finished; this.currentStep = Steps.Finished;
return this.performScanCurrentStep(cb); return this.performScanCurrentStep(cb);
}); });
break; break;
default : return cb(null); default : return cb(null);
} }
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
if(this.newScanFullExit) { if(this.newScanFullExit) {
// user has canceled the entire scan @ message list view // user has canceled the entire scan @ message list view
return cb(null); return cb(null);
} }
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
// :TODO: display scan step/etc. // :TODO: display scan step/etc.
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu, mciMap : mciData.menu,
noInput : true, noInput : true,
}; };
vc.loadFromMenuConfig(loadOpts, callback); vc.loadFromMenuConfig(loadOpts, callback);
}, },
function performCurrentStepScan(callback) { function performCurrentStepScan(callback) {
return self.performScanCurrentStep(callback); return self.performScanCurrentStep(callback);
} }
], ],
err => { err => {
if(err) { if(err) {
self.client.log.error( { error : err.toString() }, 'Error during new scan'); self.client.log.error( { error : err.toString() }, 'Error during new scan');
} }
return cb(err); return cb(err);
} }
); );
}); });
} }
}; };

View File

@ -10,136 +10,136 @@ const Config = require('./config.js').get;
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
exports.moduleInfo = { exports.moduleInfo = {
name : 'NUA', name : 'NUA',
desc : 'New User Application', desc : 'New User Application',
}; };
const MciViewIds = { const MciViewIds = {
userName : 1, userName : 1,
password : 9, password : 9,
confirm : 10, confirm : 10,
errMsg : 11, errMsg : 11,
}; };
exports.getModule = class NewUserAppModule extends MenuModule { exports.getModule = class NewUserAppModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
const self = this; const self = this;
this.menuMethods = { this.menuMethods = {
// //
// Validation stuff // Validation stuff
// //
validatePassConfirmMatch : function(data, cb) { validatePassConfirmMatch : function(data, cb) {
const passwordView = self.viewControllers.menu.getView(MciViewIds.password); const passwordView = self.viewControllers.menu.getView(MciViewIds.password);
return cb(passwordView.getData() === data ? null : new Error('Passwords do not match')); return cb(passwordView.getData() === data ? null : new Error('Passwords do not match'));
}, },
viewValidationListener : function(err, cb) { viewValidationListener : function(err, cb) {
const errMsgView = self.viewControllers.menu.getView(MciViewIds.errMsg); const errMsgView = self.viewControllers.menu.getView(MciViewIds.errMsg);
let newFocusId; let newFocusId;
if(err) { if(err) {
errMsgView.setText(err.message); errMsgView.setText(err.message);
err.view.clearText(); err.view.clearText();
if(err.view.getId() === MciViewIds.confirm) { if(err.view.getId() === MciViewIds.confirm) {
newFocusId = MciViewIds.password; newFocusId = MciViewIds.password;
self.viewControllers.menu.getView(MciViewIds.password).clearText(); self.viewControllers.menu.getView(MciViewIds.password).clearText();
} }
} else { } else {
errMsgView.clearText(); errMsgView.clearText();
} }
return cb(newFocusId); return cb(newFocusId);
}, },
// //
// Submit handlers // Submit handlers
// //
submitApplication : function(formData, extraArgs, cb) { submitApplication : function(formData, extraArgs, cb) {
const newUser = new User(); const newUser = new User();
const config = Config(); const config = Config();
newUser.username = formData.value.username; newUser.username = formData.value.username;
// //
// We have to disable ACS checks for initial default areas as the user is not yet ready // We have to disable ACS checks for initial default areas as the user is not yet ready
// //
let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck
let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck
// can't store undefined! // can't store undefined!
confTag = confTag || ''; confTag = confTag || '';
areaTag = areaTag || ''; areaTag = areaTag || '';
newUser.properties = { newUser.properties = {
real_name : formData.value.realName, real_name : formData.value.realName,
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format
sex : formData.value.sex, sex : formData.value.sex,
location : formData.value.location, location : formData.value.location,
affiliation : formData.value.affils, affiliation : formData.value.affils,
email_address : formData.value.email, email_address : formData.value.email,
web_address : formData.value.web, web_address : formData.value.web,
account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format
message_conf_tag : confTag, message_conf_tag : confTag,
message_area_tag : areaTag, message_area_tag : areaTag,
term_height : self.client.term.termHeight, term_height : self.client.term.termHeight,
term_width : self.client.term.termWidth, term_width : self.client.term.termWidth,
// :TODO: Other defaults // :TODO: Other defaults
// :TODO: should probably have a place to create defaults/etc. // :TODO: should probably have a place to create defaults/etc.
}; };
if('*' === config.defaults.theme) { if('*' === config.defaults.theme) {
newUser.properties.theme_id = theme.getRandomTheme(); newUser.properties.theme_id = theme.getRandomTheme();
} else { } else {
newUser.properties.theme_id = config.defaults.theme; newUser.properties.theme_id = config.defaults.theme;
} }
// :TODO: User.create() should validate email uniqueness! // :TODO: User.create() should validate email uniqueness!
newUser.create(formData.value.password, err => { newUser.create(formData.value.password, err => {
if(err) { if(err) {
self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed'); self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed');
self.gotoMenu(extraArgs.error, err => { self.gotoMenu(extraArgs.error, err => {
if(err) { if(err) {
return self.prevMenu(cb); return self.prevMenu(cb);
} }
return cb(null); return cb(null);
}); });
} else { } else {
self.client.log.info( { username : formData.value.username, userId : newUser.userId }, 'New user created'); self.client.log.info( { username : formData.value.username, userId : newUser.userId }, 'New user created');
// Cache SysOp information now // Cache SysOp information now
// :TODO: Similar to bbs.js. DRY // :TODO: Similar to bbs.js. DRY
if(newUser.isSysOp()) { if(newUser.isSysOp()) {
config.general.sysOp = { config.general.sysOp = {
username : formData.value.username, username : formData.value.username,
properties : newUser.properties, properties : newUser.properties,
}; };
} }
if(User.AccountStatus.inactive === self.client.user.properties.account_status) { if(User.AccountStatus.inactive === self.client.user.properties.account_status) {
return self.gotoMenu(extraArgs.inactive, cb); return self.gotoMenu(extraArgs.inactive, cb);
} else { } else {
// //
// If active now, we need to call login() to authenticate // If active now, we need to call login() to authenticate
// //
return login(self, formData, extraArgs, cb); return login(self, formData, extraArgs, cb);
} }
} }
}); });
}, },
}; };
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
return this.standardMCIReadyHandler(mciData, cb); return this.standardMCIReadyHandler(mciData, cb);
} }
}; };

View File

@ -5,8 +5,8 @@
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const { const {
getModDatabasePath, getModDatabasePath,
getTransactionDatabase getTransactionDatabase
} = require('./database.js'); } = require('./database.js');
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
@ -30,136 +30,136 @@ const moment = require('moment');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Onelinerz', name : 'Onelinerz',
desc : 'Standard local onelinerz', desc : 'Standard local onelinerz',
author : 'NuSkooler', author : 'NuSkooler',
packageName : 'codes.l33t.enigma.onelinerz', packageName : 'codes.l33t.enigma.onelinerz',
}; };
const MciViewIds = { const MciViewIds = {
ViewForm : { ViewForm : {
Entries : 1, Entries : 1,
AddPrompt : 2, AddPrompt : 2,
}, },
AddForm : { AddForm : {
NewEntry : 1, NewEntry : 1,
EntryPreview : 2, EntryPreview : 2,
AddPrompt : 3, AddPrompt : 3,
} }
}; };
const FormIds = { const FormIds = {
View : 0, View : 0,
Add : 1, Add : 1,
}; };
exports.getModule = class OnelinerzModule extends MenuModule { exports.getModule = class OnelinerzModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
const self = this; const self = this;
this.menuMethods = { this.menuMethods = {
viewAddScreen : function(formData, extraArgs, cb) { viewAddScreen : function(formData, extraArgs, cb) {
return self.displayAddScreen(cb); return self.displayAddScreen(cb);
}, },
addEntry : function(formData, extraArgs, cb) { addEntry : function(formData, extraArgs, cb) {
if(_.isString(formData.value.oneliner) && formData.value.oneliner.length > 0) { if(_.isString(formData.value.oneliner) && formData.value.oneliner.length > 0) {
const oneliner = formData.value.oneliner.trim(); // remove any trailing ws const oneliner = formData.value.oneliner.trim(); // remove any trailing ws
self.storeNewOneliner(oneliner, err => { self.storeNewOneliner(oneliner, err => {
if(err) { if(err) {
self.client.log.warn( { error : err.message }, 'Failed saving oneliner'); self.client.log.warn( { error : err.message }, 'Failed saving oneliner');
} }
self.clearAddForm(); self.clearAddForm();
return self.displayViewScreen(true, cb); // true=cls return self.displayViewScreen(true, cb); // true=cls
}); });
} else { } else {
// empty message - treat as if cancel was hit // empty message - treat as if cancel was hit
return self.displayViewScreen(true, cb); // true=cls return self.displayViewScreen(true, cb); // true=cls
} }
}, },
cancelAdd : function(formData, extraArgs, cb) { cancelAdd : function(formData, extraArgs, cb) {
self.clearAddForm(); self.clearAddForm();
return self.displayViewScreen(true, cb); // true=cls return self.displayViewScreen(true, cb); // true=cls
} }
}; };
} }
initSequence() { initSequence() {
const self = this; const self = this;
async.series( async.series(
[ [
function beforeDisplayArt(callback) { function beforeDisplayArt(callback) {
return self.beforeArt(callback); return self.beforeArt(callback);
}, },
function display(callback) { function display(callback) {
return self.displayViewScreen(false, callback); return self.displayViewScreen(false, callback);
} }
], ],
err => { err => {
if(err) { if(err) {
// :TODO: Handle me -- initSequence() should really take a completion callback // :TODO: Handle me -- initSequence() should really take a completion callback
} }
self.finishedLoading(); self.finishedLoading();
} }
); );
} }
displayViewScreen(clearScreen, cb) { displayViewScreen(clearScreen, cb) {
const self = this; const self = this;
async.waterfall( async.waterfall(
[ [
function clearAndDisplayArt(callback) { function clearAndDisplayArt(callback) {
if(self.viewControllers.add) { if(self.viewControllers.add) {
self.viewControllers.add.setFocus(false); self.viewControllers.add.setFocus(false);
} }
if(clearScreen) { if(clearScreen) {
self.client.term.rawWrite(ansi.resetScreen()); self.client.term.rawWrite(ansi.resetScreen());
} }
theme.displayThemedAsset( theme.displayThemedAsset(
self.menuConfig.config.art.entries, self.menuConfig.config.art.entries,
self.client, self.client,
{ font : self.menuConfig.font, trailingLF : false }, { font : self.menuConfig.font, trailingLF : false },
(err, artData) => { (err, artData) => {
return callback(err, artData); return callback(err, artData);
} }
); );
}, },
function initOrRedrawViewController(artData, callback) { function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) { if(_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController( const vc = self.addViewController(
'view', 'view',
new ViewController( { client : self.client, formId : FormIds.View } ) new ViewController( { client : self.client, formId : FormIds.View } )
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds.View, formId : FormIds.View,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
} else { } else {
self.viewControllers.view.setFocus(true); self.viewControllers.view.setFocus(true);
self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt).redraw(); self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt).redraw();
return callback(null); return callback(null);
} }
}, },
function fetchEntries(callback) { function fetchEntries(callback) {
const entriesView = self.viewControllers.view.getView(MciViewIds.ViewForm.Entries); const entriesView = self.viewControllers.view.getView(MciViewIds.ViewForm.Entries);
const limit = entriesView.dimens.height; const limit = entriesView.dimens.height;
let entries = []; let entries = [];
self.db.each( self.db.each(
`SELECT * `SELECT *
FROM ( FROM (
SELECT * SELECT *
FROM onelinerz FROM onelinerz
@ -167,172 +167,172 @@ exports.getModule = class OnelinerzModule extends MenuModule {
LIMIT ${limit} LIMIT ${limit}
) )
ORDER BY timestamp ASC;`, ORDER BY timestamp ASC;`,
(err, row) => { (err, row) => {
if(!err) { if(!err) {
row.timestamp = moment(row.timestamp); // convert -> moment row.timestamp = moment(row.timestamp); // convert -> moment
entries.push(row); entries.push(row);
} }
}, },
err => { err => {
return callback(err, entriesView, entries); return callback(err, entriesView, entries);
} }
); );
}, },
function populateEntries(entriesView, entries, callback) { function populateEntries(entriesView, entries, callback) {
const listFormat = self.menuConfig.config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent const listFormat = self.menuConfig.config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent
const tsFormat = self.menuConfig.config.timestampFormat || 'ddd h:mma'; const tsFormat = self.menuConfig.config.timestampFormat || 'ddd h:mma';
entriesView.setItems(entries.map( e => { entriesView.setItems(entries.map( e => {
return stringFormat(listFormat, { return stringFormat(listFormat, {
userId : e.user_id, userId : e.user_id,
username : e.user_name, username : e.user_name,
oneliner : e.oneliner, oneliner : e.oneliner,
ts : e.timestamp.format(tsFormat), ts : e.timestamp.format(tsFormat),
} ); } );
})); }));
entriesView.redraw(); entriesView.redraw();
return callback(null); return callback(null);
}, },
function finalPrep(callback) { function finalPrep(callback) {
const promptView = self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt); const promptView = self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt);
promptView.setFocusItemIndex(1); // default to NO promptView.setFocusItemIndex(1); // default to NO
return callback(null); return callback(null);
} }
], ],
err => { err => {
if(cb) { if(cb) {
return cb(err); return cb(err);
} }
} }
); );
} }
displayAddScreen(cb) { displayAddScreen(cb) {
const self = this; const self = this;
async.waterfall( async.waterfall(
[ [
function clearAndDisplayArt(callback) { function clearAndDisplayArt(callback) {
self.viewControllers.view.setFocus(false); self.viewControllers.view.setFocus(false);
self.client.term.rawWrite(ansi.resetScreen()); self.client.term.rawWrite(ansi.resetScreen());
theme.displayThemedAsset( theme.displayThemedAsset(
self.menuConfig.config.art.add, self.menuConfig.config.art.add,
self.client, self.client,
{ font : self.menuConfig.font }, { font : self.menuConfig.font },
(err, artData) => { (err, artData) => {
return callback(err, artData); return callback(err, artData);
} }
); );
}, },
function initOrRedrawViewController(artData, callback) { function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) { if(_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController( const vc = self.addViewController(
'add', 'add',
new ViewController( { client : self.client, formId : FormIds.Add } ) new ViewController( { client : self.client, formId : FormIds.Add } )
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds.Add, formId : FormIds.Add,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
} else { } else {
self.viewControllers.add.setFocus(true); self.viewControllers.add.setFocus(true);
self.viewControllers.add.redrawAll(); self.viewControllers.add.redrawAll();
self.viewControllers.add.switchFocus(MciViewIds.AddForm.NewEntry); self.viewControllers.add.switchFocus(MciViewIds.AddForm.NewEntry);
return callback(null); return callback(null);
} }
} }
], ],
err => { err => {
if(cb) { if(cb) {
return cb(err); return cb(err);
} }
} }
); );
} }
clearAddForm() { clearAddForm() {
this.setViewText('add', MciViewIds.AddForm.NewEntry, ''); this.setViewText('add', MciViewIds.AddForm.NewEntry, '');
this.setViewText('add', MciViewIds.AddForm.EntryPreview, ''); this.setViewText('add', MciViewIds.AddForm.EntryPreview, '');
} }
initDatabase(cb) { initDatabase(cb) {
const self = this; const self = this;
async.series( async.series(
[ [
function openDatabase(callback) { function openDatabase(callback) {
self.db = getTransactionDatabase(new sqlite3.Database( self.db = getTransactionDatabase(new sqlite3.Database(
getModDatabasePath(exports.moduleInfo), getModDatabasePath(exports.moduleInfo),
err => { err => {
return callback(err); return callback(err);
} }
)); ));
}, },
function createTables(callback) { function createTables(callback) {
self.db.run( self.db.run(
`CREATE TABLE IF NOT EXISTS onelinerz ( `CREATE TABLE IF NOT EXISTS onelinerz (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
user_id INTEGER_NOT NULL, user_id INTEGER_NOT NULL,
user_name VARCHAR NOT NULL, user_name VARCHAR NOT NULL,
oneliner VARCHAR NOT NULL, oneliner VARCHAR NOT NULL,
timestamp DATETIME NOT NULL timestamp DATETIME NOT NULL
);` );`
, ,
err => { err => {
return callback(err); return callback(err);
}); });
} }
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
} }
storeNewOneliner(oneliner, cb) { storeNewOneliner(oneliner, cb) {
const self = this; const self = this;
const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ');
async.series( async.series(
[ [
function addRec(callback) { function addRec(callback) {
self.db.run( self.db.run(
`INSERT INTO onelinerz (user_id, user_name, oneliner, timestamp) `INSERT INTO onelinerz (user_id, user_name, oneliner, timestamp)
VALUES (?, ?, ?, ?);`, VALUES (?, ?, ?, ?);`,
[ self.client.user.userId, self.client.user.username, oneliner, ts ], [ self.client.user.userId, self.client.user.username, oneliner, ts ],
callback callback
); );
}, },
function removeOld(callback) { function removeOld(callback) {
// keep 25 max most recent items - remove the older ones // keep 25 max most recent items - remove the older ones
self.db.run( self.db.run(
`DELETE FROM onelinerz `DELETE FROM onelinerz
WHERE id IN ( WHERE id IN (
SELECT id SELECT id
FROM onelinerz FROM onelinerz
ORDER BY id DESC ORDER BY id DESC
LIMIT -1 OFFSET 25 LIMIT -1 OFFSET 25
);`, );`,
callback callback
); );
} }
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
} }
beforeArt(cb) { beforeArt(cb) {
super.beforeArt(err => { super.beforeArt(err => {
return err ? cb(err) : this.initDatabase(cb); return err ? cb(err) : this.initDatabase(cb);
}); });
} }
}; };

View File

@ -16,83 +16,83 @@ exports.getAreaAndStorage = getAreaAndStorage;
exports.looksLikePattern = looksLikePattern; exports.looksLikePattern = looksLikePattern;
const exitCodes = exports.ExitCodes = { const exitCodes = exports.ExitCodes = {
SUCCESS : 0, SUCCESS : 0,
ERROR : -1, ERROR : -1,
BAD_COMMAND : -2, BAD_COMMAND : -2,
BAD_ARGS : -3, BAD_ARGS : -3,
}; };
const argv = exports.argv = require('minimist')(process.argv.slice(2), { const argv = exports.argv = require('minimist')(process.argv.slice(2), {
alias : { alias : {
h : 'help', h : 'help',
v : 'version', v : 'version',
c : 'config', c : 'config',
n : 'no-prompt', n : 'no-prompt',
} }
}); });
function printUsageAndSetExitCode(errMsg, exitCode) { function printUsageAndSetExitCode(errMsg, exitCode) {
if(_.isUndefined(exitCode)) { if(_.isUndefined(exitCode)) {
exitCode = exitCodes.ERROR; exitCode = exitCodes.ERROR;
} }
process.exitCode = exitCode; process.exitCode = exitCode;
if(errMsg) { if(errMsg) {
console.error(errMsg); console.error(errMsg);
} }
} }
function getDefaultConfigPath() { function getDefaultConfigPath() {
return './config/'; return './config/';
} }
function getConfigPath() { function getConfigPath() {
const baseConfigPath = argv.config ? argv.config : config.getDefaultPath(); const baseConfigPath = argv.config ? argv.config : config.getDefaultPath();
return baseConfigPath + 'config.hjson'; return baseConfigPath + 'config.hjson';
} }
function initConfig(cb) { function initConfig(cb) {
const configPath = getConfigPath(); const configPath = getConfigPath();
config.init(configPath, { keepWsc : true, noWatch : true }, cb); config.init(configPath, { keepWsc : true, noWatch : true }, cb);
} }
function initConfigAndDatabases(cb) { function initConfigAndDatabases(cb) {
async.series( async.series(
[ [
function init(callback) { function init(callback) {
initConfig(callback); initConfig(callback);
}, },
function initDb(callback) { function initDb(callback) {
db.initializeDatabases(callback); db.initializeDatabases(callback);
}, },
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
} }
function getAreaAndStorage(tags) { function getAreaAndStorage(tags) {
return tags.map(tag => { return tags.map(tag => {
const parts = tag.toString().split('@'); const parts = tag.toString().split('@');
const entry = { const entry = {
areaTag : parts[0], areaTag : parts[0],
}; };
entry.pattern = entry.areaTag; // handy entry.pattern = entry.areaTag; // handy
if(parts[1]) { if(parts[1]) {
entry.storageTag = parts[1]; entry.storageTag = parts[1];
} }
return entry; return entry;
}); });
} }
function looksLikePattern(tag) { function looksLikePattern(tag) {
// globs can start with @ // globs can start with @
if(tag.indexOf('@') > 0) { if(tag.indexOf('@') > 0) {
return false; return false;
} }
return /[*?[\]!()+|^]/.test(tag); return /[*?[\]!()+|^]/.test(tag);
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ const getDefaultConfigPath = require('./oputil_common.js').getDefaultConfigPat
exports.getHelpFor = getHelpFor; exports.getHelpFor = getHelpFor;
const usageHelp = exports.USAGE_HELP = { const usageHelp = exports.USAGE_HELP = {
General : General :
`usage: optutil.js [--version] [--help] `usage: optutil.js [--version] [--help]
<command> [<args>] <command> [<args>]
@ -21,7 +21,7 @@ commands:
fb file base management fb file base management
mb message base management mb message base management
`, `,
User : User :
`usage: optutil.js user <action> [<args>] `usage: optutil.js user <action> [<args>]
actions: actions:
@ -33,7 +33,7 @@ actions:
group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP
`, `,
Config : Config :
`usage: optutil.js config <action> [<args>] `usage: optutil.js config <action> [<args>]
actions: actions:
@ -46,7 +46,7 @@ import-areas args:
--uplinks UL1,UL2,... specify one or more comma separated uplinks --uplinks UL1,UL2,... specify one or more comma separated uplinks
--type TYPE specifies area import type. valid options are "bbs" and "na" --type TYPE specifies area import type. valid options are "bbs" and "na"
`, `,
FileBase : FileBase :
`usage: oputil.js fb <action> [<args>] `usage: oputil.js fb <action> [<args>]
actions: actions:
@ -80,7 +80,7 @@ info args:
remove args: remove args:
--phys-file also remove underlying physical file --phys-file also remove underlying physical file
`, `,
FileOpsInfo : FileOpsInfo :
` `
general information: general information:
AREA_TAG[@STORAGE_TAG] can specify an area tag and optionally, a storage specific tag AREA_TAG[@STORAGE_TAG] can specify an area tag and optionally, a storage specific tag
@ -90,7 +90,7 @@ general information:
SHA full or partial SHA-256 SHA full or partial SHA-256
FILE_ID a file identifier. see file.sqlite3 FILE_ID a file identifier. see file.sqlite3
`, `,
MessageBase : MessageBase :
`usage: oputil.js mb <action> [<args>] `usage: oputil.js mb <action> [<args>]
actions: actions:
@ -101,5 +101,5 @@ general information:
}; };
function getHelpFor(command) { function getHelpFor(command) {
return usageHelp[command]; return usageHelp[command];
} }

View File

@ -14,23 +14,23 @@ const getHelpFor = require('./oputil_help.js').getHelpFor;
module.exports = function() { module.exports = function() {
process.exitCode = ExitCodes.SUCCESS; process.exitCode = ExitCodes.SUCCESS;
if(true === argv.version) { if(true === argv.version) {
return console.info(require('../package.json').version); return console.info(require('../package.json').version);
} }
if(0 === argv._.length || if(0 === argv._.length ||
'help' === argv._[0]) 'help' === argv._[0])
{ {
return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.SUCCESS); return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.SUCCESS);
} }
switch(argv._[0]) { switch(argv._[0]) {
case 'user' : return handleUserCommand(); case 'user' : return handleUserCommand();
case 'config' : return handleConfigCommand(); case 'config' : return handleConfigCommand();
case 'fb' : return handleFileBaseCommand(); case 'fb' : return handleFileBaseCommand();
case 'mb' : return handleMessageBaseCommand(); case 'mb' : return handleMessageBaseCommand();
default : return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.BAD_COMMAND); default : return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.BAD_COMMAND);
} }
}; };

View File

@ -16,127 +16,127 @@ const async = require('async');
exports.handleMessageBaseCommand = handleMessageBaseCommand; exports.handleMessageBaseCommand = handleMessageBaseCommand;
function areaFix() { function areaFix() {
// //
// oputil mb areafix CMD1 CMD2 ... ADDR [--password PASS] // oputil mb areafix CMD1 CMD2 ... ADDR [--password PASS]
// //
if(argv._.length < 3) { if(argv._.length < 3) {
return printUsageAndSetExitCode( return printUsageAndSetExitCode(
getHelpFor('MessageBase'), getHelpFor('MessageBase'),
ExitCodes.ERROR ExitCodes.ERROR
); );
} }
async.waterfall( async.waterfall(
[ [
function init(callback) { function init(callback) {
return initConfigAndDatabases(callback); return initConfigAndDatabases(callback);
}, },
function validateAddress(callback) { function validateAddress(callback) {
const addrArg = argv._.slice(-1)[0]; const addrArg = argv._.slice(-1)[0];
const ftnAddr = Address.fromString(addrArg); const ftnAddr = Address.fromString(addrArg);
if(!ftnAddr) { if(!ftnAddr) {
return callback(Errors.Invalid(`"${addrArg}" is not a valid FTN address`)); return callback(Errors.Invalid(`"${addrArg}" is not a valid FTN address`));
} }
// //
// We need to validate the address targets a system we know unless // We need to validate the address targets a system we know unless
// the --force option is used // the --force option is used
// //
// :TODO: // :TODO:
return callback(null, ftnAddr); return callback(null, ftnAddr);
}, },
function fetchFromUser(ftnAddr, callback) { function fetchFromUser(ftnAddr, callback) {
// //
// --from USER || +op from system // --from USER || +op from system
// //
// If possible, we want the user ID of the supplied user as well // If possible, we want the user ID of the supplied user as well
// //
const User = require('../user.js'); const User = require('../user.js');
if(argv.from) { if(argv.from) {
User.getUserIdAndNameByLookup(argv.from, (err, userId, fromName) => { User.getUserIdAndNameByLookup(argv.from, (err, userId, fromName) => {
if(err) { if(err) {
return callback(null, ftnAddr, argv.from, 0); return callback(null, ftnAddr, argv.from, 0);
} }
// fromName is the same as argv.from, but case may be differnet (yet correct) // fromName is the same as argv.from, but case may be differnet (yet correct)
return callback(null, ftnAddr, fromName, userId); return callback(null, ftnAddr, fromName, userId);
}); });
} else { } else {
User.getUserName(User.RootUserID, (err, fromName) => { User.getUserName(User.RootUserID, (err, fromName) => {
return callback(null, ftnAddr, fromName || 'SysOp', err ? 0 : User.RootUserID); return callback(null, ftnAddr, fromName || 'SysOp', err ? 0 : User.RootUserID);
}); });
} }
}, },
function createMessage(ftnAddr, fromName, fromUserId, callback) { function createMessage(ftnAddr, fromName, fromUserId, callback) {
// //
// Build message as commands separated by line feed // Build message as commands separated by line feed
// //
// We need to remove quotes from arguments. These are required // We need to remove quotes from arguments. These are required
// in the case of e.g. removing an area: "-SOME_AREA" would end // in the case of e.g. removing an area: "-SOME_AREA" would end
// up confusing minimist, therefor they must be quoted: "'-SOME_AREA'" // up confusing minimist, therefor they must be quoted: "'-SOME_AREA'"
// //
const messageBody = argv._.slice(2, -1).map(arg => { const messageBody = argv._.slice(2, -1).map(arg => {
return arg.replace(/["']/g, ''); return arg.replace(/["']/g, '');
}).join('\r\n') + '\n'; }).join('\r\n') + '\n';
const Message = require('../message.js'); const Message = require('../message.js');
const message = new Message({ const message = new Message({
toUserName : argv.to || 'AreaFix', toUserName : argv.to || 'AreaFix',
fromUserName : fromName, fromUserName : fromName,
subject : argv.password || '', subject : argv.password || '',
message : messageBody, message : messageBody,
areaTag : Message.WellKnownAreaTags.Private, // mark private areaTag : Message.WellKnownAreaTags.Private, // mark private
meta : { meta : {
System : { System : {
[ Message.SystemMetaNames.RemoteToUser ] : ftnAddr.toString(), // where to send it [ Message.SystemMetaNames.RemoteToUser ] : ftnAddr.toString(), // where to send it
[ Message.SystemMetaNames.ExternalFlavor ] : Message.AddressFlavor.FTN, // on FTN-style network [ Message.SystemMetaNames.ExternalFlavor ] : Message.AddressFlavor.FTN, // on FTN-style network
} }
} }
}); });
if(0 !== fromUserId) { if(0 !== fromUserId) {
message.setLocalFromUserId(fromUserId); message.setLocalFromUserId(fromUserId);
} }
return callback(null, message); return callback(null, message);
}, },
function persistMessage(message, callback) { function persistMessage(message, callback) {
message.persist(err => { message.persist(err => {
if(!err) { if(!err) {
console.log('AreaFix message persisted and will be exported at next scheduled scan'); console.log('AreaFix message persisted and will be exported at next scheduled scan');
} }
return callback(err); return callback(err);
}); });
} }
], ],
err => { err => {
if(err) { if(err) {
process.exitCode = ExitCodes.ERROR; process.exitCode = ExitCodes.ERROR;
console.error(`${err.message}${err.reason ? ': ' + err.reason : ''}`); console.error(`${err.message}${err.reason ? ': ' + err.reason : ''}`);
} }
} }
); );
} }
function handleMessageBaseCommand() { function handleMessageBaseCommand() {
function errUsage() { function errUsage() {
return printUsageAndSetExitCode( return printUsageAndSetExitCode(
getHelpFor('MessageBase'), getHelpFor('MessageBase'),
ExitCodes.ERROR ExitCodes.ERROR
); );
} }
if(true === argv.help) { if(true === argv.help) {
return errUsage(); return errUsage();
} }
const action = argv._[1]; const action = argv._[1];
return({ return({
areafix : areaFix, areafix : areaFix,
}[action] || errUsage)(); }[action] || errUsage)();
} }

View File

@ -15,191 +15,191 @@ const _ = require('lodash');
exports.handleUserCommand = handleUserCommand; exports.handleUserCommand = handleUserCommand;
function getUser(userName, cb) { function getUser(userName, cb) {
const User = require('../../core/user.js'); const User = require('../../core/user.js');
User.getUserIdAndName(userName, (err, userId) => { User.getUserIdAndName(userName, (err, userId) => {
if(err) { if(err) {
process.exitCode = ExitCodes.BAD_ARGS; process.exitCode = ExitCodes.BAD_ARGS;
return cb(err); return cb(err);
} }
const u = new User(); const u = new User();
u.userId = userId; u.userId = userId;
return cb(null, u); return cb(null, u);
}); });
} }
function initAndGetUser(userName, cb) { function initAndGetUser(userName, cb) {
async.waterfall( async.waterfall(
[ [
function init(callback) { function init(callback) {
initConfigAndDatabases(callback); initConfigAndDatabases(callback);
}, },
function getUserObject(callback) { function getUserObject(callback) {
getUser(userName, (err, user) => { getUser(userName, (err, user) => {
if(err) { if(err) {
process.exitCode = ExitCodes.BAD_ARGS; process.exitCode = ExitCodes.BAD_ARGS;
return callback(err); return callback(err);
} }
return callback(null, user); return callback(null, user);
}); });
} }
], ],
(err, user) => { (err, user) => {
return cb(err, user); return cb(err, user);
} }
); );
} }
function setAccountStatus(user, status) { function setAccountStatus(user, status) {
if(argv._.length < 3) { if(argv._.length < 3) {
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
} }
const AccountStatus = require('../../core/user.js').AccountStatus; const AccountStatus = require('../../core/user.js').AccountStatus;
const statusDesc = _.invert(AccountStatus)[status]; const statusDesc = _.invert(AccountStatus)[status];
user.persistProperty('account_status', status, err => { user.persistProperty('account_status', status, err => {
if(err) { if(err) {
process.exitCode = ExitCodes.ERROR; process.exitCode = ExitCodes.ERROR;
console.error(err.message); console.error(err.message);
} else { } else {
console.info(`User status set to ${statusDesc}`); console.info(`User status set to ${statusDesc}`);
} }
}); });
} }
function setUserPassword(user) { function setUserPassword(user) {
if(argv._.length < 4) { if(argv._.length < 4) {
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
} }
async.waterfall( async.waterfall(
[ [
function validate(callback) { function validate(callback) {
// :TODO: prompt if no password provided (more secure, no history, etc.) // :TODO: prompt if no password provided (more secure, no history, etc.)
const password = argv._[argv._.length - 1]; const password = argv._[argv._.length - 1];
if(0 === password.length) { if(0 === password.length) {
return callback(Errors.Invalid('Invalid password')); return callback(Errors.Invalid('Invalid password'));
} }
return callback(null, password); return callback(null, password);
}, },
function set(password, callback) { function set(password, callback) {
user.setNewAuthCredentials(password, err => { user.setNewAuthCredentials(password, err => {
if(err) { if(err) {
process.exitCode = ExitCodes.BAD_ARGS; process.exitCode = ExitCodes.BAD_ARGS;
} }
return callback(err); return callback(err);
}); });
} }
], ],
err => { err => {
if(err) { if(err) {
console.error(err.message); console.error(err.message);
} else { } else {
console.info('New password set'); console.info('New password set');
} }
} }
); );
} }
function removeUser(user) { function removeUser() {
console.error('NOT YET IMPLEMENTED'); console.error('NOT YET IMPLEMENTED');
} }
function modUserGroups(user) { function modUserGroups(user) {
if(argv._.length < 3) { if(argv._.length < 3) {
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
} }
let groupName = argv._[argv._.length - 1].toString().replace(/["']/g, ''); // remove any quotes - necessary to allow "-foo" let groupName = argv._[argv._.length - 1].toString().replace(/["']/g, ''); // remove any quotes - necessary to allow "-foo"
let action = groupName[0]; // + or - let action = groupName[0]; // + or -
if('-' === action || '+' === action) { if('-' === action || '+' === action) {
groupName = groupName.substr(1); groupName = groupName.substr(1);
} }
action = action || '+'; action = action || '+';
if(0 === groupName.length) { if(0 === groupName.length) {
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
} }
// //
// Groups are currently arbritary, so do a slight validation // Groups are currently arbritary, so do a slight validation
// //
if(!/[A-Za-z0-9]+/.test(groupName)) { if(!/[A-Za-z0-9]+/.test(groupName)) {
process.exitCode = ExitCodes.BAD_ARGS; process.exitCode = ExitCodes.BAD_ARGS;
return console.error('Bad group name'); return console.error('Bad group name');
} }
function done(err) { function done(err) {
if(err) { if(err) {
process.exitCode = ExitCodes.BAD_ARGS; process.exitCode = ExitCodes.BAD_ARGS;
console.error(err.message); console.error(err.message);
} else { } else {
console.info('User groups modified'); console.info('User groups modified');
} }
} }
const UserGroup = require('../../core/user_group.js'); const UserGroup = require('../../core/user_group.js');
if('-' === action) { if('-' === action) {
UserGroup.removeUserFromGroup(user.userId, groupName, done); UserGroup.removeUserFromGroup(user.userId, groupName, done);
} else { } else {
UserGroup.addUserToGroup(user.userId, groupName, done); UserGroup.addUserToGroup(user.userId, groupName, done);
} }
} }
function activateUser(user) { function activateUser(user) {
const AccountStatus = require('../../core/user.js').AccountStatus; const AccountStatus = require('../../core/user.js').AccountStatus;
return setAccountStatus(user, AccountStatus.active); return setAccountStatus(user, AccountStatus.active);
} }
function deactivateUser(user) { function deactivateUser(user) {
const AccountStatus = require('../../core/user.js').AccountStatus; const AccountStatus = require('../../core/user.js').AccountStatus;
return setAccountStatus(user, AccountStatus.inactive); return setAccountStatus(user, AccountStatus.inactive);
} }
function disableUser(user) { function disableUser(user) {
const AccountStatus = require('../../core/user.js').AccountStatus; const AccountStatus = require('../../core/user.js').AccountStatus;
return setAccountStatus(user, AccountStatus.disabled); return setAccountStatus(user, AccountStatus.disabled);
} }
function handleUserCommand() { function handleUserCommand() {
function errUsage() { function errUsage() {
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
} }
if(true === argv.help) { if(true === argv.help) {
return errUsage(); return errUsage();
} }
const action = argv._[1]; const action = argv._[1];
const usernameIdx = [ 'pass', 'passwd', 'password', 'group' ].includes(action) ? argv._.length - 2 : argv._.length - 1; const usernameIdx = [ 'pass', 'passwd', 'password', 'group' ].includes(action) ? argv._.length - 2 : argv._.length - 1;
const userName = argv._[usernameIdx]; const userName = argv._[usernameIdx];
if(!userName) { if(!userName) {
return errUsage(); return errUsage();
} }
initAndGetUser(userName, (err, user) => { initAndGetUser(userName, (err, user) => {
if(err) { if(err) {
process.exitCode = ExitCodes.ERROR; process.exitCode = ExitCodes.ERROR;
return console.error(err.message); return console.error(err.message);
} }
return ({ return ({
pass : setUserPassword, pass : setUserPassword,
passwd : setUserPassword, passwd : setUserPassword,
password : setUserPassword, password : setUserPassword,
rm : removeUser, rm : removeUser,
remove : removeUser, remove : removeUser,
del : removeUser, del : removeUser,
delete : removeUser, delete : removeUser,
activate : activateUser, activate : activateUser,
deactivate : deactivateUser, deactivate : deactivateUser,
disable : disableUser, disable : disableUser,
group : modUserGroups, group : modUserGroups,
}[action] || errUsage)(user); }[action] || errUsage)(user);
}); });
} }

View File

@ -21,230 +21,230 @@ exports.getPredefinedMCIValue = getPredefinedMCIValue;
exports.init = init; exports.init = init;
function init(cb) { function init(cb) {
setNextRandomRumor(cb); setNextRandomRumor(cb);
} }
function setNextRandomRumor(cb) { function setNextRandomRumor(cb) {
StatLog.getSystemLogEntries('system_rumorz', StatLog.Order.Random, 1, (err, entry) => { StatLog.getSystemLogEntries('system_rumorz', StatLog.Order.Random, 1, (err, entry) => {
if(entry) { if(entry) {
entry = entry[0]; entry = entry[0];
} }
const randRumor = entry && entry.log_value ? entry.log_value : ''; const randRumor = entry && entry.log_value ? entry.log_value : '';
StatLog.setNonPeristentSystemStat('random_rumor', randRumor); StatLog.setNonPeristentSystemStat('random_rumor', randRumor);
if(cb) { if(cb) {
return cb(null); return cb(null);
} }
}); });
} }
function getUserRatio(client, propA, propB) { function getUserRatio(client, propA, propB) {
const a = StatLog.getUserStatNum(client.user, propA); const a = StatLog.getUserStatNum(client.user, propA);
const b = StatLog.getUserStatNum(client.user, propB); const b = StatLog.getUserStatNum(client.user, propB);
const ratio = ~~((a / b) * 100); const ratio = ~~((a / b) * 100);
return `${ratio}%`; return `${ratio}%`;
} }
function userStatAsString(client, statName, defaultValue) { function userStatAsString(client, statName, defaultValue) {
return (StatLog.getUserStat(client.user, statName) || defaultValue).toLocaleString(); return (StatLog.getUserStat(client.user, statName) || defaultValue).toLocaleString();
} }
function sysStatAsString(statName, defaultValue) { function sysStatAsString(statName, defaultValue) {
return (StatLog.getSystemStat(statName) || defaultValue).toLocaleString(); return (StatLog.getSystemStat(statName) || defaultValue).toLocaleString();
} }
const PREDEFINED_MCI_GENERATORS = { const PREDEFINED_MCI_GENERATORS = {
// //
// Board // Board
// //
BN : function boardName() { return Config().general.boardName; }, BN : function boardName() { return Config().general.boardName; },
// ENiGMA // ENiGMA
VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; }, VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; },
VN : function version() { return packageJson.version; }, VN : function version() { return packageJson.version; },
// +op info // +op info
SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); }, SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); },
SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); }, SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); },
SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); }, SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); },
SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); }, SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); },
SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); }, SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); },
SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); }, SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); },
// :TODO: op age, web, ????? // :TODO: op age, web, ?????
// //
// Current user / session // Current user / session
// //
UN : function userName(client) { return client.user.username; }, UN : function userName(client) { return client.user.username; },
UI : function userId(client) { return client.user.userId.toString(); }, UI : function userId(client) { return client.user.userId.toString(); },
UG : function groups(client) { return _.values(client.user.groups).join(', '); }, UG : function groups(client) { return _.values(client.user.groups).join(', '); },
UR : function realName(client) { return userStatAsString(client, 'real_name', ''); }, UR : function realName(client) { return userStatAsString(client, 'real_name', ''); },
LO : function location(client) { return userStatAsString(client, 'location', ''); }, LO : function location(client) { return userStatAsString(client, 'location', ''); },
UA : function age(client) { return client.user.getAge().toString(); }, UA : function age(client) { return client.user.getAge().toString(); },
BD : function birthdate(client) { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, // iNiQUiTY BD : function birthdate(client) { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, // iNiQUiTY
US : function sex(client) { return userStatAsString(client, 'sex', ''); }, US : function sex(client) { return userStatAsString(client, 'sex', ''); },
UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); }, UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); },
UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); }, UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); },
UF : function affils(client) { return userStatAsString(client, 'affiliation', ''); }, UF : function affils(client) { return userStatAsString(client, 'affiliation', ''); },
UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); }, UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); },
UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); }, UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); },
ND : function connectedNode(client) { return client.node.toString(); }, ND : function connectedNode(client) { return client.node.toString(); },
IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version
ST : function serverName(client) { return client.session.serverName; }, ST : function serverName(client) { return client.session.serverName; },
FN : function activeFileBaseFilterName(client) { FN : function activeFileBaseFilterName(client) {
const activeFilter = FileBaseFilters.getActiveFilter(client); const activeFilter = FileBaseFilters.getActiveFilter(client);
return activeFilter ? activeFilter.name : ''; return activeFilter ? activeFilter.name : '';
}, },
DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2
DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes
const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes'); const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr return formatByteSize(byteSize, true); // true=withAbbr
}, },
UP : function userNumUploads(client) { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2 UP : function userNumUploads(client) { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2
UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes
const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes'); const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr return formatByteSize(byteSize, true); // true=withAbbr
}, },
NR : function userUpDownRatio(client) { // Obv/2 NR : function userUpDownRatio(client) { // Obv/2
return getUserRatio(client, 'ul_total_count', 'dl_total_count'); return getUserRatio(client, 'ul_total_count', 'dl_total_count');
}, },
KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio
return getUserRatio(client, 'ul_total_bytes', 'dl_total_bytes'); return getUserRatio(client, 'ul_total_bytes', 'dl_total_bytes');
}, },
MS : function accountCreatedclient(client) { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, MS : function accountCreatedclient(client) { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); },
PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); }, PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); },
PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); }, PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); },
MD : function currentMenuDescription(client) { MD : function currentMenuDescription(client) {
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
}, },
MA : function messageAreaName(client) { MA : function messageAreaName(client) {
const area = getMessageAreaByTag(client.user.properties.message_area_tag); const area = getMessageAreaByTag(client.user.properties.message_area_tag);
return area ? area.name : ''; return area ? area.name : '';
}, },
MC : function messageConfName(client) { MC : function messageConfName(client) {
const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag);
return conf ? conf.name : ''; return conf ? conf.name : '';
}, },
ML : function messageAreaDescription(client) { ML : function messageAreaDescription(client) {
const area = getMessageAreaByTag(client.user.properties.message_area_tag); const area = getMessageAreaByTag(client.user.properties.message_area_tag);
return area ? area.desc : ''; return area ? area.desc : '';
}, },
CM : function messageConfDescription(client) { CM : function messageConfDescription(client) {
const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag);
return conf ? conf.desc : ''; return conf ? conf.desc : '';
}, },
SH : function termHeight(client) { return client.term.termHeight.toString(); }, SH : function termHeight(client) { return client.term.termHeight.toString(); },
SW : function termWidth(client) { return client.term.termWidth.toString(); }, SW : function termWidth(client) { return client.term.termWidth.toString(); },
// //
// Date/Time // Date/Time
// //
// :TODO: change to CD for 'Current Date' // :TODO: change to CD for 'Current Date'
DT : function date(client) { return moment().format(client.currentTheme.helpers.getDateFormat()); }, DT : function date(client) { return moment().format(client.currentTheme.helpers.getDateFormat()); },
CT : function time(client) { return moment().format(client.currentTheme.helpers.getTimeFormat()) ;}, CT : function time(client) { return moment().format(client.currentTheme.helpers.getTimeFormat()) ;},
// //
// OS/System Info // OS/System Info
// //
OS : function operatingSystem() { OS : function operatingSystem() {
return { return {
linux : 'Linux', linux : 'Linux',
darwin : 'Mac OS X', darwin : 'Mac OS X',
win32 : 'Windows', win32 : 'Windows',
sunos : 'SunOS', sunos : 'SunOS',
freebsd : 'FreeBSD', freebsd : 'FreeBSD',
}[os.platform()] || os.type(); }[os.platform()] || os.type();
}, },
OA : function systemArchitecture() { return os.arch(); }, OA : function systemArchitecture() { return os.arch(); },
SC : function systemCpuModel() { SC : function systemCpuModel() {
// //
// Clean up CPU strings a bit for better display // Clean up CPU strings a bit for better display
// //
return os.cpus()[0].model return os.cpus()[0].model
.replace(/\(R\)|\(TM\)|processor|CPU/g, '') .replace(/\(R\)|\(TM\)|processor|CPU/g, '')
.replace(/\s+(?= )/g, ''); .replace(/\s+(?= )/g, '');
}, },
// :TODO: MCI for core count, e.g. os.cpus().length // :TODO: MCI for core count, e.g. os.cpus().length
// :TODO: cpu load average (over N seconds): http://stackoverflow.com/questions/9565912/convert-the-output-of-os-cpus-in-node-js-to-percentage // :TODO: cpu load average (over N seconds): http://stackoverflow.com/questions/9565912/convert-the-output-of-os-cpus-in-node-js-to-percentage
NV : function nodeVersion() { return process.version; }, NV : function nodeVersion() { return process.version; },
AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); },
TC : function totalCalls() { return StatLog.getSystemStat('login_count').toLocaleString(); }, TC : function totalCalls() { return StatLog.getSystemStat('login_count').toLocaleString(); },
RR : function randomRumor() { RR : function randomRumor() {
// start the process of picking another random one // start the process of picking another random one
setNextRandomRumor(); setNextRandomRumor();
return StatLog.getSystemStat('random_rumor'); return StatLog.getSystemStat('random_rumor');
}, },
// //
// System File Base, Up/Download Info // System File Base, Up/Download Info
// //
// :TODO: DD - Today's # of downloads (iNiQUiTY) // :TODO: DD - Today's # of downloads (iNiQUiTY)
// //
SD : function systemNumDownloads() { return sysStatAsString('dl_total_count', 0); }, SD : function systemNumDownloads() { return sysStatAsString('dl_total_count', 0); },
SO : function systemByteDownload() { SO : function systemByteDownload() {
const byteSize = StatLog.getSystemStatNum('dl_total_bytes'); const byteSize = StatLog.getSystemStatNum('dl_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr return formatByteSize(byteSize, true); // true=withAbbr
}, },
SU : function systemNumUploads() { return sysStatAsString('ul_total_count', 0); }, SU : function systemNumUploads() { return sysStatAsString('ul_total_count', 0); },
SP : function systemByteUpload() { SP : function systemByteUpload() {
const byteSize = StatLog.getSystemStatNum('ul_total_bytes'); const byteSize = StatLog.getSystemStatNum('ul_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr return formatByteSize(byteSize, true); // true=withAbbr
}, },
TF : function totalFilesOnSystem() { TF : function totalFilesOnSystem() {
const areaStats = StatLog.getSystemStat('file_base_area_stats'); const areaStats = StatLog.getSystemStat('file_base_area_stats');
return _.get(areaStats, 'totalFiles', 0).toLocaleString(); return _.get(areaStats, 'totalFiles', 0).toLocaleString();
}, },
TB : function totalBytesOnSystem() { TB : function totalBytesOnSystem() {
const areaStats = StatLog.getSystemStat('file_base_area_stats'); const areaStats = StatLog.getSystemStat('file_base_area_stats');
const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0)); const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0));
return formatByteSize(totalBytes, true); // true=withAbbr return formatByteSize(totalBytes, true); // true=withAbbr
}, },
// :TODO: PT - Messages posted *today* (Obv/2) // :TODO: PT - Messages posted *today* (Obv/2)
// -> Include FTN/etc. // -> Include FTN/etc.
// :TODO: NT - New users today (Obv/2) // :TODO: NT - New users today (Obv/2)
// :TODO: CT - Calls *today* (Obv/2) // :TODO: CT - Calls *today* (Obv/2)
// :TODO: FT - Files uploaded/added *today* (Obv/2) // :TODO: FT - Files uploaded/added *today* (Obv/2)
// :TODO: DD - Files downloaded *today* (iNiQUiTY) // :TODO: DD - Files downloaded *today* (iNiQUiTY)
// :TODO: TP - total message/posts on the system (Obv/2) // :TODO: TP - total message/posts on the system (Obv/2)
// -> Include FTN/etc. // -> Include FTN/etc.
// :TODO: LC - name of last caller to system (Obv/2) // :TODO: LC - name of last caller to system (Obv/2)
// :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY)
// //
// Special handling for XY // Special handling for XY
// //
XY : function xyHack() { return; /* nothing */ }, XY : function xyHack() { return; /* nothing */ },
}; };
function getPredefinedMCIValue(client, code) { function getPredefinedMCIValue(client, code) {
if(!client || !code) { if(!client || !code) {
return; return;
} }
const generator = PREDEFINED_MCI_GENERATORS[code]; const generator = PREDEFINED_MCI_GENERATORS[code];
if(generator) { if(generator) {
let value; let value;
try { try {
value = generator(client); value = generator(client);
} catch(e) { } catch(e) {
Log.error( { code : code, exception : e.message }, 'Exception caught generating predefined MCI value' ); Log.error( { code : code, exception : e.message }, 'Exception caught generating predefined MCI value' );
} }
return value; return value;
} }
} }

View File

@ -15,233 +15,233 @@ const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Rumorz', name : 'Rumorz',
desc : 'Standard local rumorz', desc : 'Standard local rumorz',
author : 'NuSkooler', author : 'NuSkooler',
packageName : 'codes.l33t.enigma.rumorz', packageName : 'codes.l33t.enigma.rumorz',
}; };
const STATLOG_KEY_RUMORZ = 'system_rumorz'; const STATLOG_KEY_RUMORZ = 'system_rumorz';
const FormIds = { const FormIds = {
View : 0, View : 0,
Add : 1, Add : 1,
}; };
const MciCodeIds = { const MciCodeIds = {
ViewForm : { ViewForm : {
Entries : 1, Entries : 1,
AddPrompt : 2, AddPrompt : 2,
}, },
AddForm : { AddForm : {
NewEntry : 1, NewEntry : 1,
EntryPreview : 2, EntryPreview : 2,
AddPrompt : 3, AddPrompt : 3,
} }
}; };
exports.getModule = class RumorzModule extends MenuModule { exports.getModule = class RumorzModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.menuMethods = { this.menuMethods = {
viewAddScreen : (formData, extraArgs, cb) => { viewAddScreen : (formData, extraArgs, cb) => {
return this.displayAddScreen(cb); return this.displayAddScreen(cb);
}, },
addEntry : (formData, extraArgs, cb) => { addEntry : (formData, extraArgs, cb) => {
if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) { if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) {
const rumor = formData.value.rumor.trim(); // remove any trailing ws const rumor = formData.value.rumor.trim(); // remove any trailing ws
StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => { StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => {
this.clearAddForm(); this.clearAddForm();
return this.displayViewScreen(true, cb); // true=cls return this.displayViewScreen(true, cb); // true=cls
}); });
} else { } else {
// empty message - treat as if cancel was hit // empty message - treat as if cancel was hit
return this.displayViewScreen(true, cb); // true=cls return this.displayViewScreen(true, cb); // true=cls
} }
}, },
cancelAdd : (formData, extraArgs, cb) => { cancelAdd : (formData, extraArgs, cb) => {
this.clearAddForm(); this.clearAddForm();
return this.displayViewScreen(true, cb); // true=cls return this.displayViewScreen(true, cb); // true=cls
} }
}; };
} }
get config() { return this.menuConfig.config; } get config() { return this.menuConfig.config; }
clearAddForm() { clearAddForm() {
const newEntryView = this.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); const newEntryView = this.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
newEntryView.setText(''); newEntryView.setText('');
// preview is optional // preview is optional
if(previewView) { if(previewView) {
previewView.setText(''); previewView.setText('');
} }
} }
initSequence() { initSequence() {
const self = this; const self = this;
async.series( async.series(
[ [
function beforeDisplayArt(callback) { function beforeDisplayArt(callback) {
self.beforeArt(callback); self.beforeArt(callback);
}, },
function display(callback) { function display(callback) {
self.displayViewScreen(false, callback); self.displayViewScreen(false, callback);
} }
], ],
err => { err => {
if(err) { if(err) {
// :TODO: Handle me -- initSequence() should really take a completion callback // :TODO: Handle me -- initSequence() should really take a completion callback
} }
self.finishedLoading(); self.finishedLoading();
} }
); );
} }
displayViewScreen(clearScreen, cb) { displayViewScreen(clearScreen, cb) {
const self = this; const self = this;
async.waterfall( async.waterfall(
[ [
function clearAndDisplayArt(callback) { function clearAndDisplayArt(callback) {
if(self.viewControllers.add) { if(self.viewControllers.add) {
self.viewControllers.add.setFocus(false); self.viewControllers.add.setFocus(false);
} }
if(clearScreen) { if(clearScreen) {
self.client.term.rawWrite(resetScreen()); self.client.term.rawWrite(resetScreen());
} }
theme.displayThemedAsset( theme.displayThemedAsset(
self.config.art.entries, self.config.art.entries,
self.client, self.client,
{ font : self.menuConfig.font, trailingLF : false }, { font : self.menuConfig.font, trailingLF : false },
(err, artData) => { (err, artData) => {
return callback(err, artData); return callback(err, artData);
} }
); );
}, },
function initOrRedrawViewController(artData, callback) { function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) { if(_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController( const vc = self.addViewController(
'view', 'view',
new ViewController( { client : self.client, formId : FormIds.View } ) new ViewController( { client : self.client, formId : FormIds.View } )
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds.View, formId : FormIds.View,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
} else { } else {
self.viewControllers.view.setFocus(true); self.viewControllers.view.setFocus(true);
self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw(); self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw();
return callback(null); return callback(null);
} }
}, },
function fetchEntries(callback) { function fetchEntries(callback) {
const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries); const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries);
StatLog.getSystemLogEntries(STATLOG_KEY_RUMORZ, StatLog.Order.Timestamp, (err, entries) => { StatLog.getSystemLogEntries(STATLOG_KEY_RUMORZ, StatLog.Order.Timestamp, (err, entries) => {
return callback(err, entriesView, entries); return callback(err, entriesView, entries);
}); });
}, },
function populateEntries(entriesView, entries, callback) { function populateEntries(entriesView, entries, callback) {
const config = self.config; const config = self.config;
const listFormat = config.listFormat || '{rumor}'; const listFormat = config.listFormat || '{rumor}';
const focusListFormat = config.focusListFormat || listFormat; const focusListFormat = config.focusListFormat || listFormat;
entriesView.setItems(entries.map( e => stringFormat(listFormat, { rumor : e.log_value } ) ) ); entriesView.setItems(entries.map( e => stringFormat(listFormat, { rumor : e.log_value } ) ) );
entriesView.setFocusItems(entries.map(e => stringFormat(focusListFormat, { rumor : e.log_value } ) ) ); entriesView.setFocusItems(entries.map(e => stringFormat(focusListFormat, { rumor : e.log_value } ) ) );
entriesView.redraw(); entriesView.redraw();
return callback(null); return callback(null);
}, },
function finalPrep(callback) { function finalPrep(callback) {
const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt); const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt);
promptView.setFocusItemIndex(1); // default to NO promptView.setFocusItemIndex(1); // default to NO
return callback(null); return callback(null);
} }
], ],
err => { err => {
if(cb) { if(cb) {
return cb(err); return cb(err);
} }
} }
); );
} }
displayAddScreen(cb) { displayAddScreen(cb) {
const self = this; const self = this;
async.waterfall( async.waterfall(
[ [
function clearAndDisplayArt(callback) { function clearAndDisplayArt(callback) {
self.viewControllers.view.setFocus(false); self.viewControllers.view.setFocus(false);
self.client.term.rawWrite(resetScreen()); self.client.term.rawWrite(resetScreen());
theme.displayThemedAsset( theme.displayThemedAsset(
self.config.art.add, self.config.art.add,
self.client, self.client,
{ font : self.menuConfig.font }, { font : self.menuConfig.font },
(err, artData) => { (err, artData) => {
return callback(err, artData); return callback(err, artData);
} }
); );
}, },
function initOrRedrawViewController(artData, callback) { function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) { if(_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController( const vc = self.addViewController(
'add', 'add',
new ViewController( { client : self.client, formId : FormIds.Add } ) new ViewController( { client : self.client, formId : FormIds.Add } )
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds.Add, formId : FormIds.Add,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
} else { } else {
self.viewControllers.add.setFocus(true); self.viewControllers.add.setFocus(true);
self.viewControllers.add.redrawAll(); self.viewControllers.add.redrawAll();
self.viewControllers.add.switchFocus(MciCodeIds.AddForm.NewEntry); self.viewControllers.add.switchFocus(MciCodeIds.AddForm.NewEntry);
return callback(null); return callback(null);
} }
}, },
function initPreviewUpdates(callback) { function initPreviewUpdates(callback) {
const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
if(previewView) { if(previewView) {
let timerId; let timerId;
entryView.on('key press', () => { entryView.on('key press', () => {
clearTimeout(timerId); clearTimeout(timerId);
timerId = setTimeout( () => { timerId = setTimeout( () => {
const focused = self.viewControllers.add.getFocusedView(); const focused = self.viewControllers.add.getFocusedView();
if(focused === entryView) { if(focused === entryView) {
previewView.setText(entryView.getData()); previewView.setText(entryView.getData());
focused.setFocus(true); focused.setFocus(true);
} }
}, 500); }, 500);
}); });
} }
return callback(null); return callback(null);
} }
], ],
err => { err => {
if(cb) { if(cb) {
return cb(err); return cb(err);
} }
} }
); );
} }
}; };

View File

@ -27,100 +27,100 @@ exports.SAUCE_SIZE = SAUCE_SIZE;
const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ]; const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ];
function readSAUCE(data, cb) { function readSAUCE(data, cb) {
if(data.length < SAUCE_SIZE) { if(data.length < SAUCE_SIZE) {
return cb(Errors.DoesNotExist('No SAUCE record present')); return cb(Errors.DoesNotExist('No SAUCE record present'));
} }
let sauceRec; let sauceRec;
try { try {
sauceRec = new Parser() sauceRec = new Parser()
.buffer('id', { length : 5 } ) .buffer('id', { length : 5 } )
.buffer('version', { length : 2 } ) .buffer('version', { length : 2 } )
.buffer('title', { length: 35 } ) .buffer('title', { length: 35 } )
.buffer('author', { length : 20 } ) .buffer('author', { length : 20 } )
.buffer('group', { length: 20 } ) .buffer('group', { length: 20 } )
.buffer('date', { length: 8 } ) .buffer('date', { length: 8 } )
.uint32le('fileSize') .uint32le('fileSize')
.int8('dataType') .int8('dataType')
.int8('fileType') .int8('fileType')
.uint16le('tinfo1') .uint16le('tinfo1')
.uint16le('tinfo2') .uint16le('tinfo2')
.uint16le('tinfo3') .uint16le('tinfo3')
.uint16le('tinfo4') .uint16le('tinfo4')
.int8('numComments') .int8('numComments')
.int8('flags') .int8('flags')
// :TODO: does this need to be optional? // :TODO: does this need to be optional?
.buffer('tinfos', { length: 22 } ) // SAUCE 00.5 .buffer('tinfos', { length: 22 } ) // SAUCE 00.5
.parse(data.slice(data.length - SAUCE_SIZE)); .parse(data.slice(data.length - SAUCE_SIZE));
} catch(e) { } catch(e) {
return cb(Errors.Invalid('Invalid SAUCE record')); return cb(Errors.Invalid('Invalid SAUCE record'));
} }
if(!SAUCE_ID.equals(sauceRec.id)) { if(!SAUCE_ID.equals(sauceRec.id)) {
return cb(Errors.DoesNotExist('No SAUCE record present')); return cb(Errors.DoesNotExist('No SAUCE record present'));
} }
const ver = iconv.decode(sauceRec.version, 'cp437'); const ver = iconv.decode(sauceRec.version, 'cp437');
if('00' !== ver) { if('00' !== ver) {
return cb(Errors.Invalid(`Unsupported SAUCE version: ${ver}`)); return cb(Errors.Invalid(`Unsupported SAUCE version: ${ver}`));
} }
if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(sauceRec.dataType)) { if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(sauceRec.dataType)) {
return cb(Errors.Invalid(`Unsupported SAUCE DataType: ${sauceRec.dataType}`)); return cb(Errors.Invalid(`Unsupported SAUCE DataType: ${sauceRec.dataType}`));
} }
const sauce = { const sauce = {
id : iconv.decode(sauceRec.id, 'cp437'), id : iconv.decode(sauceRec.id, 'cp437'),
version : iconv.decode(sauceRec.version, 'cp437').trim(), version : iconv.decode(sauceRec.version, 'cp437').trim(),
title : iconv.decode(sauceRec.title, 'cp437').trim(), title : iconv.decode(sauceRec.title, 'cp437').trim(),
author : iconv.decode(sauceRec.author, 'cp437').trim(), author : iconv.decode(sauceRec.author, 'cp437').trim(),
group : iconv.decode(sauceRec.group, 'cp437').trim(), group : iconv.decode(sauceRec.group, 'cp437').trim(),
date : iconv.decode(sauceRec.date, 'cp437').trim(), date : iconv.decode(sauceRec.date, 'cp437').trim(),
fileSize : sauceRec.fileSize, fileSize : sauceRec.fileSize,
dataType : sauceRec.dataType, dataType : sauceRec.dataType,
fileType : sauceRec.fileType, fileType : sauceRec.fileType,
tinfo1 : sauceRec.tinfo1, tinfo1 : sauceRec.tinfo1,
tinfo2 : sauceRec.tinfo2, tinfo2 : sauceRec.tinfo2,
tinfo3 : sauceRec.tinfo3, tinfo3 : sauceRec.tinfo3,
tinfo4 : sauceRec.tinfo4, tinfo4 : sauceRec.tinfo4,
numComments : sauceRec.numComments, numComments : sauceRec.numComments,
flags : sauceRec.flags, flags : sauceRec.flags,
tinfos : sauceRec.tinfos, tinfos : sauceRec.tinfos,
}; };
const dt = SAUCE_DATA_TYPES[sauce.dataType]; const dt = SAUCE_DATA_TYPES[sauce.dataType];
if(dt && dt.parser) { if(dt && dt.parser) {
sauce[dt.name] = dt.parser(sauce); sauce[dt.name] = dt.parser(sauce);
} }
return cb(null, sauce); return cb(null, sauce);
} }
// :TODO: These need completed: // :TODO: These need completed:
const SAUCE_DATA_TYPES = { const SAUCE_DATA_TYPES = {
0 : { name : 'None' }, 0 : { name : 'None' },
1 : { name : 'Character', parser : parseCharacterSAUCE }, 1 : { name : 'Character', parser : parseCharacterSAUCE },
2 : 'Bitmap', 2 : 'Bitmap',
3 : 'Vector', 3 : 'Vector',
4 : 'Audio', 4 : 'Audio',
5 : 'BinaryText', 5 : 'BinaryText',
6 : 'XBin', 6 : 'XBin',
7 : 'Archive', 7 : 'Archive',
8 : 'Executable', 8 : 'Executable',
}; };
const SAUCE_CHARACTER_FILE_TYPES = { const SAUCE_CHARACTER_FILE_TYPES = {
0 : 'ASCII', 0 : 'ASCII',
1 : 'ANSi', 1 : 'ANSi',
2 : 'ANSiMation', 2 : 'ANSiMation',
3 : 'RIP script', 3 : 'RIP script',
4 : 'PCBoard', 4 : 'PCBoard',
5 : 'Avatar', 5 : 'Avatar',
6 : 'HTML', 6 : 'HTML',
7 : 'Source', 7 : 'Source',
8 : 'TundraDraw', 8 : 'TundraDraw',
}; };
// //
@ -129,53 +129,53 @@ const SAUCE_CHARACTER_FILE_TYPES = {
// Note that this is the same mapping that x84 uses. Be compatible! // Note that this is the same mapping that x84 uses. Be compatible!
// //
const SAUCE_FONT_TO_ENCODING_HINT = { const SAUCE_FONT_TO_ENCODING_HINT = {
'Amiga MicroKnight' : 'amiga', 'Amiga MicroKnight' : 'amiga',
'Amiga MicroKnight+' : 'amiga', 'Amiga MicroKnight+' : 'amiga',
'Amiga mOsOul' : 'amiga', 'Amiga mOsOul' : 'amiga',
'Amiga P0T-NOoDLE' : 'amiga', 'Amiga P0T-NOoDLE' : 'amiga',
'Amiga Topaz 1' : 'amiga', 'Amiga Topaz 1' : 'amiga',
'Amiga Topaz 1+' : 'amiga', 'Amiga Topaz 1+' : 'amiga',
'Amiga Topaz 2' : 'amiga', 'Amiga Topaz 2' : 'amiga',
'Amiga Topaz 2+' : 'amiga', 'Amiga Topaz 2+' : 'amiga',
'Atari ATASCII' : 'atari', 'Atari ATASCII' : 'atari',
'IBM EGA43' : 'cp437', 'IBM EGA43' : 'cp437',
'IBM EGA' : 'cp437', 'IBM EGA' : 'cp437',
'IBM VGA25G' : 'cp437', 'IBM VGA25G' : 'cp437',
'IBM VGA50' : 'cp437', 'IBM VGA50' : 'cp437',
'IBM VGA' : 'cp437', 'IBM VGA' : 'cp437',
}; };
[ [
'437', '720', '737', '775', '819', '850', '852', '855', '857', '858', '437', '720', '737', '775', '819', '850', '852', '855', '857', '858',
'860', '861', '862', '863', '864', '865', '866', '869', '872' '860', '861', '862', '863', '864', '865', '866', '869', '872'
].forEach( page => { ].forEach( page => {
const codec = 'cp' + page; const codec = 'cp' + page;
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec;
}); });
function parseCharacterSAUCE(sauce) { function parseCharacterSAUCE(sauce) {
const result = {}; const result = {};
result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown'; result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown';
if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) { if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) {
// convience: create ansiFlags // convience: create ansiFlags
sauce.ansiFlags = sauce.flags; sauce.ansiFlags = sauce.flags;
let i = 0; let i = 0;
while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) { while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) {
++i; ++i;
} }
const fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437'); const fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437');
if(fontName.length > 0) { if(fontName.length > 0) {
result.fontName = fontName; result.fontName = fontName;
} }
} }
return result; return result;
} }

Some files were not shown because too many files have changed in this diff Show More