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

View File

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

View File

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

View File

@ -5,216 +5,216 @@
const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser;
const ANSI = require('./ansi_term.js');
const {
splitTextAtTerms,
renderStringLength
splitTextAtTerms,
renderStringLength
} = require('./string_util.js');
// deps
const _ = require('lodash');
module.exports = function ansiPrep(input, options, cb) {
if(!input) {
return cb(null, '');
}
if(!input) {
return cb(null, '');
}
options.termWidth = options.termWidth || 80;
options.termHeight = options.termHeight || 25;
options.cols = options.cols || options.termWidth || 80;
options.rows = options.rows || options.termHeight || 'auto';
options.startCol = options.startCol || 1;
options.exportMode = options.exportMode || false;
options.fillLines = _.get(options, 'fillLines', true);
options.indent = options.indent || 0;
options.termWidth = options.termWidth || 80;
options.termHeight = options.termHeight || 25;
options.cols = options.cols || options.termWidth || 80;
options.rows = options.rows || options.termHeight || 'auto';
options.startCol = options.startCol || 1;
options.exportMode = options.exportMode || false;
options.fillLines = _.get(options, 'fillLines', true);
options.indent = options.indent || 0;
// 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 parser = new ANSIEscapeParser( { termHeight : options.termHeight, termWidth : options.termWidth } );
// 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 parser = new ANSIEscapeParser( { termHeight : options.termHeight, termWidth : options.termWidth } );
const state = {
row : 0,
col : 0,
};
const state = {
row : 0,
col : 0,
};
let lastRow = 0;
let lastRow = 0;
function ensureRow(row) {
if(canvas[row]) {
return;
}
function ensureRow(row) {
if(canvas[row]) {
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) => {
state.row = row - 1;
state.col = col - 1;
parser.on('position update', (row, col) => {
state.row = row - 1;
state.col = col - 1;
if(0 === state.col) {
state.initialSgr = state.lastSgr;
}
if(0 === state.col) {
state.initialSgr = state.lastSgr;
}
lastRow = Math.max(state.row, lastRow);
});
lastRow = Math.max(state.row, lastRow);
});
parser.on('literal', literal => {
//
// CR/LF are handled for 'position update'; we don't need the chars themselves
//
literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, '');
parser.on('literal', literal => {
//
// CR/LF are handled for 'position update'; we don't need the chars themselves
//
literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, '');
for(let c of literal) {
if(state.col < options.cols && ('auto' === options.rows || state.row < options.rows)) {
ensureRow(state.row);
for(let c of literal) {
if(state.col < options.cols && ('auto' === options.rows || state.row < options.rows)) {
ensureRow(state.row);
if(0 === state.col) {
canvas[state.row][state.col].initialSgr = state.initialSgr;
}
if(0 === state.col) {
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) {
canvas[state.row][state.col].sgr = _.clone(state.sgr);
state.lastSgr = canvas[state.row][state.col].sgr;
state.sgr = null;
}
}
if(state.sgr) {
canvas[state.row][state.col].sgr = _.clone(state.sgr);
state.lastSgr = canvas[state.row][state.col].sgr;
state.sgr = null;
}
}
state.col += 1;
}
});
state.col += 1;
}
});
parser.on('sgr update', sgr => {
ensureRow(state.row);
parser.on('sgr update', sgr => {
ensureRow(state.row);
if(state.col < options.cols) {
canvas[state.row][state.col].sgr = _.clone(sgr);
state.lastSgr = canvas[state.row][state.col].sgr;
} else {
state.sgr = sgr;
}
});
if(state.col < options.cols) {
canvas[state.row][state.col].sgr = _.clone(sgr);
state.lastSgr = canvas[state.row][state.col].sgr;
} else {
state.sgr = sgr;
}
});
function getLastPopulatedColumn(row) {
let col = row.length;
while(--col > 0) {
if(row[col].char || row[col].sgr) {
break;
}
}
return col;
}
function getLastPopulatedColumn(row) {
let col = row.length;
while(--col > 0) {
if(row[col].char || row[col].sgr) {
break;
}
}
return col;
}
parser.on('complete', () => {
let output = '';
let line;
let sgr;
parser.on('complete', () => {
let output = '';
let line;
let sgr;
canvas.slice(0, lastRow + 1).forEach(row => {
const lastCol = getLastPopulatedColumn(row) + 1;
canvas.slice(0, lastRow + 1).forEach(row => {
const lastCol = getLastPopulatedColumn(row) + 1;
let i;
line = options.indent ?
output.length > 0 ? ' '.repeat(options.indent) : '' :
'';
let i;
line = options.indent ?
output.length > 0 ? ' '.repeat(options.indent) : '' :
'';
for(i = 0; i < lastCol; ++i) {
const col = row[i];
for(i = 0; i < lastCol; ++i) {
const col = row[i];
sgr = !options.asciiMode && 0 === i ?
col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' :
'';
sgr = !options.asciiMode && 0 === i ?
col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' :
'';
if(!options.asciiMode && col.sgr) {
sgr += ANSI.getSGRFromGraphicRendition(col.sgr);
}
if(!options.asciiMode && col.sgr) {
sgr += ANSI.getSGRFromGraphicRendition(col.sgr);
}
line += `${sgr}${col.char || ' '}`;
}
line += `${sgr}${col.char || ' '}`;
}
output += line;
output += line;
if(i < row.length) {
output += `${options.asciiMode ? '' : ANSI.blackBG()}`;
if(options.fillLines) {
output += `${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`;
}
}
if(i < row.length) {
output += `${options.asciiMode ? '' : ANSI.blackBG()}`;
if(options.fillLines) {
output += `${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`;
}
}
if(options.startCol + i < options.termWidth || options.forceLineTerm) {
output += '\r\n';
}
});
if(options.startCol + i < options.termWidth || options.forceLineTerm) {
output += '\r\n';
}
});
if(options.exportMode) {
//
// If we're in export mode, we do some additional hackery:
//
// * 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>
// represents chars to get back to the position we were previously at
//
// * 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
const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with
let exportOutput = '';
if(options.exportMode) {
//
// If we're in export mode, we do some additional hackery:
//
// * 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>
// represents chars to get back to the position we were previously at
//
// * 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
const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with
let exportOutput = '';
let m;
let afterSeq;
let wantMore;
let renderStart;
let m;
let afterSeq;
let wantMore;
let renderStart;
splitTextAtTerms(output).forEach(fullLine => {
renderStart = 0;
splitTextAtTerms(output).forEach(fullLine => {
renderStart = 0;
while(fullLine.length > 0) {
let splitAt;
const ANSI_REGEXP = ANSI.getFullMatchRegExp();
wantMore = true;
while(fullLine.length > 0) {
let splitAt;
const ANSI_REGEXP = ANSI.getFullMatchRegExp();
wantMore = true;
while((m = ANSI_REGEXP.exec(fullLine))) {
afterSeq = m.index + m[0].length;
while((m = ANSI_REGEXP.exec(fullLine))) {
afterSeq = m.index + m[0].length;
if(afterSeq < MAX_CHARS) {
// after current seq
splitAt = afterSeq;
} else {
if(m.index < MAX_CHARS) {
// before last found seq
splitAt = m.index;
wantMore = false; // can't eat up any more
}
if(afterSeq < MAX_CHARS) {
// after current seq
splitAt = afterSeq;
} else {
if(m.index < MAX_CHARS) {
// before last found seq
splitAt = m.index;
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(wantMore) {
splitAt = Math.min(fullLine.length, MAX_CHARS - 1);
}
} else {
splitAt = Math.min(fullLine.length, MAX_CHARS - 1);
}
if(splitAt) {
if(wantMore) {
splitAt = Math.min(fullLine.length, MAX_CHARS - 1);
}
} else {
splitAt = Math.min(fullLine.length, MAX_CHARS - 1);
}
const part = fullLine.slice(0, splitAt);
fullLine = fullLine.slice(splitAt);
renderStart += renderStringLength(part);
exportOutput += `${part}\r\n`;
const part = fullLine.slice(0, splitAt);
fullLine = fullLine.slice(splitAt);
renderStart += renderStringLength(part);
exportOutput += `${part}\r\n`;
if(fullLine.length > 0) { // more to go for this line?
exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`;
} else {
exportOutput += ANSI.up();
}
}
});
if(fullLine.length > 0) { // more to go for this line?
exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`;
} else {
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 CONTROL = {
up : 'A',
down : 'B',
up : 'A',
down : 'B',
forward : 'C',
right : 'C',
forward : 'C',
right : 'C',
back : 'D',
left : 'D',
back : 'D',
left : 'D',
nextLine : 'E',
prevLine : 'F',
horizAbsolute : 'G',
nextLine : 'E',
prevLine : 'F',
horizAbsolute : 'G',
//
// CSI [ p1 ] J
// Erase in Page / Erase Data
// Defaults: p1 = 0
// Erases from the current screen according to the value of p1
// 0 - Erase from the current position to the end 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
// the cursor to position 1/1 as a number of BBS programs assume
// this behaviour.
// Erased characters are set to the current attribute.
//
// Support:
// * SyncTERM: Works as expected
// * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1
// and screen remainder
//
eraseData : 'J',
//
// CSI [ p1 ] J
// Erase in Page / Erase Data
// Defaults: p1 = 0
// Erases from the current screen according to the value of p1
// 0 - Erase from the current position to the end 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
// the cursor to position 1/1 as a number of BBS programs assume
// this behaviour.
// Erased characters are set to the current attribute.
//
// Support:
// * SyncTERM: Works as expected
// * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1
// and screen remainder
//
eraseData : 'J',
eraseLine : 'K',
insertLine : 'L',
eraseLine : 'K',
insertLine : 'L',
//
// CSI [ p1 ] M
// Delete Line(s) / "ANSI" Music
// Defaults: p1 = 1
// 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
// empty lines at the end of the screen with the current attribute.
// If "ANSI" Music is fully enabled (CSI = 2 M), performs "ANSI" music
// instead.
// See "ANSI" MUSIC section for more details.
//
// Support:
// * SyncTERM: Works as expected
// * NetRunner:
//
// General Notes:
// See also notes in bansi.txt and cterm.txt about the various
// incompatibilities & oddities around this sequence. ANSI-BBS
// states that it *should* work with any value of p1.
//
deleteLine : 'M',
ansiMusic : 'M',
//
// CSI [ p1 ] M
// Delete Line(s) / "ANSI" Music
// Defaults: p1 = 1
// 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
// empty lines at the end of the screen with the current attribute.
// If "ANSI" Music is fully enabled (CSI = 2 M), performs "ANSI" music
// instead.
// See "ANSI" MUSIC section for more details.
//
// Support:
// * SyncTERM: Works as expected
// * NetRunner:
//
// General Notes:
// See also notes in bansi.txt and cterm.txt about the various
// incompatibilities & oddities around this sequence. ANSI-BBS
// states that it *should* work with any value of p1.
//
deleteLine : 'M',
ansiMusic : 'M',
scrollUp : 'S',
scrollDown : 'T',
setScrollRegion : 'r',
savePos : 's',
restorePos : 'u',
queryPos : '6n',
queryScreenSize : '255n', // See bansi.txt
goto : 'H', // row Pr, column Pc -- same as f
gotoAlt : 'f', // same as H
scrollUp : 'S',
scrollDown : 'T',
setScrollRegion : 'r',
savePos : 's',
restorePos : 'u',
queryPos : '6n',
queryScreenSize : '255n', // See bansi.txt
goto : 'H', // row Pr, column Pc -- same as f
gotoAlt : 'f', // same as H
blinkToBrightIntensity : '?33h',
blinkNormal : '?33l',
blinkToBrightIntensity : '?33h',
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
showCursor : '?25h', // Nonstandard - cterm.txt
hideCursor : '?25l', // 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
// apparently some terms can report screen size and text area via 18t and 19t
// :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
};
//
@ -152,49 +152,49 @@ const CONTROL = {
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
//
const SGRValues = {
reset : 0,
bold : 1,
dim : 2,
blink : 5,
fastBlink : 6,
negative : 7,
hidden : 8,
reset : 0,
bold : 1,
dim : 2,
blink : 5,
fastBlink : 6,
negative : 7,
hidden : 8,
normal : 22, //
steady : 25,
positive : 27,
normal : 22, //
steady : 25,
positive : 27,
black : 30,
red : 31,
green : 32,
yellow : 33,
blue : 34,
magenta : 35,
cyan : 36,
white : 37,
black : 30,
red : 31,
green : 32,
yellow : 33,
blue : 34,
magenta : 35,
cyan : 36,
white : 37,
blackBG : 40,
redBG : 41,
greenBG : 42,
yellowBG : 43,
blueBG : 44,
magentaBG : 45,
cyanBG : 46,
whiteBG : 47,
blackBG : 40,
redBG : 41,
greenBG : 42,
yellowBG : 43,
blueBG : 44,
magentaBG : 45,
cyanBG : 46,
whiteBG : 47,
};
function getFullMatchRegExp(flags = 'g') {
// :TODO: expand this a bit - see strip-ansi/etc.
// :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
// :TODO: expand this a bit - see strip-ansi/etc.
// :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
}
function getFGColorValue(name) {
return SGRValues[name];
return SGRValues[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
//
const SYNCTERM_FONT_AND_ENCODING_TABLE = [
'cp437',
'cp1251',
'koi8_r',
'iso8859_2',
'iso8859_4',
'cp866',
'iso8859_9',
'haik8',
'iso8859_8',
'koi8_u',
'iso8859_15',
'iso8859_4',
'koi8_r_b',
'iso8859_4',
'iso8859_5',
'ARMSCII_8',
'iso8859_15',
'cp850',
'cp850',
'cp885',
'cp1251',
'iso8859_7',
'koi8-r_c',
'iso8859_4',
'iso8859_1',
'cp866',
'cp437',
'cp866',
'cp885',
'cp866_u',
'iso8859_1',
'cp1131',
'c64_upper',
'c64_lower',
'c128_upper',
'c128_lower',
'atari',
'pot_noodle',
'mo_soul',
'microknight_plus',
'topaz_plus',
'microknight',
'topaz',
'cp437',
'cp1251',
'koi8_r',
'iso8859_2',
'iso8859_4',
'cp866',
'iso8859_9',
'haik8',
'iso8859_8',
'koi8_u',
'iso8859_15',
'iso8859_4',
'koi8_r_b',
'iso8859_4',
'iso8859_5',
'ARMSCII_8',
'iso8859_15',
'cp850',
'cp850',
'cp885',
'cp1251',
'iso8859_7',
'koi8-r_c',
'iso8859_4',
'iso8859_1',
'cp866',
'cp437',
'cp866',
'cp885',
'cp866_u',
'iso8859_1',
'cp1131',
'c64_upper',
'c64_lower',
'c128_upper',
'c128_lower',
'atari',
'pot_noodle',
'mo_soul',
'microknight_plus',
'topaz_plus',
'microknight',
'topaz',
];
//
@ -267,137 +267,137 @@ const SYNCTERM_FONT_AND_ENCODING_TABLE = [
// replaced with '_' for lookup purposes.
//
const FONT_ALIAS_TO_SYNCTERM_MAP = {
'cp437' : 'cp437',
'ibm_vga' : 'cp437',
'ibmpc' : 'cp437',
'ibm_pc' : 'cp437',
'pc' : 'cp437',
'cp437_art' : 'cp437',
'ibmpcart' : 'cp437',
'ibmpc_art' : 'cp437',
'ibm_pc_art' : 'cp437',
'msdos_art' : 'cp437',
'msdosart' : 'cp437',
'pc_art' : 'cp437',
'pcart' : 'cp437',
'cp437' : 'cp437',
'ibm_vga' : 'cp437',
'ibmpc' : 'cp437',
'ibm_pc' : 'cp437',
'pc' : 'cp437',
'cp437_art' : 'cp437',
'ibmpcart' : 'cp437',
'ibmpc_art' : 'cp437',
'ibm_pc_art' : 'cp437',
'msdos_art' : 'cp437',
'msdosart' : 'cp437',
'pc_art' : 'cp437',
'pcart' : 'cp437',
'ibm_vga50' : 'cp437',
'ibm_vga25g' : 'cp437',
'ibm_ega' : 'cp437',
'ibm_ega43' : 'cp437',
'ibm_vga50' : 'cp437',
'ibm_vga25g' : 'cp437',
'ibm_ega' : 'cp437',
'ibm_ega43' : 'cp437',
'topaz' : 'topaz',
'amiga_topaz_1' : 'topaz',
'amiga_topaz_1+' : 'topaz_plus',
'topazplus' : 'topaz_plus',
'topaz_plus' : 'topaz_plus',
'amiga_topaz_2' : 'topaz',
'amiga_topaz_2+' : 'topaz_plus',
'topaz2plus' : 'topaz_plus',
'topaz' : 'topaz',
'amiga_topaz_1' : 'topaz',
'amiga_topaz_1+' : 'topaz_plus',
'topazplus' : 'topaz_plus',
'topaz_plus' : 'topaz_plus',
'amiga_topaz_2' : 'topaz',
'amiga_topaz_2+' : 'topaz_plus',
'topaz2plus' : 'topaz_plus',
'pot_noodle' : 'pot_noodle',
'p0tnoodle' : 'pot_noodle',
'amiga_p0t-noodle' : 'pot_noodle',
'pot_noodle' : 'pot_noodle',
'p0tnoodle' : 'pot_noodle',
'amiga_p0t-noodle' : 'pot_noodle',
'mo_soul' : 'mo_soul',
'mosoul' : 'mo_soul',
'mO\'sOul' : 'mo_soul',
'mo_soul' : 'mo_soul',
'mosoul' : 'mo_soul',
'mO\'sOul' : 'mo_soul',
'amiga_microknight' : 'microknight',
'amiga_microknight+' : 'microknight_plus',
'amiga_microknight' : 'microknight',
'amiga_microknight+' : 'microknight_plus',
'atari' : 'atari',
'atarist' : 'atari',
'atari' : 'atari',
'atarist' : 'atari',
};
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);
if(p2 > -1) {
return `${ESC_CSI}${p1};${p2} D`;
}
const p2 = SYNCTERM_FONT_AND_ENCODING_TABLE.indexOf(name);
if(p2 > -1) {
return `${ESC_CSI}${p1};${p2} D`;
}
return '';
return '';
}
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) {
nameOrAlias = getSyncTERMFontFromAlias(nameOrAlias) || nameOrAlias;
return setSyncTERMFont(nameOrAlias);
nameOrAlias = getSyncTERMFontFromAlias(nameOrAlias) || nameOrAlias;
return setSyncTERMFont(nameOrAlias);
}
const DEC_CURSOR_STYLE = {
'blinking block' : 0,
'default' : 1,
'steady block' : 2,
'blinking underline' : 3,
'steady underline' : 4,
'blinking bar' : 5,
'steady bar' : 6,
'blinking block' : 0,
'default' : 1,
'steady block' : 2,
'blinking underline' : 3,
'steady underline' : 4,
'blinking bar' : 5,
'steady bar' : 6,
};
function setCursorStyle(cursorStyle) {
const ps = DEC_CURSOR_STYLE[cursorStyle];
if(ps) {
return `${ESC_CSI}${ps} q`;
}
return '';
const ps = DEC_CURSOR_STYLE[cursorStyle];
if(ps) {
return `${ESC_CSI}${ps} q`;
}
return '';
}
// Create methods such as up(), nextLine(),...
Object.keys(CONTROL).forEach(function onControlName(name) {
const code = CONTROL[name];
const code = CONTROL[name];
exports[name] = function() {
let c = code;
if(arguments.length > 0) {
// arguments are array like -- we want an array
c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code;
}
return `${ESC_CSI}${c}`;
};
exports[name] = function() {
let c = code;
if(arguments.length > 0) {
// arguments are array like -- we want an array
c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code;
}
return `${ESC_CSI}${c}`;
};
});
// Create various color methods such as white(), yellowBG(), reset(), ...
Object.keys(SGRValues).forEach( name => {
const code = SGRValues[name];
const code = SGRValues[name];
exports[name] = function() {
return `${ESC_CSI}${code}m`;
};
exports[name] = function() {
return `${ESC_CSI}${code}m`;
};
});
function sgr() {
//
// - Allow an single array or variable number of arguments
// - Each element can be either a integer or string found in SGRValues
// which in turn maps to a integer
//
if(arguments.length <= 0) {
return '';
}
//
// - Allow an single array or variable number of arguments
// - Each element can be either a integer or string found in SGRValues
// which in turn maps to a integer
//
if(arguments.length <= 0) {
return '';
}
let result = [];
const args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
let result = [];
const args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
for(let i = 0; i < args.length; ++i) {
const arg = args[i];
if(_.isString(arg) && arg in SGRValues) {
result.push(SGRValues[arg]);
} else if(_.isNumber(arg)) {
result.push(arg);
}
}
for(let i = 0; i < args.length; ++i) {
const arg = args[i];
if(_.isString(arg) && arg in SGRValues) {
result.push(SGRValues[arg]);
} else if(_.isNumber(arg)) {
result.push(arg);
}
}
return `${ESC_CSI}${result.join(';')}m`;
return `${ESC_CSI}${result.join(';')}m`;
}
//
@ -405,29 +405,29 @@ function sgr() {
// to a ANSI SGR sequence.
//
function getSGRFromGraphicRendition(graphicRendition, initialReset) {
let sgrSeq = [];
let styleCount = 0;
let sgrSeq = [];
let styleCount = 0;
[ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => {
if(graphicRendition[s]) {
sgrSeq.push(graphicRendition[s]);
++styleCount;
}
});
[ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => {
if(graphicRendition[s]) {
sgrSeq.push(graphicRendition[s]);
++styleCount;
}
});
if(graphicRendition.fg) {
sgrSeq.push(graphicRendition.fg);
}
if(graphicRendition.fg) {
sgrSeq.push(graphicRendition.fg);
}
if(graphicRendition.bg) {
sgrSeq.push(graphicRendition.bg);
}
if(graphicRendition.bg) {
sgrSeq.push(graphicRendition.bg);
}
if(0 === styleCount || initialReset) {
sgrSeq.unshift(0);
}
if(0 === styleCount || initialReset) {
sgrSeq.unshift(0);
}
return sgr(sgrSeq);
return sgr(sgrSeq);
}
///////////////////////////////////////////////////////////////////////////////
@ -435,19 +435,19 @@ function getSGRFromGraphicRendition(graphicRendition, initialReset) {
///////////////////////////////////////////////////////////////////////////////
function clearScreen() {
return exports.eraseData(2);
return exports.eraseData(2);
}
function resetScreen() {
return `${exports.reset()}${exports.eraseData(2)}${exports.goHome()}`;
return `${exports.reset()}${exports.eraseData(2)}${exports.goHome()}`;
}
function normal() {
return sgr( [ 'normal', 'reset' ] );
return sgr( [ 'normal', 'reset' ] );
}
function goHome() {
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!
//
function disableVT100LineWrapping() {
return `${ESC_CSI}?7l`;
return `${ESC_CSI}?7l`;
}
function setEmulatedBaudRate(rate) {
const speed = {
unlimited : 0,
off : 0,
0 : 0,
300 : 1,
600 : 2,
1200 : 3,
2400 : 4,
4800 : 5,
9600 : 6,
19200 : 7,
38400 : 8,
57600 : 9,
76800 : 10,
115200 : 11,
}[rate] || 0;
return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed);
const speed = {
unlimited : 0,
off : 0,
0 : 0,
300 : 1,
600 : 2,
1200 : 3,
2400 : 4,
4800 : 5,
9600 : 6,
19200 : 7,
38400 : 8,
57600 : 9,
76800 : 10,
115200 : 11,
}[rate] || 0;
return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed);
}
function vtxHyperlink(client, url, len) {
if(!client.terminalSupports('vtx_hyperlink')) {
return '';
}
if(!client.terminalSupports('vtx_hyperlink')) {
return '';
}
len = len || url.length;
len = len || url.length;
url = url.split('').map(c => c.charCodeAt(0)).join(';');
return `${ESC_CSI}1;${len};1;1;${url}\\`;
url = url.split('').map(c => c.charCodeAt(0)).join(';');
return `${ESC_CSI}1;${len};1;1;${url}\\`;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,98 +23,98 @@ function getActiveConnections() { return clientConnections; }
function getActiveNodeList(authUsersOnly) {
if(!_.isBoolean(authUsersOnly)) {
authUsersOnly = true;
}
if(!_.isBoolean(authUsersOnly)) {
authUsersOnly = true;
}
const now = moment();
const now = moment();
const activeConnections = getActiveConnections().filter(ac => {
return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly);
});
const activeConnections = getActiveConnections().filter(ac => {
return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly);
});
return _.map(activeConnections, ac => {
const entry = {
node : ac.node,
authenticated : ac.user.isAuthenticated(),
userId : ac.user.userId,
action : _.has(ac, 'currentMenuModule.menuConfig.desc') ? ac.currentMenuModule.menuConfig.desc : 'Unknown',
};
return _.map(activeConnections, ac => {
const entry = {
node : ac.node,
authenticated : ac.user.isAuthenticated(),
userId : ac.user.userId,
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
//
if(ac.user.isAuthenticated()) {
entry.userName = ac.user.username;
entry.realName = ac.user.properties.real_name;
entry.location = ac.user.properties.location;
entry.affils = ac.user.properties.affiliation;
//
// There may be a connection, but not a logged in user as of yet
//
if(ac.user.isAuthenticated()) {
entry.userName = ac.user.username;
entry.realName = ac.user.properties.real_name;
entry.location = ac.user.properties.location;
entry.affils = ac.user.properties.affiliation;
const diff = now.diff(moment(ac.user.properties.last_login_timestamp), 'minutes');
entry.timeOn = moment.duration(diff, 'minutes');
}
return entry;
});
const diff = now.diff(moment(ac.user.properties.last_login_timestamp), 'minutes');
entry.timeOn = moment.duration(diff, 'minutes');
}
return entry;
});
}
function addNewClient(client, clientSock) {
const id = client.session.id = clientConnections.push(client) - 1;
const remoteAddress = client.remoteAddress = clientSock.remoteAddress;
const id = client.session.id = clientConnections.push(client) - 1;
const remoteAddress = client.remoteAddress = clientSock.remoteAddress;
// create a uniqe identifier one-time ID for this session
client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]);
// create a uniqe identifier one-time ID for this session
client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]);
// Create a client specific logger
// Note that this will be updated @ login with additional information
client.log = logger.log.child( { clientId : id, sessionId : client.session.uniqueId } );
// Create a client specific logger
// Note that this will be updated @ login with additional information
client.log = logger.log.child( { clientId : id, sessionId : client.session.uniqueId } );
const connInfo = {
remoteAddress : remoteAddress,
serverName : client.session.serverName,
isSecure : client.session.isSecure,
};
const connInfo = {
remoteAddress : remoteAddress,
serverName : client.session.serverName,
isSecure : client.session.isSecure,
};
if(client.log.debug()) {
connInfo.port = clientSock.localPort;
connInfo.family = clientSock.localFamily;
}
if(client.log.debug()) {
connInfo.port = clientSock.localPort;
connInfo.family = clientSock.localFamily;
}
client.log.info(connInfo, 'Client connected');
client.log.info(connInfo, 'Client connected');
Events.emit(
Events.getSystemEvents().ClientConnected,
{ client : client, connectionCount : clientConnections.length }
);
Events.emit(
Events.getSystemEvents().ClientConnected,
{ client : client, connectionCount : clientConnections.length }
);
return id;
return id;
}
function removeClient(client) {
client.end();
client.end();
const i = clientConnections.indexOf(client);
if(i > -1) {
clientConnections.splice(i, 1);
const i = clientConnections.indexOf(client);
if(i > -1) {
clientConnections.splice(i, 1);
logger.log.info(
{
connectionCount : clientConnections.length,
clientId : client.session.id
},
'Client disconnected'
);
logger.log.info(
{
connectionCount : clientConnections.length,
clientId : client.session.id
},
'Client disconnected'
);
if(client.user && client.user.isValid()) {
Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user } );
}
if(client.user && client.user.isValid()) {
Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user } );
}
Events.emit(
Events.getSystemEvents().ClientDisconnected,
{ client : client, connectionCount : clientConnections.length }
);
}
Events.emit(
Events.getSystemEvents().ClientDisconnected,
{ client : client, connectionCount : clientConnections.length }
);
}
}
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;
function ClientTerminal(output) {
this.output = output;
this.output = output;
var outputEncoding = 'cp437';
assert(iconv.encodingExists(outputEncoding));
var outputEncoding = 'cp437';
assert(iconv.encodingExists(outputEncoding));
// convert line feeds such as \n -> \r\n
this.convertLF = true;
// convert line feeds such as \n -> \r\n
this.convertLF = true;
//
// Some terminal we handle specially
// They can also be found in this.env{}
//
var termType = 'unknown';
var termHeight = 0;
var termWidth = 0;
var termClient = 'unknown';
//
// Some terminal we handle specially
// They can also be found in this.env{}
//
var termType = 'unknown';
var termHeight = 0;
var termWidth = 0;
var termClient = 'unknown';
this.currentSyncFont = 'not_set';
this.currentSyncFont = 'not_set';
// Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc.
this.env = {};
// Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc.
this.env = {};
Object.defineProperty(this, 'outputEncoding', {
get : function() {
return outputEncoding;
},
set : function(enc) {
if(iconv.encodingExists(enc)) {
outputEncoding = enc;
} else {
Log.warn({ encoding : enc }, 'Unknown encoding');
}
}
});
Object.defineProperty(this, 'outputEncoding', {
get : function() {
return outputEncoding;
},
set : function(enc) {
if(iconv.encodingExists(enc)) {
outputEncoding = enc;
} else {
Log.warn({ encoding : enc }, 'Unknown encoding');
}
}
});
Object.defineProperty(this, 'termType', {
get : function() {
return termType;
},
set : function(ttype) {
termType = ttype.toLowerCase();
Object.defineProperty(this, 'termType', {
get : function() {
return termType;
},
set : function(ttype) {
termType = ttype.toLowerCase();
if(this.isANSI()) {
this.outputEncoding = 'cp437';
} else {
// :TODO: See how x84 does this -- only set if local/remote are binary
this.outputEncoding = 'utf8';
}
if(this.isANSI()) {
this.outputEncoding = 'cp437';
} else {
// :TODO: See how x84 does this -- only set if local/remote are binary
this.outputEncoding = 'utf8';
}
// :TODO: according to this: http://mud-dev.wikidot.com/article:telnet-client-identification
// Windows telnet will send "VTNT". If so, set termClient='windows'
// there are some others on the page as well
// :TODO: according to this: http://mud-dev.wikidot.com/article:telnet-client-identification
// Windows telnet will send "VTNT". If so, set termClient='windows'
// 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', {
get : function() {
return termWidth;
},
set : function(width) {
if(width > 0) {
termWidth = width;
}
}
});
Object.defineProperty(this, 'termWidth', {
get : function() {
return termWidth;
},
set : function(width) {
if(width > 0) {
termWidth = width;
}
}
});
Object.defineProperty(this, 'termHeight', {
get : function() {
return termHeight;
},
set : function(height) {
if(height > 0) {
termHeight = height;
}
}
});
Object.defineProperty(this, 'termHeight', {
get : function() {
return termHeight;
},
set : function(height) {
if(height > 0) {
termHeight = height;
}
}
});
Object.defineProperty(this, 'termClient', {
get : function() {
return termClient;
},
set : function(tc) {
termClient = tc;
Object.defineProperty(this, 'termClient', {
get : function() {
return termClient;
},
set : function(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() {
this.output = null;
this.output = null;
};
ClientTerminal.prototype.isNixTerm = function() {
//
// Standard *nix type terminals
//
if(this.termType.startsWith('xterm')) {
return true;
}
//
// Standard *nix type terminals
//
if(this.termType.startsWith('xterm')) {
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() {
//
// ANSI terminals should be encoded to CP437
//
// Some terminal types provided by Mercyful Fate / Enthral:
// ANSI-BBS
// PC-ANSI
// QANSI
// SCOANSI
// VT100
// QNX
//
// Reports from various terminals
//
// syncterm:
// * SyncTERM
//
// xterm:
// * PuTTY
//
// ansi-bbs:
// * fTelnet
//
// pcansi:
// * ZOC
//
// screen:
// * ConnectBot (Android)
//
// linux:
// * JuiceSSH (note: TERM=linux also)
//
return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].includes(this.termType);
//
// ANSI terminals should be encoded to CP437
//
// Some terminal types provided by Mercyful Fate / Enthral:
// ANSI-BBS
// PC-ANSI
// QANSI
// SCOANSI
// VT100
// QNX
//
// Reports from various terminals
//
// syncterm:
// * SyncTERM
//
// xterm:
// * PuTTY
//
// ansi-bbs:
// * fTelnet
//
// pcansi:
// * ZOC
//
// screen:
// * ConnectBot (Android)
//
// linux:
// * JuiceSSH (note: TERM=linux also)
//
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)
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) {
if(this.output) {
this.output.write(s, err => {
if(cb) {
return cb(err);
}
if(this.output) {
this.output.write(s, err => {
if(cb) {
return cb(err);
}
if(err) {
Log.warn( { error : err.message }, 'Failed writing to socket');
}
});
}
if(err) {
Log.warn( { error : err.message }, 'Failed writing to socket');
}
});
}
};
ClientTerminal.prototype.pipeWrite = function(s, spec, cb) {
spec = spec || 'renegade';
spec = spec || 'renegade';
var conv = {
enigma : enigmaToAnsi,
renegade : renegadeToAnsi,
}[spec] || renegadeToAnsi;
var conv = {
enigma : enigmaToAnsi,
renegade : 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) {
convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF;
convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF;
if(convertLineFeeds && _.isString(s)) {
s = s.replace(/\n/g, '\r\n');
}
return iconv.encode(s, this.outputEncoding);
if(convertLineFeeds && _.isString(s)) {
s = s.replace(/\n/g, '\r\n');
}
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...
function enigmaToAnsi(s, client) {
if(-1 == s.indexOf('|')) {
return s; // no pipe codes present
}
if(-1 == s.indexOf('|')) {
return s; // no pipe codes present
}
var result = '';
var re = /\|([A-Z\d]{2}|\|)/g;
var m;
var lastIndex = 0;
while((m = re.exec(s))) {
var val = m[1];
var result = '';
var re = /\|([A-Z\d]{2}|\|)/g;
var m;
var lastIndex = 0;
while((m = re.exec(s))) {
var val = m[1];
if('|' == val) {
result += '|';
continue;
}
if('|' == val) {
result += '|';
continue;
}
// convert to number
val = parseInt(val, 10);
if(isNaN(val)) {
//
// ENiGMA MCI code? Only available if |client|
// is supplied.
//
val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal
}
// convert to number
val = parseInt(val, 10);
if(isNaN(val)) {
//
// ENiGMA MCI code? Only available if |client|
// is supplied.
//
val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal
}
if(_.isString(val)) {
result += s.substr(lastIndex, m.index - lastIndex) + val;
} else {
assert(val >= 0 && val <= 47);
if(_.isString(val)) {
result += s.substr(lastIndex, m.index - lastIndex) + val;
} else {
assert(val >= 0 && val <= 47);
var attr = '';
if(7 == val) {
attr = ansi.sgr('normal');
} else if (val < 7 || val >= 16) {
attr = ansi.sgr(['normal', val]);
} else if (val <= 15) {
attr = ansi.sgr(['normal', val - 8, 'bold']);
}
var attr = '';
if(7 == val) {
attr = ansi.sgr('normal');
} else if (val < 7 || val >= 16) {
attr = ansi.sgr(['normal', val]);
} else if (val <= 15) {
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) {
return s.replace(/\|[A-Z\d]{2}/g, '');
return s.replace(/\|[A-Z\d]{2}/g, '');
}
function enigmaStrLen(s) {
return stripEnigmaCodes(s).length;
return stripEnigmaCodes(s).length;
}
function ansiSgrFromRenegadeColorCode(cc) {
return ansi.sgr({
0 : [ 'reset', 'black' ],
1 : [ 'reset', 'blue' ],
2 : [ 'reset', 'green' ],
3 : [ 'reset', 'cyan' ],
4 : [ 'reset', 'red' ],
5 : [ 'reset', 'magenta' ],
6 : [ 'reset', 'yellow' ],
7 : [ 'reset', 'white' ],
return ansi.sgr({
0 : [ 'reset', 'black' ],
1 : [ 'reset', 'blue' ],
2 : [ 'reset', 'green' ],
3 : [ 'reset', 'cyan' ],
4 : [ 'reset', 'red' ],
5 : [ 'reset', 'magenta' ],
6 : [ 'reset', 'yellow' ],
7 : [ 'reset', 'white' ],
8 : [ 'bold', 'black' ],
9 : [ 'bold', 'blue' ],
10 : [ 'bold', 'green' ],
11 : [ 'bold', 'cyan' ],
12 : [ 'bold', 'red' ],
13 : [ 'bold', 'magenta' ],
14 : [ 'bold', 'yellow' ],
15 : [ 'bold', 'white' ],
8 : [ 'bold', 'black' ],
9 : [ 'bold', 'blue' ],
10 : [ 'bold', 'green' ],
11 : [ 'bold', 'cyan' ],
12 : [ 'bold', 'red' ],
13 : [ 'bold', 'magenta' ],
14 : [ 'bold', 'yellow' ],
15 : [ 'bold', 'white' ],
16 : [ 'blackBG' ],
17 : [ 'blueBG' ],
18 : [ 'greenBG' ],
19 : [ 'cyanBG' ],
20 : [ 'redBG' ],
21 : [ 'magentaBG' ],
22 : [ 'yellowBG' ],
23 : [ 'whiteBG' ],
16 : [ 'blackBG' ],
17 : [ 'blueBG' ],
18 : [ 'greenBG' ],
19 : [ 'cyanBG' ],
20 : [ 'redBG' ],
21 : [ 'magentaBG' ],
22 : [ 'yellowBG' ],
23 : [ 'whiteBG' ],
24 : [ 'blink', 'blackBG' ],
25 : [ 'blink', 'blueBG' ],
26 : [ 'blink', 'greenBG' ],
27 : [ 'blink', 'cyanBG' ],
28 : [ 'blink', 'redBG' ],
29 : [ 'blink', 'magentaBG' ],
30 : [ 'blink', 'yellowBG' ],
31 : [ 'blink', 'whiteBG' ],
}[cc] || 'normal');
24 : [ 'blink', 'blackBG' ],
25 : [ 'blink', 'blueBG' ],
26 : [ 'blink', 'greenBG' ],
27 : [ 'blink', 'cyanBG' ],
28 : [ 'blink', 'redBG' ],
29 : [ 'blink', 'magentaBG' ],
30 : [ 'blink', 'yellowBG' ],
31 : [ 'blink', 'whiteBG' ],
}[cc] || 'normal');
}
function renegadeToAnsi(s, client) {
if(-1 == s.indexOf('|')) {
return s; // no pipe codes present
}
if(-1 == s.indexOf('|')) {
return s; // no pipe codes present
}
var result = '';
var re = /\|([A-Z\d]{2}|\|)/g;
var m;
var lastIndex = 0;
while((m = re.exec(s))) {
var val = m[1];
var result = '';
var re = /\|([A-Z\d]{2}|\|)/g;
var m;
var lastIndex = 0;
while((m = re.exec(s))) {
var val = m[1];
if('|' == val) {
result += '|';
continue;
}
if('|' == val) {
result += '|';
continue;
}
// convert to number
val = parseInt(val, 10);
if(isNaN(val)) {
val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal
}
// convert to number
val = parseInt(val, 10);
if(isNaN(val)) {
val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal
}
if(_.isString(val)) {
result += s.substr(lastIndex, m.index - lastIndex) + val;
} else {
const attr = ansiSgrFromRenegadeColorCode(val);
result += s.substr(lastIndex, m.index - lastIndex) + attr;
}
if(_.isString(val)) {
result += s.substr(lastIndex, m.index - lastIndex) + val;
} else {
const attr = ansiSgrFromRenegadeColorCode(val);
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
//
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 result = '';
let lastIndex = 0;
let v;
let fg;
let bg;
let m;
let result = '';
let lastIndex = 0;
let v;
let fg;
let bg;
while((m = RE.exec(s))) {
switch(m[0].charAt(0)) {
case '|' :
// Renegade or ENiGMA MCI
v = parseInt(m[2], 10);
while((m = RE.exec(s))) {
switch(m[0].charAt(0)) {
case '|' :
// Renegade or ENiGMA MCI
v = parseInt(m[2], 10);
if(isNaN(v)) {
v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal
}
if(isNaN(v)) {
v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal
}
if(_.isString(v)) {
result += s.substr(lastIndex, m.index - lastIndex) + v;
} else {
v = ansiSgrFromRenegadeColorCode(v);
result += s.substr(lastIndex, m.index - lastIndex) + v;
}
break;
if(_.isString(v)) {
result += s.substr(lastIndex, m.index - lastIndex) + v;
} else {
v = ansiSgrFromRenegadeColorCode(v);
result += s.substr(lastIndex, m.index - lastIndex) + v;
}
break;
case '@' :
// PCBoard @X## or Wildcat! @##@
if('@' === m[0].substr(-1)) {
// Wildcat!
v = m[6];
} else {
v = m[4];
}
case '@' :
// PCBoard @X## or Wildcat! @##@
if('@' === m[0].substr(-1)) {
// Wildcat!
v = m[6];
} else {
v = m[4];
}
fg = {
0 : [ 'reset', 'black' ],
1 : [ 'reset', 'blue' ],
2 : [ 'reset', 'green' ],
3 : [ 'reset', 'cyan' ],
4 : [ 'reset', 'red' ],
5 : [ 'reset', 'magenta' ],
6 : [ 'reset', 'yellow' ],
7 : [ 'reset', 'white' ],
fg = {
0 : [ 'reset', 'black' ],
1 : [ 'reset', 'blue' ],
2 : [ 'reset', 'green' ],
3 : [ 'reset', 'cyan' ],
4 : [ 'reset', 'red' ],
5 : [ 'reset', 'magenta' ],
6 : [ 'reset', 'yellow' ],
7 : [ 'reset', 'white' ],
8 : [ 'blink', 'black' ],
9 : [ 'blink', 'blue' ],
A : [ 'blink', 'green' ],
B : [ 'blink', 'cyan' ],
C : [ 'blink', 'red' ],
D : [ 'blink', 'magenta' ],
E : [ 'blink', 'yellow' ],
F : [ 'blink', 'white' ],
}[v.charAt(0)] || ['normal'];
8 : [ 'blink', 'black' ],
9 : [ 'blink', 'blue' ],
A : [ 'blink', 'green' ],
B : [ 'blink', 'cyan' ],
C : [ 'blink', 'red' ],
D : [ 'blink', 'magenta' ],
E : [ 'blink', 'yellow' ],
F : [ 'blink', 'white' ],
}[v.charAt(0)] || ['normal'];
bg = {
0 : [ 'blackBG' ],
1 : [ 'blueBG' ],
2 : [ 'greenBG' ],
3 : [ 'cyanBG' ],
4 : [ 'redBG' ],
5 : [ 'magentaBG' ],
6 : [ 'yellowBG' ],
7 : [ 'whiteBG' ],
bg = {
0 : [ 'blackBG' ],
1 : [ 'blueBG' ],
2 : [ 'greenBG' ],
3 : [ 'cyanBG' ],
4 : [ 'redBG' ],
5 : [ 'magentaBG' ],
6 : [ 'yellowBG' ],
7 : [ 'whiteBG' ],
8 : [ 'bold', 'blackBG' ],
9 : [ 'bold', 'blueBG' ],
A : [ 'bold', 'greenBG' ],
B : [ 'bold', 'cyanBG' ],
C : [ 'bold', 'redBG' ],
D : [ 'bold', 'magentaBG' ],
E : [ 'bold', 'yellowBG' ],
F : [ 'bold', 'whiteBG' ],
}[v.charAt(1)] || [ 'normal' ];
8 : [ 'bold', 'blackBG' ],
9 : [ 'bold', 'blueBG' ],
A : [ 'bold', 'greenBG' ],
B : [ 'bold', 'cyanBG' ],
C : [ 'bold', 'redBG' ],
D : [ 'bold', 'magentaBG' ],
E : [ 'bold', 'yellowBG' ],
F : [ 'bold', 'whiteBG' ],
}[v.charAt(1)] || [ 'normal' ];
v = ansi.sgr(fg.concat(bg));
result += s.substr(lastIndex, m.index - lastIndex) + v;
break;
v = ansi.sgr(fg.concat(bg));
result += s.substr(lastIndex, m.index - lastIndex) + v;
break;
case '\x03' :
v = parseInt(m[8], 10);
case '\x03' :
v = parseInt(m[8], 10);
if(isNaN(v)) {
v += m[0];
} else {
v = ansi.sgr({
0 : [ 'reset', 'black' ],
1 : [ 'bold', 'cyan' ],
2 : [ 'bold', 'yellow' ],
3 : [ 'reset', 'magenta' ],
4 : [ 'bold', 'white', 'blueBG' ],
5 : [ 'reset', 'green' ],
6 : [ 'bold', 'blink', 'red' ],
7 : [ 'bold', 'blue' ],
8 : [ 'reset', 'blue' ],
9 : [ 'reset', 'cyan' ],
}[v] || 'normal');
}
if(isNaN(v)) {
v += m[0];
} else {
v = ansi.sgr({
0 : [ 'reset', 'black' ],
1 : [ 'bold', 'cyan' ],
2 : [ 'bold', 'yellow' ],
3 : [ 'reset', 'magenta' ],
4 : [ 'bold', 'white', 'blueBG' ],
5 : [ 'reset', 'green' ],
6 : [ 'bold', 'blink', 'red' ],
7 : [ 'bold', 'blue' ],
8 : [ 'reset', 'blue' ],
9 : [ 'reset', 'cyan' ],
}[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');
exports.moduleInfo = {
name : 'CombatNet',
desc : 'CombatNet Access Module',
author : 'Dave Stephens',
name : 'CombatNet',
desc : 'CombatNet Access Module',
author : 'Dave Stephens',
};
exports.getModule = class CombatNetModule extends MenuModule {
constructor(options) {
super(options);
constructor(options) {
super(options);
// establish defaults
this.config = options.menuConfig.config;
this.config.host = this.config.host || 'bbs.combatnet.us';
this.config.rloginPort = this.config.rloginPort || 4513;
}
// establish defaults
this.config = options.menuConfig.config;
this.config.host = this.config.host || 'bbs.combatnet.us';
this.config.rloginPort = this.config.rloginPort || 4513;
}
initSequence() {
const self = this;
initSequence() {
const self = this;
async.series(
[
function validateConfig(callback) {
if(!_.isString(self.config.password)) {
return callback(new Error('Config requires "password"!'));
}
if(!_.isString(self.config.bbsTag)) {
return callback(new Error('Config requires "bbsTag"!'));
}
return callback(null);
},
function establishRloginConnection(callback) {
self.client.term.write(resetScreen());
self.client.term.write('Connecting to CombatNet, please wait...\n');
async.series(
[
function validateConfig(callback) {
if(!_.isString(self.config.password)) {
return callback(new Error('Config requires "password"!'));
}
if(!_.isString(self.config.bbsTag)) {
return callback(new Error('Config requires "bbsTag"!'));
}
return callback(null);
},
function establishRloginConnection(callback) {
self.client.term.write(resetScreen());
self.client.term.write('Connecting to CombatNet, please wait...\n');
const restorePipeToNormal = function() {
if(self.client.term.output) {
self.client.term.output.removeListener('data', sendToRloginBuffer);
}
};
const restorePipeToNormal = function() {
if(self.client.term.output) {
self.client.term.output.removeListener('data', sendToRloginBuffer);
}
};
const rlogin = new RLogin(
{ 'clientUsername' : self.config.password,
'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`,
'host' : self.config.host,
'port' : self.config.rloginPort,
'terminalType' : self.client.term.termClient,
'terminalSpeed' : 57600
}
);
const rlogin = new RLogin(
{ 'clientUsername' : self.config.password,
'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`,
'host' : self.config.host,
'port' : self.config.rloginPort,
'terminalType' : self.client.term.termClient,
'terminalSpeed' : 57600
}
);
// If there was an error ...
rlogin.on('error', err => {
self.client.log.info(`CombatNet rlogin client error: ${err.message}`);
restorePipeToNormal();
return callback(err);
});
// If there was an error ...
rlogin.on('error', err => {
self.client.log.info(`CombatNet rlogin client error: ${err.message}`);
restorePipeToNormal();
return callback(err);
});
// If we've been disconnected ...
rlogin.on('disconnect', () => {
self.client.log.info('Disconnected from CombatNet');
restorePipeToNormal();
return callback(null);
});
// If we've been disconnected ...
rlogin.on('disconnect', () => {
self.client.log.info('Disconnected from CombatNet');
restorePipeToNormal();
return callback(null);
});
function sendToRloginBuffer(buffer) {
rlogin.send(buffer);
}
function sendToRloginBuffer(buffer) {
rlogin.send(buffer);
}
rlogin.on('connect',
/* The 'connect' event handler will be supplied with one argument,
rlogin.on('connect',
/* The 'connect' event handler will be supplied with one argument,
a boolean indicating whether or not the connection was established. */
function(state) {
if(state) {
self.client.log.info('Connected to CombatNet');
self.client.term.output.on('data', sendToRloginBuffer);
function(state) {
if(state) {
self.client.log.info('Connected to CombatNet');
self.client.term.output.on('data', sendToRloginBuffer);
} else {
return callback(new Error('Failed to establish establish CombatNet connection'));
}
}
);
} else {
return callback(new Error('Failed to establish establish CombatNet connection'));
}
}
);
// If data (a Buffer) has been received from the server ...
rlogin.on('data', (data) => {
self.client.term.rawWrite(data);
});
// If data (a Buffer) has been received from the server ...
rlogin.on('data', (data) => {
self.client.term.rawWrite(data);
});
// connect...
rlogin.connect();
// connect...
rlogin.connect();
// note: no explicit callback() until we're finished!
}
],
err => {
if(err) {
self.client.log.warn( { error : err.message }, 'CombatNet error');
}
// note: no explicit callback() until we're finished!
}
],
err => {
if(err) {
self.client.log.warn( { error : err.message }, 'CombatNet error');
}
// if the client is still here, go to previous
self.prevMenu();
}
);
}
// if the client is still here, go to previous
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
//
function sortAreasOrConfs(areasOrConfs, type) {
let entryA;
let entryB;
let entryA;
let entryB;
areasOrConfs.sort((a, b) => {
entryA = type ? a[type] : a;
entryB = type ? b[type] : b;
areasOrConfs.sort((a, b) => {
entryA = type ? a[type] : a;
entryB = type ? b[type] : b;
if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) {
return entryA.sort - entryB.sort;
} else {
const keyA = entryA.sort ? entryA.sort.toString() : entryA.name;
const keyB = entryB.sort ? entryB.sort.toString() : entryB.name;
return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare
}
});
if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) {
return entryA.sort - entryB.sort;
} else {
const keyA = entryA.sort ? entryA.sort.toString() : entryA.name;
const keyB = entryB.sort ? entryB.sort.toString() : entryB.name;
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
{
constructor() {
this.cache = new Map(); // path->parsed config
}
constructor() {
this.cache = new Map(); // path->parsed config
}
getConfigWithOptions(options, cb) {
const cached = this.cache.has(options.filePath);
getConfigWithOptions(options, cb) {
const cached = this.cache.has(options.filePath);
if(options.forceReCache || !cached) {
this.recacheConfigFromFile(options.filePath, (err, config) => {
if(!err && !cached) {
if(!options.noWatch) {
const watcher = sane(
paths.dirname(options.filePath),
{
glob : `**/${paths.basename(options.filePath)}`
}
);
if(options.forceReCache || !cached) {
this.recacheConfigFromFile(options.filePath, (err, config) => {
if(!err && !cached) {
if(!options.noWatch) {
const watcher = sane(
paths.dirname(options.filePath),
{
glob : `**/${paths.basename(options.filePath)}`
}
);
watcher.on('change', (fileName, fileRoot) => {
require('./logger.js').log.info( { fileName, fileRoot }, 'Configuration file changed; re-caching');
watcher.on('change', (fileName, fileRoot) => {
require('./logger.js').log.info( { fileName, fileRoot }, 'Configuration file changed; re-caching');
this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => {
if(!err) {
if(options.callback) {
options.callback( { fileName, fileRoot } );
}
}
});
});
}
}
return cb(err, config, true);
});
} else {
return cb(null, this.cache.get(options.filePath), false);
}
}
this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => {
if(!err) {
if(options.callback) {
options.callback( { fileName, fileRoot } );
}
}
});
});
}
}
return cb(err, config, true);
});
} else {
return cb(null, this.cache.get(options.filePath), false);
}
}
getConfig(filePath, cb) {
return this.getConfigWithOptions( { filePath }, cb);
}
getConfig(filePath, cb) {
return this.getConfigWithOptions( { filePath }, cb);
}
recacheConfigFromFile(path, cb) {
fs.readFile(path, { encoding : 'utf-8' }, (err, data) => {
if(err) {
return cb(err);
}
recacheConfigFromFile(path, cb) {
fs.readFile(path, { encoding : 'utf-8' }, (err, data) => {
if(err) {
return cb(err);
}
let parsed;
try {
parsed = hjson.parse(data);
this.cache.set(path, parsed);
} catch(e) {
require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' );
return cb(e);
}
let parsed;
try {
parsed = hjson.parse(data);
this.cache.set(path, parsed);
} catch(e) {
require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' );
return cb(e);
}
return cb(null, parsed);
});
}
return cb(null, parsed);
});
}
};

View File

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

View File

@ -11,177 +11,177 @@ const async = require('async');
exports.connectEntry = connectEntry;
function ansiDiscoverHomePosition(client, cb) {
//
// We want to find the home position. ANSI-BBS and most terminals
// 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
// our positioning to accomodate for such.
//
const done = function(err) {
client.removeListener('cursor position report', cprListener);
clearTimeout(giveUpTimer);
return cb(err);
};
//
// We want to find the home position. ANSI-BBS and most terminals
// 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
// our positioning to accomodate for such.
//
const done = function(err) {
client.removeListener('cursor position report', cprListener);
clearTimeout(giveUpTimer);
return cb(err);
};
const cprListener = function(pos) {
const h = pos[0];
const w = pos[1];
const cprListener = function(pos) {
const h = pos[0];
const w = pos[1];
//
// We expect either 0,0, or 1,1. Anything else will be filed as bad data
//
if(h > 1 || w > 1) {
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'));
}
//
// We expect either 0,0, or 1,1. Anything else will be filed as bad data
//
if(h > 1 || w > 1) {
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'));
}
if(0 === h & 0 === w) {
//
// 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.cprOffset = 1;
}
if(0 === h & 0 === w) {
//
// 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.cprOffset = 1;
}
return done(null);
};
return done(null);
};
client.once('cursor position report', cprListener);
client.once('cursor position report', cprListener);
const giveUpTimer = setTimeout( () => {
return done(new Error('Giving up on home position CPR'));
}, 3000); // 3s
const giveUpTimer = setTimeout( () => {
return done(new Error('Giving up on home position CPR'));
}, 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) {
if(client.term.termHeight > 0 || client.term.termWidth > 0) {
return cb(null);
}
if(client.term.termHeight > 0 || client.term.termWidth > 0) {
return cb(null);
}
const done = function(err) {
client.removeListener('cursor position report', cprListener);
clearTimeout(giveUpTimer);
return cb(err);
};
const done = function(err) {
client.removeListener('cursor position report', cprListener);
clearTimeout(giveUpTimer);
return cb(err);
};
const cprListener = function(pos) {
//
// If we've already found out, disregard
//
if(client.term.termHeight > 0 || client.term.termWidth > 0) {
return done(null);
}
const cprListener = function(pos) {
//
// If we've already found out, disregard
//
if(client.term.termHeight > 0 || client.term.termWidth > 0) {
return done(null);
}
const h = pos[0];
const w = pos[1];
const h = pos[0];
const w = pos[1];
//
// Netrunner for example gives us 1x1 here. Not really useful. Ignore
// values that seem obviously bad.
//
if(h < 10 || w < 10) {
client.log.warn(
{ height : h, width : w },
'Ignoring ANSI CPR screen size query response due to very small values');
return done(new Error('Term size <= 10 considered invalid'));
}
//
// Netrunner for example gives us 1x1 here. Not really useful. Ignore
// values that seem obviously bad.
//
if(h < 10 || w < 10) {
client.log.warn(
{ height : h, width : w },
'Ignoring ANSI CPR screen size query response due to very small values');
return done(new Error('Term size <= 10 considered invalid'));
}
client.term.termHeight = h;
client.term.termWidth = w;
client.term.termHeight = h;
client.term.termWidth = w;
client.log.debug(
{
termWidth : client.term.termWidth,
termHeight : client.term.termHeight,
source : 'ANSI CPR'
},
'Window size updated'
);
client.log.debug(
{
termWidth : client.term.termWidth,
termHeight : client.term.termHeight,
source : 'ANSI CPR'
},
'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
const giveUpTimer = setTimeout( () => {
return done(new Error('No term size established by CPR within timeout'));
}, 2000);
// give up after 2s
const giveUpTimer = setTimeout( () => {
return done(new Error('No term size established by CPR within timeout'));
}, 2000);
// Start the process: Query for CPR
client.term.rawWrite(ansi.queryScreenSize());
// Start the process: Query for CPR
client.term.rawWrite(ansi.queryScreenSize());
}
function prepareTerminal(term) {
term.rawWrite(ansi.normal());
//term.rawWrite(ansi.disableVT100LineWrapping());
// :TODO: set xterm stuff -- see x84/others
term.rawWrite(ansi.normal());
//term.rawWrite(ansi.disableVT100LineWrapping());
// :TODO: set xterm stuff -- see x84/others
}
function displayBanner(term) {
// note: intentional formatting:
term.pipeWrite(`
// note: intentional formatting:
term.pipeWrite(`
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|06Copyright (c) 2014-2018 Bryan Ashby |14- |12http://l33t.codes/
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|00`
);
);
}
function connectEntry(client, nextMenu) {
const term = client.term;
const term = client.term;
async.series(
[
function basicPrepWork(callback) {
term.rawWrite(ansi.queryDeviceAttributes(0));
return callback(null);
},
function discoverHomePosition(callback) {
ansiDiscoverHomePosition(client, () => {
// :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
});
},
function queryTermSizeByNonStandardAnsi(callback) {
ansiQueryTermSizeIfNeeded(client, err => {
if(err) {
//
// Check again; We may have got via NAWS/similar before CPR completed.
//
if(0 === term.termHeight || 0 === term.termWidth) {
//
// We still don't have something good for term height/width.
// Default to DOS size 80x25.
//
// :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!');
async.series(
[
function basicPrepWork(callback) {
term.rawWrite(ansi.queryDeviceAttributes(0));
return callback(null);
},
function discoverHomePosition(callback) {
ansiDiscoverHomePosition(client, () => {
// :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
});
},
function queryTermSizeByNonStandardAnsi(callback) {
ansiQueryTermSizeIfNeeded(client, err => {
if(err) {
//
// Check again; We may have got via NAWS/similar before CPR completed.
//
if(0 === term.termHeight || 0 === term.termWidth) {
//
// We still don't have something good for term height/width.
// Default to DOS size 80x25.
//
// :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!');
term.termHeight = 25;
term.termWidth = 80;
}
}
term.termHeight = 25;
term.termWidth = 80;
}
}
return callback(null);
});
},
],
() => {
prepareTerminal(term);
return callback(null);
});
},
],
() => {
prepareTerminal(term);
//
// Always show an ENiGMA½ banner
//
displayBanner(term);
//
// Always show an ENiGMA½ banner
//
displayBanner(term);
// fire event
Events.emit(Events.getSystemEvents().TermDetected, { client : client } );
// fire event
Events.emit(Events.getSystemEvents().TermDetected, { client : client } );
setTimeout( () => {
return client.menuStack.goto(nextMenu);
}, 500);
}
);
setTimeout( () => {
return client.menuStack.goto(nextMenu);
}, 500);
}
);
}

View File

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

View File

@ -25,98 +25,98 @@ exports.initializeDatabases = initializeDatabases;
exports.dbs = dbs;
function getTransactionDatabase(db) {
return sqlite3Trans.wrap(db);
return sqlite3Trans.wrap(db);
}
function getDatabasePath(name) {
return paths.join(conf.config.paths.db, `${name}.sqlite3`);
return paths.join(conf.config.paths.db, `${name}.sqlite3`);
}
function getModDatabasePath(moduleInfo, suffix) {
//
// Mods that use a database are stored in Config.paths.modsDb (e.g. enigma-bbs/db/mods)
// We expect that moduleInfo defines packageName which will be the base of the modules
// filename. An optional suffix may be supplied as well.
//
const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
//
// Mods that use a database are stored in Config.paths.modsDb (e.g. enigma-bbs/db/mods)
// We expect that moduleInfo defines packageName which will be the base of the modules
// filename. An optional suffix may be supplied as well.
//
const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
assert(_.isObject(moduleInfo));
assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!');
assert(_.isObject(moduleInfo));
assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!');
let full = moduleInfo.packageName;
if(suffix) {
full += `.${suffix}`;
}
let full = moduleInfo.packageName;
if(suffix) {
full += `.${suffix}`;
}
assert(
(full.split('.').length > 1 && HOST_RE.test(full)),
'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation');
assert(
(full.split('.').length > 1 && HOST_RE.test(full)),
'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation');
return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`);
return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`);
}
function getISOTimestampString(ts) {
ts = ts || moment();
return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ');
ts = ts || moment();
return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ');
}
function sanatizeString(s) {
return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex
switch (c) {
case '\0' : return '\\0';
case '\x08' : return '\\b';
case '\x09' : return '\\t';
case '\x1a' : return '\\z';
case '\n' : return '\\n';
case '\r' : return '\\r';
return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex
switch (c) {
case '\0' : return '\\0';
case '\x08' : return '\\b';
case '\x09' : return '\\t';
case '\x1a' : return '\\z';
case '\n' : return '\\n';
case '\r' : return '\\r';
case '"' :
case '\'' :
return `${c}${c}`;
case '"' :
case '\'' :
return `${c}${c}`;
case '\\' :
case '%' :
return `\\${c}`;
}
});
case '\\' :
case '%' :
return `\\${c}`;
}
});
}
function initializeDatabases(cb) {
async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => {
dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => {
if(err) {
return cb(err);
}
async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => {
dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => {
if(err) {
return cb(err);
}
dbs[dbName].serialize( () => {
DB_INIT_TABLE[dbName]( () => {
return next(null);
});
});
}));
}, err => {
return cb(err);
});
dbs[dbName].serialize( () => {
DB_INIT_TABLE[dbName]( () => {
return next(null);
});
});
}));
}, err => {
return cb(err);
});
}
function enableForeignKeys(db) {
db.run('PRAGMA foreign_keys = ON;');
db.run('PRAGMA foreign_keys = ON;');
}
const DB_INIT_TABLE = {
system : (cb) => {
enableForeignKeys(dbs.system);
system : (cb) => {
enableForeignKeys(dbs.system);
// Various stat/event logging - see stat_log.js
dbs.system.run(
`CREATE TABLE IF NOT EXISTS system_stat (
// Various stat/event logging - see stat_log.js
dbs.system.run(
`CREATE TABLE IF NOT EXISTS system_stat (
stat_name VARCHAR PRIMARY KEY NOT NULL,
stat_value VARCHAR NOT NULL
);`
);
);
dbs.system.run(
`CREATE TABLE IF NOT EXISTS system_event_log (
dbs.system.run(
`CREATE TABLE IF NOT EXISTS system_event_log (
id INTEGER PRIMARY KEY,
timestamp DATETIME NOT NULL,
log_name VARCHAR NOT NULL,
@ -124,10 +124,10 @@ const DB_INIT_TABLE = {
UNIQUE(timestamp, log_name)
);`
);
);
dbs.system.run(
`CREATE TABLE IF NOT EXISTS user_event_log (
dbs.system.run(
`CREATE TABLE IF NOT EXISTS user_event_log (
id INTEGER PRIMARY KEY,
timestamp DATETIME NOT NULL,
user_id INTEGER NOT NULL,
@ -136,58 +136,58 @@ const DB_INIT_TABLE = {
UNIQUE(timestamp, user_id, log_name)
);`
);
);
return cb(null);
},
return cb(null);
},
user : (cb) => {
enableForeignKeys(dbs.user);
user : (cb) => {
enableForeignKeys(dbs.user);
dbs.user.run(
`CREATE TABLE IF NOT EXISTS user (
dbs.user.run(
`CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY,
user_name VARCHAR NOT NULL,
UNIQUE(user_name)
);`
);
);
// :TODO: create FK on delete/etc.
// :TODO: create FK on delete/etc.
dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_property (
dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_property (
user_id INTEGER NOT NULL,
prop_name VARCHAR NOT NULL,
prop_value VARCHAR,
UNIQUE(user_id, prop_name),
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
);`
);
);
dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_group_member (
dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_group_member (
group_name VARCHAR NOT NULL,
user_id INTEGER NOT NULL,
UNIQUE(group_name, user_id)
);`
);
);
dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_login_history (
dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_login_history (
user_id INTEGER NOT NULL,
user_name VARCHAR NOT NULL,
timestamp DATETIME NOT NULL
);`
);
);
return cb(null);
},
return cb(null);
},
message : (cb) => {
enableForeignKeys(dbs.message);
message : (cb) => {
enableForeignKeys(dbs.message);
dbs.message.run(
`CREATE TABLE IF NOT EXISTS message (
dbs.message.run(
`CREATE TABLE IF NOT EXISTS message (
message_id INTEGER PRIMARY KEY,
area_tag VARCHAR NOT NULL,
message_uuid VARCHAR(36) NOT NULL,
@ -200,47 +200,47 @@ const DB_INIT_TABLE = {
view_count INTEGER NOT NULL DEFAULT 0,
UNIQUE(message_uuid)
);`
);
);
dbs.message.run(
`CREATE INDEX IF NOT EXISTS message_by_area_tag_index
dbs.message.run(
`CREATE INDEX IF NOT EXISTS message_by_area_tag_index
ON message (area_tag);`
);
);
dbs.message.run(
`CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 (
dbs.message.run(
`CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 (
content="message",
subject,
message
);`
);
);
dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN
dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN
DELETE FROM message_fts WHERE docid=old.rowid;
END;`
);
);
dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN
dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN
DELETE FROM message_fts WHERE docid=old.rowid;
END;`
);
);
dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN
dbs.message.run(
`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);
END;`
);
);
dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN
dbs.message.run(
`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);
END;`
);
);
dbs.message.run(
`CREATE TABLE IF NOT EXISTS message_meta (
dbs.message.run(
`CREATE TABLE IF NOT EXISTS message_meta (
message_id INTEGER NOT NULL,
meta_category INTEGER NOT NULL,
meta_name VARCHAR NOT NULL,
@ -248,11 +248,11 @@ const DB_INIT_TABLE = {
UNIQUE(message_id, meta_category, meta_name, meta_value),
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(
`CREATE TABLE IF NOT EXISTS hash_tag (
hash_tag_id INTEGER PRIMARY KEY,
@ -270,33 +270,33 @@ const DB_INIT_TABLE = {
);
*/
dbs.message.run(
`CREATE TABLE IF NOT EXISTS user_message_area_last_read (
dbs.message.run(
`CREATE TABLE IF NOT EXISTS user_message_area_last_read (
user_id INTEGER NOT NULL,
area_tag VARCHAR NOT NULL,
message_id INTEGER NOT NULL,
UNIQUE(user_id, area_tag)
);`
);
);
dbs.message.run(
`CREATE TABLE IF NOT EXISTS message_area_last_scan (
dbs.message.run(
`CREATE TABLE IF NOT EXISTS message_area_last_scan (
scan_toss VARCHAR NOT NULL,
area_tag VARCHAR NOT NULL,
message_id INTEGER NOT NULL,
UNIQUE(scan_toss, area_tag)
);`
);
);
return cb(null);
},
return cb(null);
},
file : (cb) => {
enableForeignKeys(dbs.file);
file : (cb) => {
enableForeignKeys(dbs.file);
dbs.file.run(
// :TODO: should any of this be unique -- file_sha256 unless dupes are allowed on the system
`CREATE TABLE IF NOT EXISTS file (
dbs.file.run(
// :TODO: should any of this be unique -- file_sha256 unless dupes are allowed on the system
`CREATE TABLE IF NOT EXISTS file (
file_id INTEGER PRIMARY KEY,
area_tag VARCHAR NOT NULL,
file_sha256 VARCHAR NOT NULL,
@ -306,105 +306,105 @@ const DB_INIT_TABLE = {
desc_long, /* FTS @ file_fts */
upload_timestamp DATETIME NOT NULL
);`
);
);
dbs.file.run(
`CREATE INDEX IF NOT EXISTS file_by_area_tag_index
dbs.file.run(
`CREATE INDEX IF NOT EXISTS file_by_area_tag_index
ON file (area_tag);`
);
);
dbs.file.run(
`CREATE INDEX IF NOT EXISTS file_by_sha256_index
dbs.file.run(
`CREATE INDEX IF NOT EXISTS file_by_sha256_index
ON file (file_sha256);`
);
);
dbs.file.run(
`CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts4 (
dbs.file.run(
`CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts4 (
content="file",
file_name,
desc,
desc_long
);`
);
);
dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN
dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN
DELETE FROM file_fts WHERE docid=old.rowid;
END;`
);
);
dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN
dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN
DELETE FROM file_fts WHERE docid=old.rowid;
END;`
);
);
dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN
dbs.file.run(
`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);
END;`
);
);
dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN
dbs.file.run(
`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);
END;`
);
);
dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_meta (
dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_meta (
file_id INTEGER NOT NULL,
meta_name VARCHAR NOT NULL,
meta_value VARCHAR NOT NULL,
UNIQUE(file_id, meta_name, meta_value),
FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE
);`
);
);
dbs.file.run(
`CREATE TABLE IF NOT EXISTS hash_tag (
dbs.file.run(
`CREATE TABLE IF NOT EXISTS hash_tag (
hash_tag_id INTEGER PRIMARY KEY,
hash_tag VARCHAR NOT NULL,
UNIQUE(hash_tag)
);`
);
);
dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_hash_tag (
dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_hash_tag (
hash_tag_id INTEGER NOT NULL,
file_id INTEGER NOT NULL,
UNIQUE(hash_tag_id, file_id)
);`
);
);
dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_user_rating (
dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_user_rating (
file_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
rating INTEGER NOT NULL,
UNIQUE(file_id, user_id)
);`
);
);
dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_web_serve (
dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_web_serve (
hash_id VARCHAR NOT NULL PRIMARY KEY,
expire_timestamp DATETIME NOT NULL
);`
);
);
dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_web_serve_batch (
dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_web_serve_batch (
hash_id VARCHAR NOT NULL,
file_id INTEGER NOT NULL,
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');
module.exports = class DescriptIonFile {
constructor() {
this.entries = new Map();
}
constructor() {
this.entries = new Map();
}
get(fileName) {
return this.entries.get(fileName);
}
get(fileName) {
return this.entries.get(fileName);
}
getDescription(fileName) {
const entry = this.get(fileName);
if(entry) {
return entry.desc;
}
}
getDescription(fileName) {
const entry = this.get(fileName);
if(entry) {
return entry.desc;
}
}
static createFromFile(path, cb) {
fs.readFile(path, (err, descData) => {
if(err) {
return cb(err);
}
static createFromFile(path, cb) {
fs.readFile(path, (err, descData) => {
if(err) {
return cb(err);
}
const descIonFile = new DescriptIonFile();
const descIonFile = new DescriptIonFile();
// DESCRIPT.ION entries are terminated with a CR and/or LF
const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g);
// DESCRIPT.ION entries are terminated with a CR and/or LF
const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g);
async.each(lines, (entryData, nextLine) => {
//
// We allow quoted (long) filenames or non-quoted filenames.
// FILENAME<SPC>DESC<0x04><program data><CR/LF>
//
const parts = entryData.match(/^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/); // eslint-disable-line no-control-regex
if(!parts) {
return nextLine(null);
}
async.each(lines, (entryData, nextLine) => {
//
// We allow quoted (long) filenames or non-quoted filenames.
// FILENAME<SPC>DESC<0x04><program data><CR/LF>
//
const parts = entryData.match(/^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/); // eslint-disable-line no-control-regex
if(!parts) {
return nextLine(null);
}
const fileName = parts[1] || parts[2];
const fileName = parts[1] || parts[2];
//
// Un-escape CR/LF's
// - escapped \r and/or \n
// - BBBS style @n - See https://www.bbbs.net/sysop.html
//
const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n');
//
// Un-escape CR/LF's
// - escapped \r and/or \n
// - BBBS style @n - See https://www.bbbs.net/sysop.html
//
const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n');
descIonFile.entries.set(
fileName,
{
desc : desc,
programId : parts[4],
programData : parts[5],
}
);
descIonFile.entries.set(
fileName,
{
desc : desc,
programId : parts[4],
programData : parts[5],
}
);
return nextLine(null);
},
() => {
return cb(null, descIonFile);
});
});
}
return nextLine(null);
},
() => {
return cb(null, descIonFile);
});
});
}
};

View File

@ -13,137 +13,137 @@ const createServer = require('net').createServer;
exports.Door = Door;
function Door(client, exeInfo) {
events.EventEmitter.call(this);
events.EventEmitter.call(this);
const self = this;
this.client = client;
this.exeInfo = exeInfo;
this.exeInfo.encoding = (this.exeInfo.encoding || 'cp437').toLowerCase();
let restored = false;
const self = this;
this.client = client;
this.exeInfo = exeInfo;
this.exeInfo.encoding = (this.exeInfo.encoding || 'cp437').toLowerCase();
let restored = false;
//
// Members of exeInfo:
// cmd
// args[]
// env{}
// cwd
// io
// encoding
// dropFile
// node
// inhSocket
//
//
// Members of exeInfo:
// cmd
// args[]
// env{}
// cwd
// io
// encoding
// dropFile
// node
// inhSocket
//
this.doorDataHandler = function(data) {
self.client.term.write(decode(data, self.exeInfo.encoding));
};
this.doorDataHandler = function(data) {
self.client.term.write(decode(data, self.exeInfo.encoding));
};
this.restoreIo = function(piped) {
if(!restored && self.client.term.output) {
self.client.term.output.unpipe(piped);
self.client.term.output.resume();
restored = true;
}
};
this.restoreIo = function(piped) {
if(!restored && self.client.term.output) {
self.client.term.output.unpipe(piped);
self.client.term.output.resume();
restored = true;
}
};
this.prepareSocketIoServer = function(cb) {
if('socket' === self.exeInfo.io) {
const sockServer = createServer(conn => {
this.prepareSocketIoServer = function(cb) {
if('socket' === self.exeInfo.io) {
const sockServer = createServer(conn => {
sockServer.getConnections( (err, count) => {
sockServer.getConnections( (err, count) => {
// We expect only one connection from our DOOR/emulator/etc.
if(!err && count <= 1) {
self.client.term.output.pipe(conn);
// We expect only one connection from our DOOR/emulator/etc.
if(!err && count <= 1) {
self.client.term.output.pipe(conn);
conn.on('data', self.doorDataHandler);
conn.on('data', self.doorDataHandler);
conn.once('end', () => {
return self.restoreIo(conn);
});
conn.once('end', () => {
return self.restoreIo(conn);
});
conn.once('error', err => {
self.client.log.info( { error : err.toString() }, 'Door socket server connection');
return self.restoreIo(conn);
});
}
});
});
conn.once('error', err => {
self.client.log.info( { error : err.toString() }, 'Door socket server connection');
return self.restoreIo(conn);
});
}
});
});
sockServer.listen(0, () => {
return cb(null, sockServer);
});
} else {
return cb(null);
}
};
sockServer.listen(0, () => {
return cb(null, sockServer);
});
} else {
return cb(null);
}
};
this.doorExited = function() {
self.emit('finished');
};
this.doorExited = function() {
self.emit('finished');
};
}
require('util').inherits(Door, events.EventEmitter);
Door.prototype.run = function() {
const self = this;
const self = this;
this.prepareSocketIoServer( (err, sockServer) => {
if(err) {
this.client.log.warn( { error : err.toString() }, 'Failed executing door');
return self.doorExited();
}
this.prepareSocketIoServer( (err, sockServer) => {
if(err) {
this.client.log.warn( { error : err.toString() }, 'Failed executing door');
return self.doorExited();
}
// Expand arg strings, e.g. {dropFile} -> DOOR32.SYS
// :TODO: Use .map() here
let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified
// Expand arg strings, e.g. {dropFile} -> DOOR32.SYS
// :TODO: Use .map() here
let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified
for(let i = 0; i < args.length; ++i) {
args[i] = stringFormat(self.exeInfo.args[i], {
dropFile : self.exeInfo.dropFile,
node : self.exeInfo.node.toString(),
srvPort : sockServer ? sockServer.address().port.toString() : '-1',
userId : self.client.user.userId.toString(),
});
}
for(let i = 0; i < args.length; ++i) {
args[i] = stringFormat(self.exeInfo.args[i], {
dropFile : self.exeInfo.dropFile,
node : self.exeInfo.node.toString(),
srvPort : sockServer ? sockServer.address().port.toString() : '-1',
userId : self.client.user.userId.toString(),
});
}
const door = pty.spawn(self.exeInfo.cmd, args, {
cols : self.client.term.termWidth,
rows : self.client.term.termHeight,
// :TODO: cwd
env : self.exeInfo.env,
encoding : null, // we want to handle all encoding ourself
});
const door = pty.spawn(self.exeInfo.cmd, args, {
cols : self.client.term.termWidth,
rows : self.client.term.termHeight,
// :TODO: cwd
env : self.exeInfo.env,
encoding : null, // we want to handle all encoding ourself
});
if('stdio' === self.exeInfo.io) {
self.client.log.debug('Using stdio for door I/O');
if('stdio' === self.exeInfo.io) {
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', () => {
return self.restoreIo(door);
});
} else if('socket' === self.exeInfo.io) {
self.client.log.debug( { port : sockServer.address().port }, 'Using temporary socket server for door I/O');
}
door.once('close', () => {
return self.restoreIo(door);
});
} else if('socket' === self.exeInfo.io) {
self.client.log.debug( { port : sockServer.address().port }, 'Using temporary socket server for door I/O');
}
door.once('exit', exitCode => {
self.client.log.info( { exitCode : exitCode }, 'Door exited');
door.once('exit', exitCode => {
self.client.log.info( { exitCode : exitCode }, 'Door exited');
if(sockServer) {
sockServer.close();
}
if(sockServer) {
sockServer.close();
}
// we may not get a close
if('stdio' === self.exeInfo.io) {
self.restoreIo(door);
}
// we may not get a close
if('stdio' === self.exeInfo.io) {
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;
exports.moduleInfo = {
name : 'DoorParty',
desc : 'DoorParty Access Module',
author : 'NuSkooler',
name : 'DoorParty',
desc : 'DoorParty Access Module',
author : 'NuSkooler',
};
exports.getModule = class DoorPartyModule extends MenuModule {
constructor(options) {
super(options);
constructor(options) {
super(options);
// establish defaults
this.config = options.menuConfig.config;
this.config.host = this.config.host || 'dp.throwbackbbs.com';
this.config.sshPort = this.config.sshPort || 2022;
this.config.rloginPort = this.config.rloginPort || 513;
}
// establish defaults
this.config = options.menuConfig.config;
this.config.host = this.config.host || 'dp.throwbackbbs.com';
this.config.sshPort = this.config.sshPort || 2022;
this.config.rloginPort = this.config.rloginPort || 513;
}
initSequence() {
let clientTerminated;
const self = this;
initSequence() {
let clientTerminated;
const self = this;
async.series(
[
function validateConfig(callback) {
if(!_.isString(self.config.username)) {
return callback(new Error('Config requires "username"!'));
}
if(!_.isString(self.config.password)) {
return callback(new Error('Config requires "password"!'));
}
if(!_.isString(self.config.bbsTag)) {
return callback(new Error('Config requires "bbsTag"!'));
}
return callback(null);
},
function establishSecureConnection(callback) {
self.client.term.write(resetScreen());
self.client.term.write('Connecting to DoorParty, please wait...\n');
async.series(
[
function validateConfig(callback) {
if(!_.isString(self.config.username)) {
return callback(new Error('Config requires "username"!'));
}
if(!_.isString(self.config.password)) {
return callback(new Error('Config requires "password"!'));
}
if(!_.isString(self.config.bbsTag)) {
return callback(new Error('Config requires "bbsTag"!'));
}
return callback(null);
},
function establishSecureConnection(callback) {
self.client.term.write(resetScreen());
self.client.term.write('Connecting to DoorParty, please wait...\n');
const sshClient = new SSHClient();
const sshClient = new SSHClient();
let pipeRestored = false;
let pipedStream;
const restorePipe = function() {
if(pipedStream && !pipeRestored && !clientTerminated) {
self.client.term.output.unpipe(pipedStream);
self.client.term.output.resume();
}
};
let pipeRestored = false;
let pipedStream;
const restorePipe = function() {
if(pipedStream && !pipeRestored && !clientTerminated) {
self.client.term.output.unpipe(pipedStream);
self.client.term.output.resume();
}
};
sshClient.on('ready', () => {
// track client termination so we can clean up early
self.client.once('end', () => {
self.client.log.info('Connection ended. Terminating DoorParty connection');
clientTerminated = true;
sshClient.end();
});
sshClient.on('ready', () => {
// track client termination so we can clean up early
self.client.once('end', () => {
self.client.log.info('Connection ended. Terminating DoorParty connection');
clientTerminated = true;
sshClient.end();
});
// establish tunnel for rlogin
sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => {
if(err) {
return callback(new Error('Failed to establish tunnel'));
}
// establish tunnel for rlogin
sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => {
if(err) {
return callback(new Error('Failed to establish tunnel'));
}
//
// Send rlogin
// DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g.
// [XA]nuskooler
//
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
stream.write(rlogin);
//
// Send rlogin
// DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g.
// [XA]nuskooler
//
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
stream.write(rlogin);
pipedStream = stream; // :TODO: this is hacky...
self.client.term.output.pipe(stream);
pipedStream = stream; // :TODO: this is hacky...
self.client.term.output.pipe(stream);
stream.on('data', d => {
// :TODO: we should just pipe this...
self.client.term.rawWrite(d);
});
stream.on('data', d => {
// :TODO: we should just pipe this...
self.client.term.rawWrite(d);
});
stream.on('close', () => {
restorePipe();
sshClient.end();
});
});
});
stream.on('close', () => {
restorePipe();
sshClient.end();
});
});
});
sshClient.on('error', err => {
self.client.log.info(`DoorParty SSH client error: ${err.message}`);
});
sshClient.on('error', err => {
self.client.log.info(`DoorParty SSH client error: ${err.message}`);
});
sshClient.on('close', () => {
restorePipe();
callback(null);
});
sshClient.on('close', () => {
restorePipe();
callback(null);
});
sshClient.connect( {
host : self.config.host,
port : self.config.sshPort,
username : self.config.username,
password : self.config.password,
});
sshClient.connect( {
host : self.config.host,
port : self.config.sshPort,
username : self.config.username,
password : self.config.password,
});
// note: no explicit callback() until we're finished!
}
],
err => {
if(err) {
self.client.log.warn( { error : err.message }, 'DoorParty error');
}
// note: no explicit callback() until we're finished!
}
],
err => {
if(err) {
self.client.log.warn( { error : err.message }, 'DoorParty error');
}
// if the client is stil here, go to previous
if(!clientTerminated) {
self.prevMenu();
}
}
);
}
// if the client is stil here, go to previous
if(!clientTerminated) {
self.prevMenu();
}
}
);
}
};

View File

@ -7,72 +7,72 @@ const FileEntry = require('./file_entry.js');
const { partition } = require('lodash');
module.exports = class DownloadQueue {
constructor(client) {
this.client = client;
constructor(client) {
this.client = client;
if(!Array.isArray(this.client.user.downloadQueue)) {
if(this.client.user.properties.dl_queue) {
this.loadFromProperty(this.client.user.properties.dl_queue);
} else {
this.client.user.downloadQueue = [];
}
}
}
if(!Array.isArray(this.client.user.downloadQueue)) {
if(this.client.user.properties.dl_queue) {
this.loadFromProperty(this.client.user.properties.dl_queue);
} else {
this.client.user.downloadQueue = [];
}
}
}
get items() {
return this.client.user.downloadQueue;
}
get items() {
return this.client.user.downloadQueue;
}
clear() {
this.client.user.downloadQueue = [];
}
clear() {
this.client.user.downloadQueue = [];
}
toggle(fileEntry, systemFile=false) {
if(this.isQueued(fileEntry)) {
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId);
} else {
this.add(fileEntry, systemFile);
}
}
toggle(fileEntry, systemFile=false) {
if(this.isQueued(fileEntry)) {
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId);
} else {
this.add(fileEntry, systemFile);
}
}
add(fileEntry, systemFile=false) {
this.client.user.downloadQueue.push({
fileId : fileEntry.fileId,
areaTag : fileEntry.areaTag,
fileName : fileEntry.fileName,
path : fileEntry.filePath,
byteSize : fileEntry.meta.byte_size || 0,
systemFile : systemFile,
});
}
add(fileEntry, systemFile=false) {
this.client.user.downloadQueue.push({
fileId : fileEntry.fileId,
areaTag : fileEntry.areaTag,
fileName : fileEntry.fileName,
path : fileEntry.filePath,
byteSize : fileEntry.meta.byte_size || 0,
systemFile : systemFile,
});
}
removeItems(fileIds) {
if(!Array.isArray(fileIds)) {
fileIds = [ fileIds ];
}
removeItems(fileIds) {
if(!Array.isArray(fileIds)) {
fileIds = [ fileIds ];
}
const [ remain, removed ] = partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) ));
this.client.user.downloadQueue = remain;
return removed;
}
const [ remain, removed ] = partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) ));
this.client.user.downloadQueue = remain;
return removed;
}
isQueued(entryOrId) {
if(entryOrId instanceof FileEntry) {
entryOrId = entryOrId.fileId;
}
isQueued(entryOrId) {
if(entryOrId instanceof FileEntry) {
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) {
try {
this.client.user.downloadQueue = JSON.parse(prop);
} catch(e) {
this.client.user.downloadQueue = [];
loadFromProperty(prop) {
try {
this.client.user.downloadQueue = JSON.parse(prop);
} catch(e) {
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) {
var self = this;
this.client = client;
this.fileType = (fileType || 'DORINFO').toUpperCase();
var self = this;
this.client = client;
this.fileType = (fileType || 'DORINFO').toUpperCase();
Object.defineProperty(this, 'fullPath', {
get : function() {
return paths.join(Config().paths.dropFiles, ('node' + self.client.node), self.fileName);
}
});
Object.defineProperty(this, 'fullPath', {
get : function() {
return paths.join(Config().paths.dropFiles, ('node' + self.client.node), self.fileName);
}
});
Object.defineProperty(this, 'fileName', {
get : function() {
return {
DOOR : 'DOOR.SYS', // GAP BBS, many others
DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ...
CALLINFO : 'CALLINFO.BBS', // Citadel?
DORINFO : self.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ...
CHAIN : 'CHAIN.TXT', // WWIV
CURRUSER : 'CURRUSER.BBS', // RyBBS
SFDOORS : 'SFDOORS.DAT', // Spitfire
PCBOARD : 'PCBOARD.SYS', // PCBoard
TRIBBS : 'TRIBBS.SYS', // TriBBS
USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+
JUMPER : 'JUMPER.DAT', // 2AM BBS
SXDOOR : // System/X, dESiRE
Object.defineProperty(this, 'fileName', {
get : function() {
return {
DOOR : 'DOOR.SYS', // GAP BBS, many others
DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ...
CALLINFO : 'CALLINFO.BBS', // Citadel?
DORINFO : self.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ...
CHAIN : 'CHAIN.TXT', // WWIV
CURRUSER : 'CURRUSER.BBS', // RyBBS
SFDOORS : 'SFDOORS.DAT', // Spitfire
PCBOARD : 'PCBOARD.SYS', // PCBoard
TRIBBS : 'TRIBBS.SYS', // TriBBS
USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+
JUMPER : 'JUMPER.DAT', // 2AM BBS
SXDOOR : // System/X, dESiRE
'SXDOOR.' + _.pad(self.client.node.toString(), 3, '0'),
INFO : 'INFO.BBS', // Phoenix BBS
}[self.fileType];
}
});
INFO : 'INFO.BBS', // Phoenix BBS
}[self.fileType];
}
});
Object.defineProperty(this, 'dropFileContents', {
get : function() {
return {
DOOR : self.getDoorSysBuffer(),
DOOR32 : self.getDoor32Buffer(),
DORINFO : self.getDoorInfoDefBuffer(),
}[self.fileType];
}
});
Object.defineProperty(this, 'dropFileContents', {
get : function() {
return {
DOOR : self.getDoorSysBuffer(),
DOOR32 : self.getDoor32Buffer(),
DORINFO : self.getDoorInfoDefBuffer(),
}[self.fileType];
}
});
this.getDoorInfoFileName = function() {
var x;
var node = self.client.node;
if(10 === node) {
x = 0;
} else if(node < 10) {
x = node;
} else {
x = String.fromCharCode('a'.charCodeAt(0) + (node - 11));
}
return 'DORINFO' + x + '.DEF';
};
this.getDoorInfoFileName = function() {
var x;
var node = self.client.node;
if(10 === node) {
x = 0;
} else if(node < 10) {
x = node;
} else {
x = String.fromCharCode('a'.charCodeAt(0) + (node - 11));
}
return 'DORINFO' + x + '.DEF';
};
this.getDoorSysBuffer = function() {
var up = self.client.user.properties;
var now = moment();
var secLevel = self.client.user.getLegacySecurityLevel().toString();
this.getDoorSysBuffer = function() {
var up = self.client.user.properties;
var now = moment();
var secLevel = self.client.user.getLegacySecurityLevel().toString();
// :TODO: fix time remaining
// :TODO: fix default protocol -- user prop: transfer_protocol
// :TODO: fix time remaining
// :TODO: fix default protocol -- user prop: transfer_protocol
return iconv.encode( [
'COM1:', // "Comm Port - COM0: = LOCAL MODE"
'57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!)
'8', // "Parity - 7 or 8"
self.client.node.toString(), // "Node Number - 1 to 99"
'57600', // "DTE Rate. Actual BPS rate to use. (kg)"
'Y', // "Screen Display - 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', // "Caller Alarm - Y=On N=Off (Default to Y)"
up.real_name || self.client.user.username, // "User Full Name"
up.location || 'Anywhere', // "Calling From"
'123-456-7890', // "Home Phone"
'123-456-7890', // "Work/Data Phone"
'NOPE', // "Password" (Note: this is never given out or even stored plaintext)
secLevel, // "Security Level"
up.login_count.toString(), // "Total Times On"
now.format('MM/DD/YY'), // "Last Date Called"
'15360', // "Seconds Remaining THIS call (for those that particular)"
'256', // "Minutes Remaining THIS call"
'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller"
self.client.term.termHeight.toString(), // "Page Length"
'N', // "User Mode - Y = Expert, N = Novice"
'1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)"
'1', // "Conference Exited To DOOR From (G)"
'01/01/99', // "User Expiration Date (mm/dd/yy)"
self.client.user.userId.toString(), // "User File's Record Number"
'Z', // "Default Protocol - X, C, Y, G, I, N, Etc."
// :TODO: fix up, down, etc. form user properties
'0', // "Total Uploads"
'0', // "Total Downloads"
'0', // "Daily Download "K" Total"
'999999', // "Daily Download Max. "K" Limit"
moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate"
'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)"
'X:\\GEN\\', // "Path to the GEN directory"
StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)"
self.client.user.username, // "Alias name"
'00:05', // "Event time (hh:mm)" (note: wat?)
'Y', // "If its an error correcting connection (Y/N)"
'Y', // "ANSI supported & caller using NG mode (Y/N)"
'Y', // "Use Record Locking (Y/N)"
'7', // "BBS Default Color (Standard IBM color code, ie, 1-15)"
// :TODO: fix minutes here also:
'256', // "Time Credits In Minutes (positive/negative)"
'07/07/90', // "Last New Files Scan Date (mm/dd/yy)"
// :TODO: fix last vs now times:
now.format('hh:mm'), // "Time of This Call"
now.format('hh:mm'), // "Time of Last Call (hh:mm)"
'9999', // "Maximum daily files available"
// :TODO: fix these stats:
'0', // "Files d/led so far today"
'0', // "Total "K" Bytes Uploaded"
'0', // "Total "K" Bytes Downloaded"
up.user_comment || 'None', // "User Comment"
'0', // "Total Doors Opened"
'0', // "Total Messages Left"
return iconv.encode( [
'COM1:', // "Comm Port - COM0: = LOCAL MODE"
'57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!)
'8', // "Parity - 7 or 8"
self.client.node.toString(), // "Node Number - 1 to 99"
'57600', // "DTE Rate. Actual BPS rate to use. (kg)"
'Y', // "Screen Display - 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', // "Caller Alarm - Y=On N=Off (Default to Y)"
up.real_name || self.client.user.username, // "User Full Name"
up.location || 'Anywhere', // "Calling From"
'123-456-7890', // "Home Phone"
'123-456-7890', // "Work/Data Phone"
'NOPE', // "Password" (Note: this is never given out or even stored plaintext)
secLevel, // "Security Level"
up.login_count.toString(), // "Total Times On"
now.format('MM/DD/YY'), // "Last Date Called"
'15360', // "Seconds Remaining THIS call (for those that particular)"
'256', // "Minutes Remaining THIS call"
'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller"
self.client.term.termHeight.toString(), // "Page Length"
'N', // "User Mode - Y = Expert, N = Novice"
'1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)"
'1', // "Conference Exited To DOOR From (G)"
'01/01/99', // "User Expiration Date (mm/dd/yy)"
self.client.user.userId.toString(), // "User File's Record Number"
'Z', // "Default Protocol - X, C, Y, G, I, N, Etc."
// :TODO: fix up, down, etc. form user properties
'0', // "Total Uploads"
'0', // "Total Downloads"
'0', // "Daily Download "K" Total"
'999999', // "Daily Download Max. "K" Limit"
moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate"
'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)"
'X:\\GEN\\', // "Path to the GEN directory"
StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)"
self.client.user.username, // "Alias name"
'00:05', // "Event time (hh:mm)" (note: wat?)
'Y', // "If its an error correcting connection (Y/N)"
'Y', // "ANSI supported & caller using NG mode (Y/N)"
'Y', // "Use Record Locking (Y/N)"
'7', // "BBS Default Color (Standard IBM color code, ie, 1-15)"
// :TODO: fix minutes here also:
'256', // "Time Credits In Minutes (positive/negative)"
'07/07/90', // "Last New Files Scan Date (mm/dd/yy)"
// :TODO: fix last vs now times:
now.format('hh:mm'), // "Time of This Call"
now.format('hh:mm'), // "Time of Last Call (hh:mm)"
'9999', // "Maximum daily files available"
// :TODO: fix these stats:
'0', // "Files d/led so far today"
'0', // "Total "K" Bytes Uploaded"
'0', // "Total "K" Bytes Downloaded"
up.user_comment || 'None', // "User Comment"
'0', // "Total Doors Opened"
'0', // "Total Messages Left"
].join('\r\n') + '\r\n', 'cp437');
};
].join('\r\n') + '\r\n', 'cp437');
};
this.getDoor32Buffer = function() {
//
// Resources:
// * http://wiki.bbses.info/index.php/DOOR32.SYS
//
// :TODO: local/serial/telnet need to be configurable -- which also changes socket handle!
return iconv.encode([
'2', // :TODO: This needs to be configurable!
// :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!
'57600',
Config().general.boardName,
self.client.user.userId.toString(),
self.client.user.properties.real_name || self.client.user.username,
self.client.user.username,
self.client.user.getLegacySecurityLevel().toString(),
'546', // :TODO: Minutes left!
'1', // ANSI
self.client.node.toString(),
].join('\r\n') + '\r\n', 'cp437');
this.getDoor32Buffer = function() {
//
// Resources:
// * http://wiki.bbses.info/index.php/DOOR32.SYS
//
// :TODO: local/serial/telnet need to be configurable -- which also changes socket handle!
return iconv.encode([
'2', // :TODO: This needs to be configurable!
// :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!
'57600',
Config().general.boardName,
self.client.user.userId.toString(),
self.client.user.properties.real_name || self.client.user.username,
self.client.user.username,
self.client.user.getLegacySecurityLevel().toString(),
'546', // :TODO: Minutes left!
'1', // ANSI
self.client.node.toString(),
].join('\r\n') + '\r\n', 'cp437');
};
};
this.getDoorInfoDefBuffer = function() {
// :TODO: fix time remaining
this.getDoorInfoDefBuffer = function() {
// :TODO: fix time remaining
//
// Resources:
// * http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm
//
// Note that usernames are just used for first/last names here
//
var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0];
var un = /[^\s]*/.exec(self.client.user.username)[0];
var secLevel = self.client.user.getLegacySecurityLevel().toString();
//
// Resources:
// * http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm
//
// Note that usernames are just used for first/last names here
//
var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0];
var un = /[^\s]*/.exec(self.client.user.username)[0];
var secLevel = self.client.user.getLegacySecurityLevel().toString();
return iconv.encode( [
Config().general.boardName, // "The name of the system."
opUn, // "The sysop's name up to 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."
'57600', // "The current port (DTE) rate."
'0', // "The number "0""
un, // "The current user's name, up to 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."
'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."
'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."
].join('\r\n') + '\r\n', 'cp437');
};
return iconv.encode( [
Config().general.boardName, // "The name of the system."
opUn, // "The sysop's name up to 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."
'57600', // "The current port (DTE) rate."
'0', // "The number "0""
un, // "The current user's name, up to 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."
'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."
'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."
].join('\r\n') + '\r\n', 'cp437');
};
}
DropFile.fileTypes = [ 'DORINFO' ];
DropFile.prototype.createFile = function(cb) {
fs.writeFile(this.fullPath, this.dropFileContents, function written(err) {
cb(err);
});
fs.writeFile(this.fullPath, this.dropFileContents, function written(err) {
cb(err);
});
};

View File

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

View File

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

View File

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

View File

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

View File

@ -23,157 +23,157 @@ const net = require('net');
exports.getModule = ErcClientModule;
exports.moduleInfo = {
name : 'ENiGMA Relay Chat Client',
desc : 'Chat with other ENiGMA BBSes',
author : 'Andrew Pamment',
name : 'ENiGMA Relay Chat Client',
desc : 'Chat with other ENiGMA BBSes',
author : 'Andrew Pamment',
};
var MciViewIds = {
ChatDisplay : 1,
InputArea : 3,
ChatDisplay : 1,
InputArea : 3,
};
// :TODO: needs converted to ES6 MenuModule subclass
function ErcClientModule(options) {
MenuModule.prototype.ctorShim.call(this, options);
MenuModule.prototype.ctorShim.call(this, options);
const self = this;
this.config = options.menuConfig.config;
const self = this;
this.config = options.menuConfig.config;
this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}';
this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}';
this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}';
this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}';
this.finishedLoading = function() {
async.waterfall(
[
function validateConfig(callback) {
if(_.isString(self.config.host) &&
this.finishedLoading = function() {
async.waterfall(
[
function validateConfig(callback) {
if(_.isString(self.config.host) &&
_.isNumber(self.config.port) &&
_.isString(self.config.bbsTag))
{
return callback(null);
} else {
return callback(new Error('Configuration is missing required option(s)'));
}
},
function connectToServer(callback) {
const connectOpts = {
port : self.config.port,
host : self.config.host,
};
{
return callback(null);
} else {
return callback(new Error('Configuration is missing required option(s)'));
}
},
function connectToServer(callback) {
const connectOpts = {
port : self.config.port,
host : self.config.host,
};
const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay);
const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay);
chatMessageView.setText('Connecting to server...');
chatMessageView.redraw();
chatMessageView.setText('Connecting to server...');
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
self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host);
// :TODO: Track actual client->enig connection for optional prevMenu @ final CB
self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host);
self.chatConnection.on('data', data => {
data = data.toString();
self.chatConnection.on('data', data => {
data = data.toString();
if(data.startsWith('ERCHANDSHAKE')) {
self.chatConnection.write(`ERCMAGIC|${self.config.bbsTag}|${self.client.user.username}\r\n`);
} else if(data.startsWith('{')) {
try {
data = JSON.parse(data);
} catch(e) {
return self.client.log.warn( { error : e.message }, 'ERC: Error parsing ERC data from server');
}
if(data.startsWith('ERCHANDSHAKE')) {
self.chatConnection.write(`ERCMAGIC|${self.config.bbsTag}|${self.client.user.username}\r\n`);
} else if(data.startsWith('{')) {
try {
data = JSON.parse(data);
} catch(e) {
return self.client.log.warn( { error : e.message }, 'ERC: Error parsing ERC data from server');
}
let text;
try {
if(data.userName) {
// user message
text = stringFormat(self.chatEntryFormat, data);
} else {
// system message
text = stringFormat(self.systemEntryFormat, data);
}
} catch(e) {
return self.client.log.warn( { error : e.message }, 'ERC: chatEntryFormat error');
}
let text;
try {
if(data.userName) {
// user message
text = stringFormat(self.chatEntryFormat, data);
} else {
// system message
text = stringFormat(self.systemEntryFormat, data);
}
} catch(e) {
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?
chatMessageView.deleteLine(0);
chatMessageView.scrollDown();
}
if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height?
chatMessageView.deleteLine(0);
chatMessageView.scrollDown();
}
chatMessageView.redraw();
self.viewControllers.menu.switchFocus(MciViewIds.InputArea);
}
});
chatMessageView.redraw();
self.viewControllers.menu.switchFocus(MciViewIds.InputArea);
}
});
self.chatConnection.once('end', () => {
return callback(null);
});
self.chatConnection.once('end', () => {
return callback(null);
});
self.chatConnection.once('error', err => {
self.client.log.info(`ERC connection error: ${err.message}`);
return callback(new Error('Failed connecting to ERC server!'));
});
}
],
err => {
if(err) {
self.client.log.warn( { error : err.message }, 'ERC error');
}
self.chatConnection.once('error', err => {
self.client.log.info(`ERC connection error: ${err.message}`);
return callback(new Error('Failed connecting to ERC server!'));
});
}
],
err => {
if(err) {
self.client.log.warn( { error : err.message }, 'ERC error');
}
self.prevMenu();
}
);
};
self.prevMenu();
}
);
};
this.scrollHandler = function(keyName) {
const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea);
const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay);
this.scrollHandler = function(keyName) {
const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea);
const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay);
if('up arrow' === keyName) {
chatDisplayView.scrollUp();
} else {
chatDisplayView.scrollDown();
}
if('up arrow' === keyName) {
chatDisplayView.scrollUp();
} else {
chatDisplayView.scrollDown();
}
chatDisplayView.redraw();
inputAreaView.setFocus(true);
};
chatDisplayView.redraw();
inputAreaView.setFocus(true);
};
this.menuMethods = {
inputAreaSubmit : function(formData, extraArgs, cb) {
const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea);
const inputData = inputAreaView.getData();
this.menuMethods = {
inputAreaSubmit : function(formData, extraArgs, cb) {
const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea);
const inputData = inputAreaView.getData();
if('/quit' === inputData.toLowerCase()) {
self.chatConnection.end();
} else {
try {
self.chatConnection.write(`${inputData}\r\n`);
} catch(e) {
self.client.log.warn( { error : e.message }, 'ERC error');
}
inputAreaView.clearText();
}
return cb(null);
},
scrollUp : function(formData, extraArgs, cb) {
self.scrollHandler(formData.key.name);
return cb(null);
},
scrollDown : function(formData, extraArgs, cb) {
self.scrollHandler(formData.key.name);
return cb(null);
}
};
if('/quit' === inputData.toLowerCase()) {
self.chatConnection.end();
} else {
try {
self.chatConnection.write(`${inputData}\r\n`);
} catch(e) {
self.client.log.warn( { error : e.message }, 'ERC error');
}
inputAreaView.clearText();
}
return cb(null);
},
scrollUp : function(formData, extraArgs, cb) {
self.scrollHandler(formData.key.name);
return cb(null);
},
scrollDown : function(formData, extraArgs, cb) {
self.scrollHandler(formData.key.name);
return cb(null);
}
};
}
require('util').inherits(ErcClientModule, MenuModule);
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.moduleInfo = {
name : 'Event Scheduler',
desc : 'Support for scheduling arbritary events',
author : 'NuSkooler',
name : 'Event Scheduler',
desc : 'Support for scheduling arbritary events',
author : 'NuSkooler',
};
const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/;
const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/;
class ScheduledEvent {
constructor(events, name) {
this.name = name;
this.schedule = this.parseScheduleString(events[name].schedule);
this.action = this.parseActionSpec(events[name].action);
if(this.action) {
this.action.args = events[name].args || [];
}
}
constructor(events, name) {
this.name = name;
this.schedule = this.parseScheduleString(events[name].schedule);
this.action = this.parseActionSpec(events[name].action);
if(this.action) {
this.action.args = events[name].args || [];
}
}
get isValid() {
if((!this.schedule || (!this.schedule.sched && !this.schedule.watchFile)) || !this.action) {
return false;
}
get isValid() {
if((!this.schedule || (!this.schedule.sched && !this.schedule.watchFile)) || !this.action) {
return false;
}
if('method' === this.action.type && !this.action.location) {
return false;
}
if('method' === this.action.type && !this.action.location) {
return false;
}
return true;
}
return true;
}
parseScheduleString(schedStr) {
if(!schedStr) {
return false;
}
parseScheduleString(schedStr) {
if(!schedStr) {
return false;
}
let schedule = {};
let schedule = {};
const m = SCHEDULE_REGEXP.exec(schedStr);
if(m) {
schedStr = schedStr.substr(0, m.index).trim();
const m = SCHEDULE_REGEXP.exec(schedStr);
if(m) {
schedStr = schedStr.substr(0, m.index).trim();
if('@watch:' === m[1]) {
schedule.watchFile = m[2];
}
}
if('@watch:' === m[1]) {
schedule.watchFile = m[2];
}
}
if(schedStr.length > 0) {
const sched = later.parse.text(schedStr);
if(-1 === sched.error) {
schedule.sched = sched;
}
}
if(schedStr.length > 0) {
const sched = later.parse.text(schedStr);
if(-1 === sched.error) {
schedule.sched = sched;
}
}
// return undefined if we couldn't parse out anything useful
if(!_.isEmpty(schedule)) {
return schedule;
}
}
// return undefined if we couldn't parse out anything useful
if(!_.isEmpty(schedule)) {
return schedule;
}
}
parseActionSpec(actionSpec) {
if(actionSpec) {
if('@' === actionSpec[0]) {
const m = ACTION_REGEXP.exec(actionSpec);
if(m) {
if(m[2].indexOf(':') > -1) {
const parts = m[2].split(':');
return {
type : m[1],
location : parts[0],
what : parts[1],
};
} else {
return {
type : m[1],
what : m[2],
};
}
}
} else {
return {
type : 'execute',
what : actionSpec,
};
}
}
}
parseActionSpec(actionSpec) {
if(actionSpec) {
if('@' === actionSpec[0]) {
const m = ACTION_REGEXP.exec(actionSpec);
if(m) {
if(m[2].indexOf(':') > -1) {
const parts = m[2].split(':');
return {
type : m[1],
location : parts[0],
what : parts[1],
};
} else {
return {
type : m[1],
what : m[2],
};
}
}
} else {
return {
type : 'execute',
what : actionSpec,
};
}
}
}
executeAction(reason, cb) {
Log.info( { eventName : this.name, action : this.action, reason : reason }, 'Executing scheduled event action...');
executeAction(reason, cb) {
Log.info( { eventName : this.name, action : this.action, reason : reason }, 'Executing scheduled event action...');
if('method' === this.action.type) {
const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js')
try {
const methodModule = require(modulePath);
methodModule[this.action.what](this.action.args, err => {
if(err) {
Log.debug(
{ error : err.toString(), eventName : this.name, action : this.action },
'Error performing scheduled event action');
}
if('method' === this.action.type) {
const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js')
try {
const methodModule = require(modulePath);
methodModule[this.action.what](this.action.args, err => {
if(err) {
Log.debug(
{ error : err.toString(), eventName : this.name, action : this.action },
'Error performing scheduled event action');
}
return cb(err);
});
} catch(e) {
Log.warn(
{ error : e.toString(), eventName : this.name, action : this.action },
'Failed to perform scheduled event action');
return cb(err);
});
} catch(e) {
Log.warn(
{ error : e.toString(), eventName : this.name, action : this.action },
'Failed to perform scheduled event action');
return cb(e);
}
} else if('execute' === this.action.type) {
const opts = {
// :TODO: cwd
name : this.name,
cols : 80,
rows : 24,
env : process.env,
};
return cb(e);
}
} else if('execute' === this.action.type) {
const opts = {
// :TODO: cwd
name : this.name,
cols : 80,
rows : 24,
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 => {
if(exitCode) {
Log.warn(
{ eventName : this.name, action : this.action, exitCode : exitCode },
'Bad exit code while performing scheduled event action');
}
return cb(exitCode ? new Error(`Bad exit code while performing scheduled event action: ${exitCode}`) : null);
});
}
}
proc.once('exit', exitCode => {
if(exitCode) {
Log.warn(
{ eventName : this.name, action : this.action, exitCode : exitCode },
'Bad exit code while performing scheduled event action');
}
return cb(exitCode ? new Error(`Bad exit code while performing scheduled event action: ${exitCode}`) : null);
});
}
}
}
function EventSchedulerModule(options) {
PluginModule.call(this, options);
PluginModule.call(this, options);
const config = Config();
if(_.has(config, 'eventScheduler')) {
this.moduleConfig = config.eventScheduler;
}
const config = Config();
if(_.has(config, 'eventScheduler')) {
this.moduleConfig = config.eventScheduler;
}
const self = this;
this.runningActions = new Set();
const self = this;
this.runningActions = new Set();
this.performAction = function(schedEvent, reason) {
if(self.runningActions.has(schedEvent.name)) {
return; // already running
}
this.performAction = function(schedEvent, reason) {
if(self.runningActions.has(schedEvent.name)) {
return; // already running
}
self.runningActions.add(schedEvent.name);
self.runningActions.add(schedEvent.name);
schedEvent.executeAction(reason, () => {
self.runningActions.delete(schedEvent.name);
});
};
schedEvent.executeAction(reason, () => {
self.runningActions.delete(schedEvent.name);
});
};
}
// convienence static method for direct load + start
EventSchedulerModule.loadAndStart = function(cb) {
const loadModuleEx = require('./module_util.js').loadModuleEx;
const loadModuleEx = require('./module_util.js').loadModuleEx;
const loadOpts = {
name : path.basename(__filename, '.js'),
path : __dirname,
};
const loadOpts = {
name : path.basename(__filename, '.js'),
path : __dirname,
};
loadModuleEx(loadOpts, (err, mod) => {
if(err) {
return cb(err);
}
loadModuleEx(loadOpts, (err, mod) => {
if(err) {
return cb(err);
}
const modInst = new mod.getModule();
modInst.startup( err => {
return cb(err, modInst);
});
});
const modInst = new mod.getModule();
modInst.startup( err => {
return cb(err, modInst);
});
});
};
EventSchedulerModule.prototype.startup = function(cb) {
this.eventTimers = [];
const self = this;
this.eventTimers = [];
const self = this;
if(this.moduleConfig && _.has(this.moduleConfig, 'events')) {
const events = Object.keys(this.moduleConfig.events).map( name => {
return new ScheduledEvent(this.moduleConfig.events, name);
});
if(this.moduleConfig && _.has(this.moduleConfig, 'events')) {
const events = Object.keys(this.moduleConfig.events).map( name => {
return new ScheduledEvent(this.moduleConfig.events, name);
});
events.forEach( schedEvent => {
if(!schedEvent.isValid) {
Log.warn( { eventName : schedEvent.name }, 'Invalid scheduled event entry');
return;
}
events.forEach( schedEvent => {
if(!schedEvent.isValid) {
Log.warn( { eventName : schedEvent.name }, 'Invalid scheduled event entry');
return;
}
Log.debug(
{
eventName : schedEvent.name,
schedule : this.moduleConfig.events[schedEvent.name].schedule,
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',
},
'Scheduled event loaded'
);
Log.debug(
{
eventName : schedEvent.name,
schedule : this.moduleConfig.events[schedEvent.name].schedule,
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',
},
'Scheduled event loaded'
);
if(schedEvent.schedule.sched) {
this.eventTimers.push(later.setInterval( () => {
self.performAction(schedEvent, 'Schedule');
}, schedEvent.schedule.sched));
}
if(schedEvent.schedule.sched) {
this.eventTimers.push(later.setInterval( () => {
self.performAction(schedEvent, 'Schedule');
}, schedEvent.schedule.sched));
}
if(schedEvent.schedule.watchFile) {
const watcher = sane(
paths.dirname(schedEvent.schedule.watchFile),
{
glob : `**/${paths.basename(schedEvent.schedule.watchFile)}`
}
);
if(schedEvent.schedule.watchFile) {
const watcher = sane(
paths.dirname(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 => {
watcher.on(event, (fileName, fileRoot) => {
const eventPath = paths.join(fileRoot, fileName);
if(schedEvent.schedule.watchFile === eventPath) {
self.performAction(schedEvent, `Watch file: ${eventPath}`);
}
});
});
[ 'change', 'add', 'delete' ].forEach(event => {
watcher.on(event, (fileName, fileRoot) => {
const eventPath = paths.join(fileRoot, fileName);
if(schedEvent.schedule.watchFile === eventPath) {
self.performAction(schedEvent, `Watch file: ${eventPath}`);
}
});
});
fse.exists(schedEvent.schedule.watchFile, exists => {
if(exists) {
self.performAction(schedEvent, `Watch file: ${schedEvent.schedule.watchFile}`);
}
});
}
});
}
fse.exists(schedEvent.schedule.watchFile, exists => {
if(exists) {
self.performAction(schedEvent, `Watch file: ${schedEvent.schedule.watchFile}`);
}
});
}
});
}
cb(null);
cb(null);
};
EventSchedulerModule.prototype.shutdown = function(cb) {
if(this.eventTimers) {
this.eventTimers.forEach( et => et.clear() );
}
if(this.eventTimers) {
this.eventTimers.forEach( et => et.clear() );
}
cb(null);
cb(null);
};

View File

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

View File

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

View File

@ -12,328 +12,328 @@ const stringFormat = require('./string_format.js');
const async = require('async');
exports.moduleInfo = {
name : 'File Area Filter Editor',
desc : 'Module for adding, deleting, and modifying file base filters',
author : 'NuSkooler',
name : 'File Area Filter Editor',
desc : 'Module for adding, deleting, and modifying file base filters',
author : 'NuSkooler',
};
const MciViewIds = {
editor : {
searchTerms : 1,
tags : 2,
area : 3,
sort : 4,
order : 5,
filterName : 6,
navMenu : 7,
editor : {
searchTerms : 1,
tags : 2,
area : 3,
sort : 4,
order : 5,
filterName : 6,
navMenu : 7,
// :TODO: use the customs new standard thing - filter obj can have active/selected, etc.
selectedFilterInfo : 10, // { ...filter object ... }
activeFilterInfo : 11, // { ...filter object ... }
error : 12, // validation errors
}
// :TODO: use the customs new standard thing - filter obj can have active/selected, etc.
selectedFilterInfo : 10, // { ...filter object ... }
activeFilterInfo : 11, // { ...filter object ... }
error : 12, // validation errors
}
};
exports.getModule = class FileAreaFilterEdit extends MenuModule {
constructor(options) {
super(options);
constructor(options) {
super(options);
this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them
this.currentFilterIndex = 0; // into |filtersArray|
this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them
this.currentFilterIndex = 0; // into |filtersArray|
//
// Lexical sort + keep currently active filter (if any) as the first item in |filtersArray|
//
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
this.filtersArray.sort( (filterA, filterB) => {
if(activeFilter) {
if(filterA.uuid === activeFilter.uuid) {
return -1;
}
if(filterB.uuid === activeFilter.uuid) {
return 1;
}
}
//
// Lexical sort + keep currently active filter (if any) as the first item in |filtersArray|
//
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
this.filtersArray.sort( (filterA, filterB) => {
if(activeFilter) {
if(filterA.uuid === activeFilter.uuid) {
return -1;
}
if(filterB.uuid === activeFilter.uuid) {
return 1;
}
}
return filterA.name.localeCompare(filterB.name, { sensitivity : false, numeric : true } );
});
return filterA.name.localeCompare(filterB.name, { sensitivity : false, numeric : true } );
});
this.menuMethods = {
saveFilter : (formData, extraArgs, cb) => {
return this.saveCurrentFilter(formData, cb);
},
prevFilter : (formData, extraArgs, cb) => {
this.currentFilterIndex -= 1;
if(this.currentFilterIndex < 0) {
this.currentFilterIndex = this.filtersArray.length - 1;
}
this.loadDataForFilter(this.currentFilterIndex);
return cb(null);
},
nextFilter : (formData, extraArgs, cb) => {
this.currentFilterIndex += 1;
if(this.currentFilterIndex >= this.filtersArray.length) {
this.currentFilterIndex = 0;
}
this.loadDataForFilter(this.currentFilterIndex);
return cb(null);
},
makeFilterActive : (formData, extraArgs, cb) => {
const filters = new FileBaseFilters(this.client);
filters.setActive(this.filtersArray[this.currentFilterIndex].uuid);
this.menuMethods = {
saveFilter : (formData, extraArgs, cb) => {
return this.saveCurrentFilter(formData, cb);
},
prevFilter : (formData, extraArgs, cb) => {
this.currentFilterIndex -= 1;
if(this.currentFilterIndex < 0) {
this.currentFilterIndex = this.filtersArray.length - 1;
}
this.loadDataForFilter(this.currentFilterIndex);
return cb(null);
},
nextFilter : (formData, extraArgs, cb) => {
this.currentFilterIndex += 1;
if(this.currentFilterIndex >= this.filtersArray.length) {
this.currentFilterIndex = 0;
}
this.loadDataForFilter(this.currentFilterIndex);
return cb(null);
},
makeFilterActive : (formData, extraArgs, cb) => {
const filters = new FileBaseFilters(this.client);
filters.setActive(this.filtersArray[this.currentFilterIndex].uuid);
this.updateActiveLabel();
this.updateActiveLabel();
return cb(null);
},
newFilter : (formData, extraArgs, cb) => {
this.currentFilterIndex = this.filtersArray.length; // next avail slot
this.clearForm(MciViewIds.editor.searchTerms);
return cb(null);
},
deleteFilter : (formData, extraArgs, cb) => {
const selectedFilter = this.filtersArray[this.currentFilterIndex];
const filterUuid = selectedFilter.uuid;
return cb(null);
},
newFilter : (formData, extraArgs, cb) => {
this.currentFilterIndex = this.filtersArray.length; // next avail slot
this.clearForm(MciViewIds.editor.searchTerms);
return cb(null);
},
deleteFilter : (formData, extraArgs, cb) => {
const selectedFilter = this.filtersArray[this.currentFilterIndex];
const filterUuid = selectedFilter.uuid;
// cannot delete built-in/system filters
if(true === selectedFilter.system) {
this.showError('Cannot delete built in filters!');
return cb(null);
}
// cannot delete built-in/system filters
if(true === selectedFilter.system) {
this.showError('Cannot delete built in filters!');
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
const filters = new FileBaseFilters(this.client);
filters.remove(filterUuid);
filters.persist( () => {
// remove from stored properties
const filters = new FileBaseFilters(this.client);
filters.remove(filterUuid);
filters.persist( () => {
//
// 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) {
const newActive = this.filtersArray[this.currentFilterIndex];
if(newActive) {
filters.setActive(newActive.uuid);
} else {
// nothing to set active to
this.client.user.removeProperty('file_base_filter_active_uuid');
}
}
//
// 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) {
const newActive = this.filtersArray[this.currentFilterIndex];
if(newActive) {
filters.setActive(newActive.uuid);
} else {
// nothing to set active to
this.client.user.removeProperty('file_base_filter_active_uuid');
}
}
// update UI
this.updateActiveLabel();
// update UI
this.updateActiveLabel();
if(this.filtersArray.length > 0) {
this.loadDataForFilter(this.currentFilterIndex);
} else {
this.clearForm();
}
return cb(null);
});
},
if(this.filtersArray.length > 0) {
this.loadDataForFilter(this.currentFilterIndex);
} else {
this.clearForm();
}
return cb(null);
});
},
viewValidationListener : (err, cb) => {
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
let newFocusId;
viewValidationListener : (err, cb) => {
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
let newFocusId;
if(errorView) {
if(err) {
errorView.setText(err.message);
err.view.clearText(); // clear out the invalid data
} else {
errorView.clearText();
}
}
if(errorView) {
if(err) {
errorView.setText(err.message);
err.view.clearText(); // clear out the invalid data
} else {
errorView.clearText();
}
}
return cb(newFocusId);
},
};
}
return cb(newFocusId);
},
};
}
showError(errMsg) {
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
if(errorView) {
if(errMsg) {
errorView.setText(errMsg);
} else {
errorView.clearText();
}
}
}
showError(errMsg) {
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
if(errorView) {
if(errMsg) {
errorView.setText(errMsg);
} else {
errorView.clearText();
}
}
}
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
const self = this;
const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) );
const self = this;
const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) );
async.series(
[
function loadFromConfig(callback) {
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
},
function populateAreas(callback) {
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []);
async.series(
[
function loadFromConfig(callback) {
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
},
function populateAreas(callback) {
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []);
const areasView = vc.getView(MciViewIds.editor.area);
if(areasView) {
areasView.setItems( self.availAreas.map( a => a.name ) );
}
const areasView = vc.getView(MciViewIds.editor.area);
if(areasView) {
areasView.setItems( self.availAreas.map( a => a.name ) );
}
self.updateActiveLabel();
self.loadDataForFilter(self.currentFilterIndex);
self.viewControllers.editor.resetInitialFocus();
return callback(null);
}
],
err => {
return cb(err);
}
);
});
}
self.updateActiveLabel();
self.loadDataForFilter(self.currentFilterIndex);
self.viewControllers.editor.resetInitialFocus();
return callback(null);
}
],
err => {
return cb(err);
}
);
});
}
getCurrentFilter() {
return this.filtersArray[this.currentFilterIndex];
}
getCurrentFilter() {
return this.filtersArray[this.currentFilterIndex];
}
setText(mciId, text) {
const view = this.viewControllers.editor.getView(mciId);
if(view) {
view.setText(text);
}
}
setText(mciId, text) {
const view = this.viewControllers.editor.getView(mciId);
if(view) {
view.setText(text);
}
}
updateActiveLabel() {
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
if(activeFilter) {
const activeFormat = this.menuConfig.config.activeFormat || '{name}';
this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter));
}
}
updateActiveLabel() {
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
if(activeFilter) {
const activeFormat = this.menuConfig.config.activeFormat || '{name}';
this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter));
}
}
setFocusItemIndex(mciId, index) {
const view = this.viewControllers.editor.getView(mciId);
if(view) {
view.setFocusItemIndex(index);
}
}
setFocusItemIndex(mciId, index) {
const view = this.viewControllers.editor.getView(mciId);
if(view) {
view.setFocusItemIndex(index);
}
}
clearForm(newFocusId) {
[ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => {
this.setText(mciId, '');
});
clearForm(newFocusId) {
[ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => {
this.setText(mciId, '');
});
[ MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort ].forEach(mciId => {
this.setFocusItemIndex(mciId, 0);
});
[ MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort ].forEach(mciId => {
this.setFocusItemIndex(mciId, 0);
});
if(newFocusId) {
this.viewControllers.editor.switchFocus(newFocusId);
} else {
this.viewControllers.editor.resetInitialFocus();
}
}
if(newFocusId) {
this.viewControllers.editor.switchFocus(newFocusId);
} else {
this.viewControllers.editor.resetInitialFocus();
}
}
getSelectedAreaTag(index) {
if(0 === index) {
return ''; // -ALL-
}
const area = this.availAreas[index];
if(!area) {
return '';
}
return area.areaTag;
}
getSelectedAreaTag(index) {
if(0 === index) {
return ''; // -ALL-
}
const area = this.availAreas[index];
if(!area) {
return '';
}
return area.areaTag;
}
getOrderBy(index) {
return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0];
}
getOrderBy(index) {
return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0];
}
setAreaIndexFromCurrentFilter() {
let index;
const filter = this.getCurrentFilter();
if(filter) {
// special treatment: areaTag saved as blank ("") if -ALL-
index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0;
} else {
index = 0;
}
this.setFocusItemIndex(MciViewIds.editor.area, index);
}
setAreaIndexFromCurrentFilter() {
let index;
const filter = this.getCurrentFilter();
if(filter) {
// special treatment: areaTag saved as blank ("") if -ALL-
index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0;
} else {
index = 0;
}
this.setFocusItemIndex(MciViewIds.editor.area, index);
}
setOrderByFromCurrentFilter() {
let index;
const filter = this.getCurrentFilter();
if(filter) {
index = FileBaseFilters.OrderByValues.findIndex( ob => filter.order === ob ) || 0;
} else {
index = 0;
}
this.setFocusItemIndex(MciViewIds.editor.order, index);
}
setOrderByFromCurrentFilter() {
let index;
const filter = this.getCurrentFilter();
if(filter) {
index = FileBaseFilters.OrderByValues.findIndex( ob => filter.order === ob ) || 0;
} else {
index = 0;
}
this.setFocusItemIndex(MciViewIds.editor.order, index);
}
setSortByFromCurrentFilter() {
let index;
const filter = this.getCurrentFilter();
if(filter) {
index = FileBaseFilters.SortByValues.findIndex( sb => filter.sort === sb ) || 0;
} else {
index = 0;
}
this.setFocusItemIndex(MciViewIds.editor.sort, index);
}
setSortByFromCurrentFilter() {
let index;
const filter = this.getCurrentFilter();
if(filter) {
index = FileBaseFilters.SortByValues.findIndex( sb => filter.sort === sb ) || 0;
} else {
index = 0;
}
this.setFocusItemIndex(MciViewIds.editor.sort, index);
}
getSortBy(index) {
return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0];
}
getSortBy(index) {
return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0];
}
setFilterValuesFromFormData(filter, formData) {
filter.name = formData.value.name;
filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex);
filter.terms = formData.value.searchTerms;
filter.tags = formData.value.tags;
filter.order = this.getOrderBy(formData.value.orderByIndex);
filter.sort = this.getSortBy(formData.value.sortByIndex);
}
setFilterValuesFromFormData(filter, formData) {
filter.name = formData.value.name;
filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex);
filter.terms = formData.value.searchTerms;
filter.tags = formData.value.tags;
filter.order = this.getOrderBy(formData.value.orderByIndex);
filter.sort = this.getSortBy(formData.value.sortByIndex);
}
saveCurrentFilter(formData, cb) {
const filters = new FileBaseFilters(this.client);
const selectedFilter = this.filtersArray[this.currentFilterIndex];
saveCurrentFilter(formData, cb) {
const filters = new FileBaseFilters(this.client);
const selectedFilter = this.filtersArray[this.currentFilterIndex];
if(selectedFilter) {
// *update* currently selected filter
this.setFilterValuesFromFormData(selectedFilter, formData);
filters.replace(selectedFilter.uuid, selectedFilter);
} else {
// add a new entry; note that UUID will be generated
const newFilter = {};
this.setFilterValuesFromFormData(newFilter, formData);
if(selectedFilter) {
// *update* currently selected filter
this.setFilterValuesFromFormData(selectedFilter, formData);
filters.replace(selectedFilter.uuid, selectedFilter);
} else {
// add a new entry; note that UUID will be generated
const newFilter = {};
this.setFilterValuesFromFormData(newFilter, formData);
// set current to what we just saved
newFilter.uuid = filters.add(newFilter);
// set current to what we just saved
newFilter.uuid = filters.add(newFilter);
// add to our array (at current index position)
this.filtersArray[this.currentFilterIndex] = newFilter;
}
// add to our array (at current index position)
this.filtersArray[this.currentFilterIndex] = newFilter;
}
return filters.persist(cb);
}
return filters.persist(cb);
}
loadDataForFilter(filterIndex) {
const filter = this.filtersArray[filterIndex];
if(filter) {
this.setText(MciViewIds.editor.searchTerms, filter.terms);
this.setText(MciViewIds.editor.tags, filter.tags);
this.setText(MciViewIds.editor.filterName, filter.name);
loadDataForFilter(filterIndex) {
const filter = this.filtersArray[filterIndex];
if(filter) {
this.setText(MciViewIds.editor.searchTerms, filter.terms);
this.setText(MciViewIds.editor.tags, filter.tags);
this.setText(MciViewIds.editor.filterName, filter.name);
this.setAreaIndexFromCurrentFilter();
this.setSortByFromCurrentFilter();
this.setOrderByFromCurrentFilter();
}
}
this.setAreaIndexFromCurrentFilter();
this.setSortByFromCurrentFilter();
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');
function notEnabledError() {
return Errors.General('Web server is not enabled', ErrNotEnabled);
return Errors.General('Web server is not enabled', ErrNotEnabled);
}
class FileAreaWebAccess {
constructor() {
this.hashids = new hashids(Config().general.boardName);
this.expireTimers = {}; // hashId->timer
}
constructor() {
this.hashids = new hashids(Config().general.boardName);
this.expireTimers = {}; // hashId->timer
}
startup(cb) {
const self = this;
startup(cb) {
const self = this;
async.series(
[
function initFromDb(callback) {
return self.load(callback);
},
function addWebRoute(callback) {
self.webServer = getServer(webServerPackageName);
if(!self.webServer) {
return callback(Errors.DoesNotExist(`Server with package name "${webServerPackageName}" does not exist`));
}
async.series(
[
function initFromDb(callback) {
return self.load(callback);
},
function addWebRoute(callback) {
self.webServer = getServer(webServerPackageName);
if(!self.webServer) {
return callback(Errors.DoesNotExist(`Server with package name "${webServerPackageName}" does not exist`));
}
if(self.isEnabled()) {
const routeAdded = self.webServer.instance.addRoute({
method : 'GET',
path : Config().fileBase.web.routePath,
handler : self.routeWebRequest.bind(self),
});
return callback(routeAdded ? null : Errors.General('Failed adding route'));
} else {
return callback(null); // not enabled, but no error
}
}
],
err => {
return cb(err);
}
);
}
if(self.isEnabled()) {
const routeAdded = self.webServer.instance.addRoute({
method : 'GET',
path : Config().fileBase.web.routePath,
handler : self.routeWebRequest.bind(self),
});
return callback(routeAdded ? null : Errors.General('Failed adding route'));
} else {
return callback(null); // not enabled, but no error
}
}
],
err => {
return cb(err);
}
);
}
shutdown(cb) {
return cb(null);
}
shutdown(cb) {
return cb(null);
}
isEnabled() {
return this.webServer.instance.isEnabled();
}
isEnabled() {
return this.webServer.instance.isEnabled();
}
static getHashIdTypes() {
return {
SingleFile : 0,
BatchArchive : 1,
};
}
static getHashIdTypes() {
return {
SingleFile : 0,
BatchArchive : 1,
};
}
load(cb) {
//
// Load entries, register expiration timers
//
FileDb.each(
`SELECT hash_id, expire_timestamp
load(cb) {
//
// Load entries, register expiration timers
//
FileDb.each(
`SELECT hash_id, expire_timestamp
FROM file_web_serve;`,
(err, row) => {
if(row) {
this.scheduleExpire(row.hash_id, moment(row.expire_timestamp));
}
},
err => {
return cb(err);
}
);
}
(err, row) => {
if(row) {
this.scheduleExpire(row.hash_id, moment(row.expire_timestamp));
}
},
err => {
return cb(err);
}
);
}
removeEntry(hashId) {
//
// Delete record from DB, and our timer
//
FileDb.run(
`DELETE FROM file_web_serve
removeEntry(hashId) {
//
// Delete record from DB, and our timer
//
FileDb.run(
`DELETE FROM file_web_serve
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
const previous = this.expireTimers[hashId];
if(previous) {
clearTimeout(previous);
delete this.expireTimers[hashId];
}
// remove any previous entry for this hashId
const previous = this.expireTimers[hashId];
if(previous) {
clearTimeout(previous);
delete this.expireTimers[hashId];
}
const timeoutMs = expireTime.diff(moment());
const timeoutMs = expireTime.diff(moment());
if(timeoutMs <= 0) {
setImmediate( () => {
this.removeEntry(hashId);
});
} else {
this.expireTimers[hashId] = setTimeout( () => {
this.removeEntry(hashId);
}, timeoutMs);
}
}
if(timeoutMs <= 0) {
setImmediate( () => {
this.removeEntry(hashId);
});
} else {
this.expireTimers[hashId] = setTimeout( () => {
this.removeEntry(hashId);
}, timeoutMs);
}
}
loadServedHashId(hashId, cb) {
FileDb.get(
`SELECT expire_timestamp FROM
loadServedHashId(hashId, cb) {
FileDb.get(
`SELECT expire_timestamp FROM
file_web_serve
WHERE hash_id = ?`,
[ hashId ],
(err, result) => {
if(err || !result) {
return cb(err ? err : Errors.DoesNotExist('Invalid or missing hash ID'));
}
[ hashId ],
(err, result) => {
if(err || !result) {
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, ... ]
if(!Array.isArray(decoded) || decoded.length < 3) {
return cb(Errors.Invalid('Invalid or unknown hash ID'));
}
// decode() should provide an array of [ userId, hashIdType, id, ... ]
if(!Array.isArray(decoded) || decoded.length < 3) {
return cb(Errors.Invalid('Invalid or unknown hash ID'));
}
const servedItem = {
hashId : hashId,
userId : decoded[0],
hashIdType : decoded[1],
expireTimestamp : moment(result.expire_timestamp),
};
const servedItem = {
hashId : hashId,
userId : decoded[0],
hashIdType : decoded[1],
expireTimestamp : moment(result.expire_timestamp),
};
if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) {
servedItem.fileIds = decoded.slice(2);
}
if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) {
servedItem.fileIds = decoded.slice(2);
}
return cb(null, servedItem);
}
);
}
return cb(null, servedItem);
}
);
}
getSingleFileHashId(client, fileEntry) {
return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [ fileEntry.fileId ] );
}
getSingleFileHashId(client, fileEntry) {
return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [ fileEntry.fileId ] );
}
getBatchArchiveHashId(client, batchId) {
return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().BatchArchive, batchId);
}
getBatchArchiveHashId(client, batchId) {
return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().BatchArchive, batchId);
}
getHashId(client, hashIdType, identifier) {
return this.hashids.encode(client.user.userId, hashIdType, identifier);
}
getHashId(client, hashIdType, identifier) {
return this.hashids.encode(client.user.userId, hashIdType, identifier);
}
buildSingleFileTempDownloadLink(client, fileEntry, hashId) {
hashId = hashId || this.getSingleFileHashId(client, fileEntry);
buildSingleFileTempDownloadLink(client, fileEntry, hashId) {
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) {
return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`);
}
buildBatchArchiveTempDownloadLink(client, hashId) {
return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`);
}
getExistingTempDownloadServeItem(client, fileEntry, cb) {
if(!this.isEnabled()) {
return cb(notEnabledError());
}
getExistingTempDownloadServeItem(client, fileEntry, cb) {
if(!this.isEnabled()) {
return cb(notEnabledError());
}
const hashId = this.getSingleFileHashId(client, fileEntry);
this.loadServedHashId(hashId, (err, servedItem) => {
if(err) {
return cb(err);
}
const hashId = this.getSingleFileHashId(client, fileEntry);
this.loadServedHashId(hashId, (err, servedItem) => {
if(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) {
// add/update rec with hash id and (latest) timestamp
dbOrTrans.run(
`REPLACE INTO file_web_serve (hash_id, expire_timestamp)
_addOrUpdateHashIdRecord(dbOrTrans, hashId, expireTime, cb) {
// add/update rec with hash id and (latest) timestamp
dbOrTrans.run(
`REPLACE INTO file_web_serve (hash_id, expire_timestamp)
VALUES (?, ?);`,
[ hashId, getISOTimestampString(expireTime) ],
err => {
if(err) {
return cb(err);
}
[ hashId, getISOTimestampString(expireTime) ],
err => {
if(err) {
return cb(err);
}
this.scheduleExpire(hashId, expireTime);
this.scheduleExpire(hashId, expireTime);
return cb(null);
}
);
}
return cb(null);
}
);
}
createAndServeTempDownload(client, fileEntry, options, cb) {
if(!this.isEnabled()) {
return cb(notEnabledError());
}
createAndServeTempDownload(client, fileEntry, options, cb) {
if(!this.isEnabled()) {
return cb(notEnabledError());
}
const hashId = this.getSingleFileHashId(client, fileEntry);
const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId);
options.expireTime = options.expireTime || moment().add(2, 'days');
const hashId = this.getSingleFileHashId(client, fileEntry);
const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId);
options.expireTime = options.expireTime || moment().add(2, 'days');
this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => {
return cb(err, url);
});
}
this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => {
return cb(err, url);
});
}
createAndServeTempBatchDownload(client, fileEntries, options, cb) {
if(!this.isEnabled()) {
return cb(notEnabledError());
}
createAndServeTempBatchDownload(client, fileEntries, options, cb) {
if(!this.isEnabled()) {
return cb(notEnabledError());
}
const batchId = moment().utc().unix();
const hashId = this.getBatchArchiveHashId(client, batchId);
const url = this.buildBatchArchiveTempDownloadLink(client, hashId);
options.expireTime = options.expireTime || moment().add(2, 'days');
const batchId = moment().utc().unix();
const hashId = this.getBatchArchiveHashId(client, batchId);
const url = this.buildBatchArchiveTempDownloadLink(client, hashId);
options.expireTime = options.expireTime || moment().add(2, 'days');
FileDb.beginTransaction( (err, trans) => {
if(err) {
return cb(err);
}
FileDb.beginTransaction( (err, trans) => {
if(err) {
return cb(err);
}
this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => {
if(err) {
return trans.rollback( () => {
return cb(err);
});
}
this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => {
if(err) {
return trans.rollback( () => {
return cb(err);
});
}
async.eachSeries(fileEntries, (entry, nextEntry) => {
trans.run(
`INSERT INTO file_web_serve_batch (hash_id, file_id)
async.eachSeries(fileEntries, (entry, nextEntry) => {
trans.run(
`INSERT INTO file_web_serve_batch (hash_id, file_id)
VALUES (?, ?);`,
[ hashId, entry.fileId ],
err => {
return nextEntry(err);
}
);
}, err => {
trans[err ? 'rollback' : 'commit']( () => {
return cb(err, url);
});
});
});
});
}
[ hashId, entry.fileId ],
err => {
return nextEntry(err);
}
);
}, err => {
trans[err ? 'rollback' : 'commit']( () => {
return cb(err, url);
});
});
});
});
}
fileNotFound(resp) {
return this.webServer.instance.fileNotFound(resp);
}
fileNotFound(resp) {
return this.webServer.instance.fileNotFound(resp);
}
routeWebRequest(req, resp) {
const hashId = paths.basename(req.url);
routeWebRequest(req, resp) {
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) {
return this.fileNotFound(resp);
}
if(err) {
return this.fileNotFound(resp);
}
const hashIdTypes = FileAreaWebAccess.getHashIdTypes();
switch(servedItem.hashIdType) {
case hashIdTypes.SingleFile :
return this.routeWebRequestForSingleFile(servedItem, req, resp);
const hashIdTypes = FileAreaWebAccess.getHashIdTypes();
switch(servedItem.hashIdType) {
case hashIdTypes.SingleFile :
return this.routeWebRequestForSingleFile(servedItem, req, resp);
case hashIdTypes.BatchArchive :
return this.routeWebRequestForBatchArchive(servedItem, req, resp);
case hashIdTypes.BatchArchive :
return this.routeWebRequestForBatchArchive(servedItem, req, resp);
default :
return this.fileNotFound(resp);
}
});
}
default :
return this.fileNotFound(resp);
}
});
}
routeWebRequestForSingleFile(servedItem, req, resp) {
Log.debug( { servedItem : servedItem }, 'Single file web request');
routeWebRequestForSingleFile(servedItem, req, resp) {
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 => {
if(err) {
return this.fileNotFound(resp);
}
fileEntry.load(servedItem.fileId, err => {
if(err) {
return this.fileNotFound(resp);
}
const filePath = fileEntry.filePath;
if(!filePath) {
return this.fileNotFound(resp);
}
const filePath = fileEntry.filePath;
if(!filePath) {
return this.fileNotFound(resp);
}
fs.stat(filePath, (err, stats) => {
if(err) {
return this.fileNotFound(resp);
}
fs.stat(filePath, (err, stats) => {
if(err) {
return this.fileNotFound(resp);
}
resp.on('close', () => {
// connection closed *before* the response was fully sent
// :TODO: Log and such
});
resp.on('close', () => {
// connection closed *before* the response was fully sent
// :TODO: Log and such
});
resp.on('finish', () => {
// transfer completed fully
this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]);
});
resp.on('finish', () => {
// transfer completed fully
this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]);
});
const headers = {
'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'),
'Content-Length' : stats.size,
'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`,
};
const headers = {
'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'),
'Content-Length' : stats.size,
'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`,
};
const readStream = fs.createReadStream(filePath);
resp.writeHead(200, headers);
return readStream.pipe(resp);
});
});
}
const readStream = fs.createReadStream(filePath);
resp.writeHead(200, headers);
return readStream.pipe(resp);
});
});
}
routeWebRequestForBatchArchive(servedItem, req, resp) {
Log.debug( { servedItem : servedItem }, 'Batch file web request');
routeWebRequestForBatchArchive(servedItem, req, resp) {
Log.debug( { servedItem : servedItem }, 'Batch file web request');
//
// We are going to build an on-the-fly zip file stream of 1:n
// files in the batch.
//
// First, collect all file IDs
//
const self = this;
//
// We are going to build an on-the-fly zip file stream of 1:n
// files in the batch.
//
// First, collect all file IDs
//
const self = this;
async.waterfall(
[
function fetchFileIds(callback) {
FileDb.all(
`SELECT file_id
async.waterfall(
[
function fetchFileIds(callback) {
FileDb.all(
`SELECT file_id
FROM file_web_serve_batch
WHERE hash_id = ?;`,
[ servedItem.hashId ],
(err, fileIdRows) => {
if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) {
return callback(Errors.DoesNotExist('Could not get file IDs for batch'));
}
[ servedItem.hashId ],
(err, fileIdRows) => {
if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) {
return callback(Errors.DoesNotExist('Could not get file IDs for batch'));
}
return callback(null, fileIdRows.map(r => r.file_id));
}
);
},
function loadFileEntries(fileIds, callback) {
async.map(fileIds, (fileId, nextFileId) => {
const fileEntry = new FileEntry();
fileEntry.load(fileId, err => {
return nextFileId(err, fileEntry);
});
}, (err, fileEntries) => {
if(err) {
return callback(Errors.DoesNotExist('Could not load file IDs for batch'));
}
return callback(null, fileIdRows.map(r => r.file_id));
}
);
},
function loadFileEntries(fileIds, callback) {
async.map(fileIds, (fileId, nextFileId) => {
const fileEntry = new FileEntry();
fileEntry.load(fileId, err => {
return nextFileId(err, fileEntry);
});
}, (err, fileEntries) => {
if(err) {
return callback(Errors.DoesNotExist('Could not load file IDs for batch'));
}
return callback(null, fileEntries);
});
},
function createAndServeStream(fileEntries, callback) {
const filePaths = fileEntries.map(fe => fe.filePath);
Log.trace( { filePaths : filePaths }, 'Creating zip archive for batch web request');
return callback(null, fileEntries);
});
},
function createAndServeStream(fileEntries, callback) {
const filePaths = fileEntries.map(fe => fe.filePath);
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 => {
Log.warn( { error : err.message }, 'Error adding file to batch web request archive');
});
zipFile.on('error', err => {
Log.warn( { error : err.message }, 'Error adding file to batch web request archive');
});
filePaths.forEach(fp => {
zipFile.addFile(
fp, // path to physical file
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.
}
);
});
filePaths.forEach(fp => {
zipFile.addFile(
fp, // path to physical file
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.
}
);
});
zipFile.end( finalZipSize => {
if(-1 === finalZipSize) {
return callback(Errors.UnexpectedState('Unable to acquire final zip size'));
}
zipFile.end( finalZipSize => {
if(-1 === finalZipSize) {
return callback(Errors.UnexpectedState('Unable to acquire final zip size'));
}
resp.on('close', () => {
// connection closed *before* the response was fully sent
// :TODO: Log and such
});
resp.on('close', () => {
// connection closed *before* the response was fully sent
// :TODO: Log and such
});
resp.on('finish', () => {
// transfer completed fully
self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries);
});
resp.on('finish', () => {
// transfer completed fully
self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries);
});
const batchFileName = `batch_${servedItem.hashId}.zip`;
const batchFileName = `batch_${servedItem.hashId}.zip`;
const headers = {
'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'),
'Content-Length' : finalZipSize,
'Content-Disposition' : `attachment; filename="${batchFileName}"`,
};
const headers = {
'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'),
'Content-Length' : finalZipSize,
'Content-Disposition' : `attachment; filename="${batchFileName}"`,
};
resp.writeHead(200, headers);
return zipFile.outputStream.pipe(resp);
});
}
],
err => {
if(err) {
// :TODO: Log me!
return this.fileNotFound(resp);
}
resp.writeHead(200, headers);
return zipFile.outputStream.pipe(resp);
});
}
],
err => {
if(err) {
// :TODO: Log me!
return this.fileNotFound(resp);
}
// ...otherwise, we would have called resp() already.
}
);
}
// ...otherwise, we would have called resp() already.
}
);
}
updateDownloadStatsForUserIdAndSystem(userId, dlBytes, fileEntries) {
async.waterfall(
[
function fetchActiveUser(callback) {
const clientForUserId = getConnectionByUserId(userId);
if(clientForUserId) {
return callback(null, clientForUserId.user);
}
updateDownloadStatsForUserIdAndSystem(userId, dlBytes, fileEntries) {
async.waterfall(
[
function fetchActiveUser(callback) {
const clientForUserId = getConnectionByUserId(userId);
if(clientForUserId) {
return callback(null, clientForUserId.user);
}
// not online now - look 'em up
User.getUser(userId, (err, assocUser) => {
return callback(err, assocUser);
});
},
function updateStats(user, callback) {
StatLog.incrementUserStat(user, 'dl_total_count', 1);
StatLog.incrementUserStat(user, 'dl_total_bytes', dlBytes);
StatLog.incrementSystemStat('dl_total_count', 1);
StatLog.incrementSystemStat('dl_total_bytes', dlBytes);
// not online now - look 'em up
User.getUser(userId, (err, assocUser) => {
return callback(err, assocUser);
});
},
function updateStats(user, callback) {
StatLog.incrementUserStat(user, 'dl_total_count', 1);
StatLog.incrementUserStat(user, 'dl_total_bytes', dlBytes);
StatLog.incrementSystemStat('dl_total_count', 1);
StatLog.incrementSystemStat('dl_total_bytes', dlBytes);
return callback(null, user);
},
function sendEvent(user, callback) {
Events.emit(
Events.getSystemEvents().UserDownload,
{
user : user,
files : fileEntries,
}
);
return callback(null);
}
]
);
}
return callback(null, user);
},
function sendEvent(user, callback) {
Events.emit(
Events.getSystemEvents().UserDownload,
{
user : user,
files : fileEntries,
}
);
return callback(null);
}
]
);
}
}
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');
exports.moduleInfo = {
name : 'File Area Selector',
desc : 'Select from available file areas',
author : 'NuSkooler',
name : 'File Area Selector',
desc : 'Select from available file areas',
author : 'NuSkooler',
};
const MciViewIds = {
areaList : 1,
areaList : 1,
};
exports.getModule = class FileAreaSelectModule extends MenuModule {
constructor(options) {
super(options);
constructor(options) {
super(options);
this.menuMethods = {
selectArea : (formData, extraArgs, cb) => {
const filterCriteria = {
areaTag : formData.value.areaTag,
};
this.menuMethods = {
selectArea : (formData, extraArgs, cb) => {
const filterCriteria = {
areaTag : formData.value.areaTag,
};
const menuOpts = {
extraArgs : {
filterCriteria : filterCriteria,
},
menuFlags : [ 'popParent', 'mergeFlags' ],
};
const menuOpts = {
extraArgs : {
filterCriteria : filterCriteria,
},
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) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
const self = this;
const self = this;
async.waterfall(
[
function mergeAreaStats(callback) {
const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} };
async.waterfall(
[
function mergeAreaStats(callback) {
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
const availAreas = getSortedAvailableFileAreas(self.client);
availAreas.forEach(area => {
const stats = areaStats.areas[area.areaTag];
area.totalFiles = stats ? stats.files : 0;
area.totalBytes = stats ? stats.bytes : 0;
});
// we could use 'sort' alone, but area/conf sorting has some special properties; user can still override
const availAreas = getSortedAvailableFileAreas(self.client);
availAreas.forEach(area => {
const stats = areaStats.areas[area.areaTag];
area.totalFiles = stats ? stats.files : 0;
area.totalBytes = stats ? stats.bytes : 0;
});
return callback(null, availAreas);
},
function prepView(availAreas, callback) {
self.prepViewController('allViews', 0, mciData.menu, (err, vc) => {
if(err) {
return callback(err);
}
return callback(null, availAreas);
},
function prepView(availAreas, callback) {
self.prepViewController('allViews', 0, mciData.menu, (err, vc) => {
if(err) {
return callback(err);
}
const areaListView = vc.getView(MciViewIds.areaList);
areaListView.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } )));
areaListView.redraw();
const areaListView = vc.getView(MciViewIds.areaList);
areaListView.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } )));
areaListView.redraw();
return callback(null);
});
}
],
err => {
return cb(err);
}
);
});
}
return callback(null);
});
}
],
err => {
return cb(err);
}
);
});
}
};

View File

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

View File

@ -6,150 +6,150 @@ const _ = require('lodash');
const uuidV4 = require('uuid/v4');
module.exports = class FileBaseFilters {
constructor(client) {
this.client = client;
constructor(client) {
this.client = client;
this.load();
}
this.load();
}
static get OrderByValues() {
return [ 'descending', 'ascending' ];
}
static get OrderByValues() {
return [ 'descending', 'ascending' ];
}
static get SortByValues() {
return [
'upload_timestamp',
'upload_by_username',
'dl_count',
'user_rating',
'est_release_year',
'byte_size',
'file_name',
];
}
static get SortByValues() {
return [
'upload_timestamp',
'upload_by_username',
'dl_count',
'user_rating',
'est_release_year',
'byte_size',
'file_name',
];
}
toArray() {
return _.map(this.filters, (filter, uuid) => {
return Object.assign( { uuid : uuid }, filter );
});
}
toArray() {
return _.map(this.filters, (filter, uuid) => {
return Object.assign( { uuid : uuid }, filter );
});
}
get(filterUuid) {
return this.filters[filterUuid];
}
get(filterUuid) {
return this.filters[filterUuid];
}
add(filterInfo) {
const filterUuid = uuidV4();
add(filterInfo) {
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) {
const filter = this.get(filterUuid);
if(!filter) {
return false;
}
replace(filterUuid, filterInfo) {
const filter = this.get(filterUuid);
if(!filter) {
return false;
}
filterInfo.tags = this.cleanTags(filterInfo.tags);
this.filters[filterUuid] = filterInfo;
return true;
}
filterInfo.tags = this.cleanTags(filterInfo.tags);
this.filters[filterUuid] = filterInfo;
return true;
}
remove(filterUuid) {
delete this.filters[filterUuid];
}
remove(filterUuid) {
delete this.filters[filterUuid];
}
load() {
let filtersProperty = this.client.user.properties.file_base_filters;
let defaulted;
if(!filtersProperty) {
filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters());
defaulted = true;
}
load() {
let filtersProperty = this.client.user.properties.file_base_filters;
let defaulted;
if(!filtersProperty) {
filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters());
defaulted = true;
}
try {
this.filters = JSON.parse(filtersProperty);
} catch(e) {
this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :(
defaulted = true;
this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' );
}
try {
this.filters = JSON.parse(filtersProperty);
} catch(e) {
this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :(
defaulted = true;
this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' );
}
if(defaulted) {
this.persist( err => {
if(!err) {
const defaultActiveUuid = this.toArray()[0].uuid;
this.setActive(defaultActiveUuid);
}
});
}
}
if(defaulted) {
this.persist( err => {
if(!err) {
const defaultActiveUuid = this.toArray()[0].uuid;
this.setActive(defaultActiveUuid);
}
});
}
}
persist(cb) {
return this.client.user.persistProperty('file_base_filters', JSON.stringify(this.filters), cb);
}
persist(cb) {
return this.client.user.persistProperty('file_base_filters', JSON.stringify(this.filters), cb);
}
cleanTags(tags) {
return tags.toLowerCase().replace(/,?\s+|,/g, ' ').trim();
}
cleanTags(tags) {
return tags.toLowerCase().replace(/,?\s+|,/g, ' ').trim();
}
setActive(filterUuid) {
const activeFilter = this.get(filterUuid);
setActive(filterUuid) {
const activeFilter = this.get(filterUuid);
if(activeFilter) {
this.activeFilter = activeFilter;
this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid);
return true;
}
if(activeFilter) {
this.activeFilter = activeFilter;
this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid);
return true;
}
return false;
}
return false;
}
static getBuiltInSystemFilters() {
const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329';
static getBuiltInSystemFilters() {
const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329';
const filters = {
[ U_LATEST ] : {
name : 'By Date Added',
areaTag : '', // all
terms : '', // *
tags : '', // *
order : 'descending',
sort : 'upload_timestamp',
uuid : U_LATEST,
system : true,
}
};
const filters = {
[ U_LATEST ] : {
name : 'By Date Added',
areaTag : '', // all
terms : '', // *
tags : '', // *
order : 'descending',
sort : 'upload_timestamp',
uuid : U_LATEST,
system : true,
}
};
return filters;
}
return filters;
}
static getActiveFilter(client) {
return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid);
}
static getActiveFilter(client) {
return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid);
}
static getFileBaseLastViewedFileIdByUser(user) {
return parseInt((user.properties.user_file_base_last_viewed || 0));
}
static getFileBaseLastViewedFileIdByUser(user) {
return parseInt((user.properties.user_file_base_last_viewed || 0));
}
static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) {
if(!cb && _.isFunction(allowOlder)) {
cb = allowOlder;
allowOlder = false;
}
static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) {
if(!cb && _.isFunction(allowOlder)) {
cb = allowOlder;
allowOlder = false;
}
const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user);
if(!allowOlder && fileId < current) {
if(cb) {
cb(null);
}
return;
}
const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user);
if(!allowOlder && fileId < current) {
if(cb) {
cb(null);
}
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 { Errors } = require('./enig_error.js');
const {
splitTextAtTerms,
isAnsi,
splitTextAtTerms,
isAnsi,
} = require('./string_util.js');
const AnsiPrep = require('./ansi_prep.js');
const Log = require('./logger.js').log;
@ -26,276 +26,276 @@ exports.exportFileList = exportFileList;
exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent;
function exportFileList(filterCriteria, options, cb) {
options.templateEncoding = options.templateEncoding || 'utf8';
options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc';
options.tsFormat = options.tsFormat || 'YYYY-MM-DD';
options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec
options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc?
options.templateEncoding = options.templateEncoding || 'utf8';
options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc';
options.tsFormat = options.tsFormat || 'YYYY-MM-DD';
options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec
options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc?
if(true === options.escapeDesc) {
options.escapeDesc = '\\n';
}
if(true === options.escapeDesc) {
options.escapeDesc = '\\n';
}
const state = {
total : 0,
current : 0,
step : 'preparing',
status : 'Preparing',
};
const state = {
total : 0,
current : 0,
step : 'preparing',
status : 'Preparing',
};
const updateProgress = _.isFunction(options.progress) ?
progCb => {
return options.progress(state, progCb);
} :
progCb => {
return progCb(null);
}
const updateProgress = _.isFunction(options.progress) ?
progCb => {
return options.progress(state, progCb);
} :
progCb => {
return progCb(null);
}
;
async.waterfall(
[
function readTemplateFiles(callback) {
updateProgress(err => {
if(err) {
return callback(err);
}
async.waterfall(
[
function readTemplateFiles(callback) {
updateProgress(err => {
if(err) {
return callback(err);
}
const templateFiles = [
{ name : options.headerTemplate, req : false },
{ name : options.entryTemplate, req : true }
];
const templateFiles = [
{ name : options.headerTemplate, req : false },
{ name : options.entryTemplate, req : true }
];
const config = Config();
async.map(templateFiles, (template, nextTemplate) => {
if(!template.name && !template.req) {
return nextTemplate(null, Buffer.from([]));
}
const config = Config();
async.map(templateFiles, (template, nextTemplate) => {
if(!template.name && !template.req) {
return nextTemplate(null, Buffer.from([]));
}
template.name = paths.isAbsolute(template.name) ? template.name : paths.join(config.paths.misc, template.name);
fs.readFile(template.name, (err, data) => {
return nextTemplate(err, data);
});
}, (err, templates) => {
if(err) {
return callback(Errors.General(err.message));
}
template.name = paths.isAbsolute(template.name) ? template.name : paths.join(config.paths.misc, template.name);
fs.readFile(template.name, (err, data) => {
return nextTemplate(err, data);
});
}, (err, templates) => {
if(err) {
return callback(Errors.General(err.message));
}
// decode + ensure DOS style CRLF
templates = templates.map(tmp => iconv.decode(tmp, options.templateEncoding).replace(/\r?\n/g, '\r\n') );
// decode + ensure DOS style CRLF
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
let descIndent = 0;
if(!options.escapeDesc) {
splitTextAtTerms(templates[1]).some(line => {
const pos = line.indexOf('{fileDesc}');
if(pos > -1) {
descIndent = pos;
return true; // found it!
}
return false; // keep looking
});
}
// Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements
let descIndent = 0;
if(!options.escapeDesc) {
splitTextAtTerms(templates[1]).some(line => {
const pos = line.indexOf('{fileDesc}');
if(pos > -1) {
descIndent = pos;
return true; // found it!
}
return false; // keep looking
});
}
return callback(null, templates[0], templates[1], descIndent);
});
});
},
function findFiles(headerTemplate, entryTemplate, descIndent, callback) {
state.step = 'gathering';
state.status = 'Gathering files for supplied criteria';
updateProgress(err => {
if(err) {
return callback(err);
}
return callback(null, templates[0], templates[1], descIndent);
});
});
},
function findFiles(headerTemplate, entryTemplate, descIndent, callback) {
state.step = 'gathering';
state.status = 'Gathering files for supplied criteria';
updateProgress(err => {
if(err) {
return callback(err);
}
FileEntry.findFiles(filterCriteria, (err, fileIds) => {
if(0 === fileIds.length) {
return callback(Errors.General('No results for criteria', 'NORESULTS'));
}
FileEntry.findFiles(filterCriteria, (err, fileIds) => {
if(0 === fileIds.length) {
return callback(Errors.General('No results for criteria', 'NORESULTS'));
}
return callback(err, headerTemplate, entryTemplate, descIndent, fileIds);
});
});
},
function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) {
const formatObj = {
totalFileCount : fileIds.length,
};
return callback(err, headerTemplate, entryTemplate, descIndent, fileIds);
});
});
},
function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) {
const formatObj = {
totalFileCount : fileIds.length,
};
let current = 0;
let listBody = '';
const totals = { fileCount : fileIds.length, bytes : 0 };
state.total = fileIds.length;
let current = 0;
let listBody = '';
const totals = { fileCount : fileIds.length, bytes : 0 };
state.total = fileIds.length;
state.step = 'file';
state.step = 'file';
async.eachSeries(fileIds, (fileId, nextFileId) => {
const fileInfo = new FileEntry();
current += 1;
async.eachSeries(fileIds, (fileId, nextFileId) => {
const fileInfo = new FileEntry();
current += 1;
fileInfo.load(fileId, err => {
if(err) {
return nextFileId(null); // failed, but try the next
}
fileInfo.load(fileId, err => {
if(err) {
return nextFileId(null); // failed, but try the next
}
totals.bytes += fileInfo.meta.byte_size;
totals.bytes += fileInfo.meta.byte_size;
const appendFileInfo = () => {
if(options.escapeDesc) {
formatObj.fileDesc = formatObj.fileDesc.replace(/\r?\n/g, options.escapeDesc);
}
const appendFileInfo = () => {
if(options.escapeDesc) {
formatObj.fileDesc = formatObj.fileDesc.replace(/\r?\n/g, options.escapeDesc);
}
if(options.maxDescLen) {
formatObj.fileDesc = formatObj.fileDesc.slice(0, options.maxDescLen);
}
if(options.maxDescLen) {
formatObj.fileDesc = formatObj.fileDesc.slice(0, options.maxDescLen);
}
listBody += stringFormat(entryTemplate, formatObj);
listBody += stringFormat(entryTemplate, formatObj);
state.current = current;
state.status = `Processing ${fileInfo.fileName}`;
state.fileInfo = formatObj;
state.current = current;
state.status = `Processing ${fileInfo.fileName}`;
state.fileInfo = formatObj;
updateProgress(err => {
return nextFileId(err);
});
};
updateProgress(err => {
return nextFileId(err);
});
};
const area = FileArea.getFileAreaByTag(fileInfo.areaTag);
const area = FileArea.getFileAreaByTag(fileInfo.areaTag);
formatObj.fileId = fileId;
formatObj.areaName = _.get(area, 'name') || 'N/A';
formatObj.areaDesc = _.get(area, 'desc') || 'N/A';
formatObj.userRating = fileInfo.userRating || 0;
formatObj.fileName = fileInfo.fileName;
formatObj.fileSize = fileInfo.meta.byte_size;
formatObj.fileDesc = fileInfo.desc || '';
formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth);
formatObj.fileSha256 = fileInfo.fileSha256;
formatObj.fileCrc32 = fileInfo.meta.file_crc32;
formatObj.fileMd5 = fileInfo.meta.file_md5;
formatObj.fileSha1 = fileInfo.meta.file_sha1;
formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A';
formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat);
formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A';
formatObj.currentFile = current;
formatObj.progress = Math.floor( (current / fileIds.length) * 100 );
formatObj.fileId = fileId;
formatObj.areaName = _.get(area, 'name') || 'N/A';
formatObj.areaDesc = _.get(area, 'desc') || 'N/A';
formatObj.userRating = fileInfo.userRating || 0;
formatObj.fileName = fileInfo.fileName;
formatObj.fileSize = fileInfo.meta.byte_size;
formatObj.fileDesc = fileInfo.desc || '';
formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth);
formatObj.fileSha256 = fileInfo.fileSha256;
formatObj.fileCrc32 = fileInfo.meta.file_crc32;
formatObj.fileMd5 = fileInfo.meta.file_md5;
formatObj.fileSha1 = fileInfo.meta.file_sha1;
formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A';
formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat);
formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A';
formatObj.currentFile = current;
formatObj.progress = Math.floor( (current / fileIds.length) * 100 );
if(isAnsi(fileInfo.desc)) {
AnsiPrep(
fileInfo.desc,
{
cols : Math.min(options.descWidth, 79 - descIndent),
forceLineTerm : true, // ensure each line is term'd
asciiMode : true, // export to ASCII
fillLines : false, // don't fill up to |cols|
indent : descIndent,
},
(err, desc) => {
if(desc) {
formatObj.fileDesc = desc;
}
return appendFileInfo();
}
);
} else {
const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : '';
formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n';
return appendFileInfo();
}
});
}, err => {
return callback(err, listBody, headerTemplate, totals);
});
},
function buildHeader(listBody, headerTemplate, totals, callback) {
// header is built last such that we can have totals/etc.
if(isAnsi(fileInfo.desc)) {
AnsiPrep(
fileInfo.desc,
{
cols : Math.min(options.descWidth, 79 - descIndent),
forceLineTerm : true, // ensure each line is term'd
asciiMode : true, // export to ASCII
fillLines : false, // don't fill up to |cols|
indent : descIndent,
},
(err, desc) => {
if(desc) {
formatObj.fileDesc = desc;
}
return appendFileInfo();
}
);
} else {
const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : '';
formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n';
return appendFileInfo();
}
});
}, err => {
return callback(err, listBody, headerTemplate, totals);
});
},
function buildHeader(listBody, headerTemplate, totals, callback) {
// header is built last such that we can have totals/etc.
let filterAreaName;
let filterAreaDesc;
if(filterCriteria.areaTag) {
const area = FileArea.getFileAreaByTag(filterCriteria.areaTag);
filterAreaName = _.get(area, 'name') || 'N/A';
filterAreaDesc = _.get(area, 'desc') || 'N/A';
} else {
filterAreaName = '-ALL-';
filterAreaDesc = 'All areas';
}
let filterAreaName;
let filterAreaDesc;
if(filterCriteria.areaTag) {
const area = FileArea.getFileAreaByTag(filterCriteria.areaTag);
filterAreaName = _.get(area, 'name') || 'N/A';
filterAreaDesc = _.get(area, 'desc') || 'N/A';
} else {
filterAreaName = '-ALL-';
filterAreaDesc = 'All areas';
}
const headerFormatObj = {
nowTs : moment().format(options.tsFormat),
boardName : Config().general.boardName,
totalFileCount : totals.fileCount,
totalFileSize : totals.bytes,
filterAreaTag : filterCriteria.areaTag || '-ALL-',
filterAreaName : filterAreaName,
filterAreaDesc : filterAreaDesc,
filterTerms : filterCriteria.terms || '(none)',
filterHashTags : filterCriteria.tags || '(none)',
};
const headerFormatObj = {
nowTs : moment().format(options.tsFormat),
boardName : Config().general.boardName,
totalFileCount : totals.fileCount,
totalFileSize : totals.bytes,
filterAreaTag : filterCriteria.areaTag || '-ALL-',
filterAreaName : filterAreaName,
filterAreaDesc : filterAreaDesc,
filterTerms : filterCriteria.terms || '(none)',
filterHashTags : filterCriteria.tags || '(none)',
};
listBody = stringFormat(headerTemplate, headerFormatObj) + listBody;
return callback(null, listBody);
},
function done(listBody, callback) {
delete state.fileInfo;
state.step = 'finished';
state.status = 'Finished processing';
updateProgress( () => {
return callback(null, listBody);
});
}
], (err, listBody) => {
return cb(err, listBody);
}
);
listBody = stringFormat(headerTemplate, headerFormatObj) + listBody;
return callback(null, listBody);
},
function done(listBody, callback) {
delete state.fileInfo;
state.step = 'finished';
state.status = 'Finished processing';
updateProgress( () => {
return callback(null, listBody);
});
}
], (err, listBody) => {
return cb(err, listBody);
}
);
}
function updateFileBaseDescFilesScheduledEvent(args, cb) {
//
// For each area, loop over storage locations and build
// DESCRIPT.ION file to store in the same directory.
//
// Standard-ish 4DOS spec is as such:
// * Entry: <QUOTED_LFN> <DESC>[0x04<AppData>]\r\n
// * 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
//
const entryTemplate = args[0];
const headerTemplate = args[1];
//
// For each area, loop over storage locations and build
// DESCRIPT.ION file to store in the same directory.
//
// Standard-ish 4DOS spec is as such:
// * Entry: <QUOTED_LFN> <DESC>[0x04<AppData>]\r\n
// * 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
//
const entryTemplate = args[0];
const headerTemplate = args[1];
const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck : true });
async.each(areas, (area, nextArea) => {
const storageLocations = FileArea.getAreaStorageLocations(area);
const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck : true });
async.each(areas, (area, nextArea) => {
const storageLocations = FileArea.getAreaStorageLocations(area);
async.each(storageLocations, (storageLoc, nextStorageLoc) => {
const filterCriteria = {
areaTag : area.areaTag,
storageTag : storageLoc.storageTag,
};
async.each(storageLocations, (storageLoc, nextStorageLoc) => {
const filterCriteria = {
areaTag : area.areaTag,
storageTag : storageLoc.storageTag,
};
const exportOpts = {
headerTemplate : headerTemplate,
entryTemplate : entryTemplate,
escapeDesc : true, // escape CRLF's
maxDescLen : 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes"
};
const exportOpts = {
headerTemplate : headerTemplate,
entryTemplate : entryTemplate,
escapeDesc : true, // escape CRLF's
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');
fs.writeFile(descIonPath, iconv.encode(listBody, 'cp437'), err => {
if(err) {
Log.warn( { error : err.message, path : descIonPath }, 'Failed (re)creating DESCRIPT.ION');
} else {
Log.debug( { path : descIonPath }, '(Re)generated DESCRIPT.ION');
}
return nextStorageLoc(null);
});
});
}, () => {
return nextArea(null);
});
}, () => {
return cb(null);
});
const descIonPath = paths.join(storageLoc.dir, 'DESCRIPT.ION');
fs.writeFile(descIonPath, iconv.encode(listBody, 'cp437'), err => {
if(err) {
Log.warn( { error : err.message, path : descIonPath }, 'Failed (re)creating DESCRIPT.ION');
} else {
Log.debug( { path : descIonPath }, '(Re)generated DESCRIPT.ION');
}
return nextStorageLoc(null);
});
});
}, () => {
return nextArea(null);
});
}, () => {
return cb(null);
});
}

View File

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

View File

@ -19,269 +19,269 @@ const _ = require('lodash');
const moment = require('moment');
exports.moduleInfo = {
name : 'File Base Download Web Queue Manager',
desc : 'Module for interacting with web backed download queue/batch',
author : 'NuSkooler',
name : 'File Base Download Web Queue Manager',
desc : 'Module for interacting with web backed download queue/batch',
author : 'NuSkooler',
};
const FormIds = {
queueManager : 0
queueManager : 0
};
const MciViewIds = {
queueManager : {
queue : 1,
navMenu : 2,
queueManager : {
queue : 1,
navMenu : 2,
customRangeStart : 10,
}
customRangeStart : 10,
}
};
exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
constructor(options) {
super(options);
constructor(options) {
super(options);
this.dlQueue = new DownloadQueue(this.client);
this.dlQueue = new DownloadQueue(this.client);
this.menuMethods = {
removeItem : (formData, extraArgs, cb) => {
const selectedItem = this.dlQueue.items[formData.value.queueItem];
if(!selectedItem) {
return cb(null);
}
this.menuMethods = {
removeItem : (formData, extraArgs, cb) => {
const selectedItem = this.dlQueue.items[formData.value.queueItem];
if(!selectedItem) {
return cb(null);
}
this.dlQueue.removeItems(selectedItem.fileId);
this.dlQueue.removeItems(selectedItem.fileId);
// :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb);
},
clearQueue : (formData, extraArgs, cb) => {
this.dlQueue.clear();
// :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb);
},
clearQueue : (formData, extraArgs, cb) => {
this.dlQueue.clear();
// :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView('all', cb);
},
getBatchLink : (formData, extraArgs, cb) => {
return this.generateAndDisplayBatchLink(cb);
}
};
}
// :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView('all', cb);
},
getBatchLink : (formData, extraArgs, cb) => {
return this.generateAndDisplayBatchLink(cb);
}
};
}
initSequence() {
if(0 === this.dlQueue.items.length) {
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue');
}
initSequence() {
if(0 === this.dlQueue.items.length) {
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue');
}
const self = this;
const self = this;
async.series(
[
function beforeArt(callback) {
return self.beforeArt(callback);
},
function display(callback) {
return self.displayQueueManagerPage(false, callback);
}
],
() => {
return self.finishedLoading();
}
);
}
async.series(
[
function beforeArt(callback) {
return self.beforeArt(callback);
},
function display(callback) {
return self.displayQueueManagerPage(false, callback);
}
],
() => {
return self.finishedLoading();
}
);
}
removeItemsFromDownloadQueueView(itemIndex, cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
if(!queueView) {
return cb(Errors.DoesNotExist('Queue view does not exist'));
}
removeItemsFromDownloadQueueView(itemIndex, cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
if(!queueView) {
return cb(Errors.DoesNotExist('Queue view does not exist'));
}
if('all' === itemIndex) {
queueView.setItems([]);
queueView.setFocusItems([]);
} else {
queueView.removeItem(itemIndex);
}
if('all' === itemIndex) {
queueView.setItems([]);
queueView.setFocusItems([]);
} else {
queueView.removeItem(itemIndex);
}
queueView.redraw();
return cb(null);
}
queueView.redraw();
return cb(null);
}
displayFileInfoForFileEntry(fileEntry) {
this.updateCustomViewTextsWithFilter(
'queueManager',
MciViewIds.queueManager.customRangeStart, fileEntry,
{ filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others....
);
}
displayFileInfoForFileEntry(fileEntry) {
this.updateCustomViewTextsWithFilter(
'queueManager',
MciViewIds.queueManager.customRangeStart, fileEntry,
{ filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others....
);
}
updateDownloadQueueView(cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
if(!queueView) {
return cb(Errors.DoesNotExist('Queue view does not exist'));
}
updateDownloadQueueView(cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
if(!queueView) {
return cb(Errors.DoesNotExist('Queue view does not exist'));
}
const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}';
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}';
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
queueView.on('index update', idx => {
const fileEntry = this.dlQueue.items[idx];
this.displayFileInfoForFileEntry(fileEntry);
});
queueView.on('index update', idx => {
const fileEntry = this.dlQueue.items[idx];
this.displayFileInfoForFileEntry(fileEntry);
});
queueView.redraw();
this.displayFileInfoForFileEntry(this.dlQueue.items[0]);
queueView.redraw();
this.displayFileInfoForFileEntry(this.dlQueue.items[0]);
return cb(null);
}
return cb(null);
}
generateAndDisplayBatchLink(cb) {
const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes');
generateAndDisplayBatchLink(cb) {
const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes');
FileAreaWeb.createAndServeTempBatchDownload(
this.client,
this.dlQueue.items,
{
expireTime : expireTime
},
(err, webBatchDlLink) => {
// :TODO: handle not enabled -> display such
if(err) {
return cb(err);
}
FileAreaWeb.createAndServeTempBatchDownload(
this.client,
this.dlQueue.items,
{
expireTime : expireTime
},
(err, webBatchDlLink) => {
// :TODO: handle not enabled -> display such
if(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 = {
webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink,
webBatchDlExpire : expireTime.format(webDlExpireTimeFormat),
};
const formatObj = {
webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink,
webBatchDlExpire : expireTime.format(webDlExpireTimeFormat),
};
this.updateCustomViewTextsWithFilter(
'queueManager',
MciViewIds.queueManager.customRangeStart,
formatObj,
{ filter : Object.keys(formatObj).map(k => '{' + k + '}' ) }
);
this.updateCustomViewTextsWithFilter(
'queueManager',
MciViewIds.queueManager.customRangeStart,
formatObj,
{ filter : Object.keys(formatObj).map(k => '{' + k + '}' ) }
);
return cb(null);
}
);
}
return cb(null);
}
);
}
displayQueueManagerPage(clearScreen, cb) {
const self = this;
displayQueueManagerPage(clearScreen, cb) {
const self = this;
async.series(
[
function prepArtAndViewController(callback) {
return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback);
},
function prepareQueueDownloadLinks(callback) {
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
async.series(
[
function prepArtAndViewController(callback) {
return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback);
},
function prepareQueueDownloadLinks(callback) {
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
const config = Config();
async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => {
FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => {
if(err) {
if(ErrNotEnabled === err.reasonCode) {
return nextFileEntry(err); // we should have caught this prior
}
const config = Config();
async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => {
FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => {
if(err) {
if(ErrNotEnabled === err.reasonCode) {
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(
self.client,
fileEntry,
{ expireTime : expireTime },
(err, url) => {
if(err) {
return nextFileEntry(err);
}
FileAreaWeb.createAndServeTempDownload(
self.client,
fileEntry,
{ expireTime : expireTime },
(err, url) => {
if(err) {
return nextFileEntry(err);
}
fileEntry.webDlLinkRaw = url;
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url;
fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat);
fileEntry.webDlLinkRaw = url;
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url;
fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat);
return nextFileEntry(null);
}
);
} else {
fileEntry.webDlLinkRaw = serveItem.url;
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url;
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
return nextFileEntry(null);
}
});
}, err => {
return callback(err);
});
},
function populateViews(callback) {
return self.updateDownloadQueueView(callback);
}
],
err => {
if(cb) {
return cb(err);
}
}
);
}
return nextFileEntry(null);
}
);
} else {
fileEntry.webDlLinkRaw = serveItem.url;
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url;
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
return nextFileEntry(null);
}
});
}, err => {
return callback(err);
});
},
function populateViews(callback) {
return self.updateDownloadQueueView(callback);
}
],
err => {
if(cb) {
return cb(err);
}
}
);
}
displayArtAndPrepViewController(name, options, cb) {
const self = this;
const config = this.menuConfig.config;
displayArtAndPrepViewController(name, options, cb) {
const self = this;
const config = this.menuConfig.config;
async.waterfall(
[
function readyAndDisplayArt(callback) {
if(options.clearScreen) {
self.client.term.rawWrite(ansi.resetScreen());
}
async.waterfall(
[
function readyAndDisplayArt(callback) {
if(options.clearScreen) {
self.client.term.rawWrite(ansi.resetScreen());
}
theme.displayThemedAsset(
config.art[name],
self.client,
{ font : self.menuConfig.font, trailingLF : false },
(err, artData) => {
return callback(err, artData);
}
);
},
function prepeareViewController(artData, callback) {
if(_.isUndefined(self.viewControllers[name])) {
const vcOpts = {
client : self.client,
formId : FormIds[name],
};
theme.displayThemedAsset(
config.art[name],
self.client,
{ font : self.menuConfig.font, trailingLF : false },
(err, artData) => {
return callback(err, artData);
}
);
},
function prepeareViewController(artData, callback) {
if(_.isUndefined(self.viewControllers[name])) {
const vcOpts = {
client : self.client,
formId : FormIds[name],
};
if(!_.isUndefined(options.noInput)) {
vcOpts.noInput = options.noInput;
}
if(!_.isUndefined(options.noInput)) {
vcOpts.noInput = options.noInput;
}
const vc = self.addViewController(name, new ViewController(vcOpts));
const vc = self.addViewController(name, new ViewController(vcOpts));
const loadOpts = {
callingMenu : self,
mciMap : artData.mciMap,
formId : FormIds[name],
};
const loadOpts = {
callingMenu : self,
mciMap : artData.mciMap,
formId : FormIds[name],
};
return vc.loadFromMenuConfig(loadOpts, callback);
}
return vc.loadFromMenuConfig(loadOpts, callback);
}
self.viewControllers[name].setFocus(true);
return callback(null);
self.viewControllers[name].setFocus(true);
return callback(null);
},
],
err => {
return cb(err);
}
);
}
},
],
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 = {
name : 'Transfer file',
desc : 'Sends or receives a file(s)',
author : 'NuSkooler',
name : 'Transfer file',
desc : 'Sends or receives a file(s)',
author : 'NuSkooler',
};
exports.getModule = class TransferFileModule extends MenuModule {
constructor(options) {
super(options);
constructor(options) {
super(options);
this.config = this.menuConfig.config || {};
this.config = this.menuConfig.config || {};
//
// Most options can be set via extraArgs or config block
//
const config = Config();
if(options.extraArgs) {
if(options.extraArgs.protocol) {
this.protocolConfig = config.fileTransferProtocols[options.extraArgs.protocol];
}
//
// Most options can be set via extraArgs or config block
//
const config = Config();
if(options.extraArgs) {
if(options.extraArgs.protocol) {
this.protocolConfig = config.fileTransferProtocols[options.extraArgs.protocol];
}
if(options.extraArgs.direction) {
this.direction = options.extraArgs.direction;
}
if(options.extraArgs.direction) {
this.direction = options.extraArgs.direction;
}
if(options.extraArgs.sendQueue) {
this.sendQueue = options.extraArgs.sendQueue;
}
if(options.extraArgs.sendQueue) {
this.sendQueue = options.extraArgs.sendQueue;
}
if(options.extraArgs.recvFileName) {
this.recvFileName = options.extraArgs.recvFileName;
}
if(options.extraArgs.recvFileName) {
this.recvFileName = options.extraArgs.recvFileName;
}
if(options.extraArgs.recvDirectory) {
this.recvDirectory = options.extraArgs.recvDirectory;
}
} else {
if(this.config.protocol) {
this.protocolConfig = config.fileTransferProtocols[this.config.protocol];
}
if(options.extraArgs.recvDirectory) {
this.recvDirectory = options.extraArgs.recvDirectory;
}
} else {
if(this.config.protocol) {
this.protocolConfig = config.fileTransferProtocols[this.config.protocol];
}
if(this.config.direction) {
this.direction = this.config.direction;
}
if(this.config.direction) {
this.direction = this.config.direction;
}
if(this.config.sendQueue) {
this.sendQueue = this.config.sendQueue;
}
if(this.config.sendQueue) {
this.sendQueue = this.config.sendQueue;
}
if(this.config.recvFileName) {
this.recvFileName = this.config.recvFileName;
}
if(this.config.recvFileName) {
this.recvFileName = this.config.recvFileName;
}
if(this.config.recvDirectory) {
this.recvDirectory = this.config.recvDirectory;
}
}
if(this.config.recvDirectory) {
this.recvDirectory = this.config.recvDirectory;
}
}
this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something*
this.direction = this.direction || 'send';
this.sendQueue = this.sendQueue || [];
this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something*
this.direction = this.direction || 'send';
this.sendQueue = this.sendQueue || [];
// Ensure sendQueue is an array of objects that contain at least a 'path' member
this.sendQueue = this.sendQueue.map(item => {
if(_.isString(item)) {
return { path : item };
} else {
return item;
}
});
// Ensure sendQueue is an array of objects that contain at least a 'path' member
this.sendQueue = this.sendQueue.map(item => {
if(_.isString(item)) {
return { path : item };
} else {
return item;
}
});
this.sentFileIds = [];
}
this.sentFileIds = [];
}
isSending() {
return ('send' === this.direction);
}
isSending() {
return ('send' === this.direction);
}
restorePipeAfterExternalProc() {
if(!this.pipeRestored) {
this.pipeRestored = true;
restorePipeAfterExternalProc() {
if(!this.pipeRestored) {
this.pipeRestored = true;
this.client.restoreDataHandler();
}
}
this.client.restoreDataHandler();
}
}
sendFiles(cb) {
// assume *sending* can always batch
// :TODO: Look into this further
const allFiles = this.sendQueue.map(f => f.path);
this.executeExternalProtocolHandlerForSend(allFiles, err => {
if(err) {
this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' );
} else {
const sentFiles = [];
this.sendQueue.forEach(f => {
f.sent = true;
sentFiles.push(f.path);
sendFiles(cb) {
// assume *sending* can always batch
// :TODO: Look into this further
const allFiles = this.sendQueue.map(f => f.path);
this.executeExternalProtocolHandlerForSend(allFiles, err => {
if(err) {
this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' );
} else {
const sentFiles = [];
this.sendQueue.forEach(f => {
f.sent = true;
sentFiles.push(f.path);
});
});
this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` );
}
return cb(err);
});
}
this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` );
}
return cb(err);
});
}
/*
/*
sendFiles(cb) {
// :TODO: built in/native protocol support
@ -189,408 +189,408 @@ exports.getModule = class TransferFileModule extends MenuModule {
}
*/
moveFileWithCollisionHandling(src, dst, cb) {
//
// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc.
// in the case of collisions.
//
const dstPath = paths.dirname(dst);
const dstFileExt = paths.extname(dst);
const dstFileSuffix = paths.basename(dst, dstFileExt);
moveFileWithCollisionHandling(src, dst, cb) {
//
// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc.
// in the case of collisions.
//
const dstPath = paths.dirname(dst);
const dstFileExt = paths.extname(dst);
const dstFileSuffix = paths.basename(dst, dstFileExt);
let renameIndex = 0;
let movedOk = false;
let tryDstPath;
let renameIndex = 0;
let movedOk = false;
let tryDstPath;
async.until(
() => movedOk, // until moved OK
(cb) => {
if(0 === renameIndex) {
// try originally supplied path first
tryDstPath = dst;
} else {
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`);
}
async.until(
() => movedOk, // until moved OK
(cb) => {
if(0 === renameIndex) {
// try originally supplied path first
tryDstPath = dst;
} else {
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`);
}
fse.move(src, tryDstPath, err => {
if(err) {
if('EEXIST' === err.code) {
renameIndex += 1;
return cb(null); // keep trying
}
fse.move(src, tryDstPath, err => {
if(err) {
if('EEXIST' === err.code) {
renameIndex += 1;
return cb(null); // keep trying
}
return cb(err);
}
return cb(err);
}
movedOk = true;
return cb(null, tryDstPath);
});
},
(err, finalPath) => {
return cb(err, finalPath);
}
);
}
movedOk = true;
return cb(null, tryDstPath);
});
},
(err, finalPath) => {
return cb(err, finalPath);
}
);
}
recvFiles(cb) {
this.executeExternalProtocolHandlerForRecv(err => {
if(err) {
return cb(err);
}
recvFiles(cb) {
this.executeExternalProtocolHandlerForRecv(err => {
if(err) {
return cb(err);
}
this.recvFilePaths = [];
this.recvFilePaths = [];
if(this.recvFileName) {
//
// file name specified - we expect a single file in |this.recvDirectory|
// by the name of |this.recvFileName|
//
const recvFullPath = paths.join(this.recvDirectory, this.recvFileName);
fs.stat(recvFullPath, (err, stats) => {
if(err) {
return cb(err);
}
if(this.recvFileName) {
//
// file name specified - we expect a single file in |this.recvDirectory|
// by the name of |this.recvFileName|
//
const recvFullPath = paths.join(this.recvDirectory, this.recvFileName);
fs.stat(recvFullPath, (err, stats) => {
if(err) {
return cb(err);
}
if(!stats.isFile()) {
return cb(Errors.Invalid('Expected file entry in recv directory'));
}
if(!stats.isFile()) {
return cb(Errors.Invalid('Expected file entry in recv directory'));
}
this.recvFilePaths.push(recvFullPath);
return cb(null);
});
} else {
//
// Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already
//
fs.readdir(this.recvDirectory, (err, files) => {
if(err) {
return cb(err);
}
this.recvFilePaths.push(recvFullPath);
return cb(null);
});
} else {
//
// Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already
//
fs.readdir(this.recvDirectory, (err, files) => {
if(err) {
return cb(err);
}
// stat each to grab files only
async.each(files, (fileName, nextFile) => {
const recvFullPath = paths.join(this.recvDirectory, fileName);
// stat each to grab files only
async.each(files, (fileName, nextFile) => {
const recvFullPath = paths.join(this.recvDirectory, fileName);
fs.stat(recvFullPath, (err, stats) => {
if(err) {
this.client.log.warn('Failed to stat file', { path : recvFullPath } );
return nextFile(null); // just try the next one
}
fs.stat(recvFullPath, (err, stats) => {
if(err) {
this.client.log.warn('Failed to stat file', { path : recvFullPath } );
return nextFile(null); // just try the next one
}
if(stats.isFile()) {
this.recvFilePaths.push(recvFullPath);
}
if(stats.isFile()) {
this.recvFilePaths.push(recvFullPath);
}
return nextFile(null);
});
}, () => {
return cb(null);
});
});
}
});
}
return nextFile(null);
});
}, () => {
return cb(null);
});
});
}
});
}
pathWithTerminatingSeparator(path) {
if(path && paths.sep !== path.charAt(path.length - 1)) {
path = path + paths.sep;
}
return path;
}
pathWithTerminatingSeparator(path) {
if(path && paths.sep !== path.charAt(path.length - 1)) {
path = path + paths.sep;
}
return path;
}
prepAndBuildSendArgs(filePaths, cb) {
const externalArgs = this.protocolConfig.external['sendArgs'];
prepAndBuildSendArgs(filePaths, cb) {
const externalArgs = this.protocolConfig.external['sendArgs'];
async.waterfall(
[
function getTempFileListPath(callback) {
const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) );
if(!hasFileList) {
return callback(null, null);
}
async.waterfall(
[
function getTempFileListPath(callback) {
const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) );
if(!hasFileList) {
return callback(null, null);
}
temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => {
if(err) {
return callback(err); // failed to create it
}
temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => {
if(err) {
return callback(err); // failed to create it
}
fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL));
fs.close(tempFileInfo.fd, err => {
return callback(err, tempFileInfo.path);
});
});
},
function createArgs(tempFileListPath, callback) {
// initial args: ignore {filePaths} as we must break that into it's own sep array items
const args = externalArgs.map(arg => {
return '{filePaths}' === arg ? arg : stringFormat(arg, {
fileListPath : tempFileListPath || '',
});
});
fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL));
fs.close(tempFileInfo.fd, err => {
return callback(err, tempFileInfo.path);
});
});
},
function createArgs(tempFileListPath, callback) {
// initial args: ignore {filePaths} as we must break that into it's own sep array items
const args = externalArgs.map(arg => {
return '{filePaths}' === arg ? arg : stringFormat(arg, {
fileListPath : tempFileListPath || '',
});
});
const filePathsPos = args.indexOf('{filePaths}');
if(filePathsPos > -1) {
// replace {filePaths} with 0:n individual entries in |args|
args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) );
}
const filePathsPos = args.indexOf('{filePaths}');
if(filePathsPos > -1) {
// replace {filePaths} with 0:n individual entries in |args|
args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) );
}
return callback(null, args);
}
],
(err, args) => {
return cb(err, args);
}
);
}
return callback(null, args);
}
],
(err, args) => {
return cb(err, args);
}
);
}
prepAndBuildRecvArgs(cb) {
const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs';
const externalArgs = this.protocolConfig.external[argsKey];
const args = externalArgs.map(arg => stringFormat(arg, {
uploadDir : this.recvDirectory,
fileName : this.recvFileName || '',
}));
prepAndBuildRecvArgs(cb) {
const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs';
const externalArgs = this.protocolConfig.external[argsKey];
const args = externalArgs.map(arg => stringFormat(arg, {
uploadDir : this.recvDirectory,
fileName : this.recvFileName || '',
}));
return cb(null, args);
}
return cb(null, args);
}
executeExternalProtocolHandler(args, cb) {
const external = this.protocolConfig.external;
const cmd = external[`${this.direction}Cmd`];
executeExternalProtocolHandler(args, cb) {
const external = this.protocolConfig.external;
const cmd = external[`${this.direction}Cmd`];
this.client.log.debug(
{ cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction },
'Executing external protocol'
);
this.client.log.debug(
{ cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction },
'Executing external protocol'
);
const spawnOpts = {
cols : this.client.term.termWidth,
rows : this.client.term.termHeight,
cwd : this.recvDirectory,
encoding : null, // don't bork our data!
};
const spawnOpts = {
cols : this.client.term.termWidth,
rows : this.client.term.termHeight,
cwd : this.recvDirectory,
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 => {
// needed for things like sz/rz
if(external.escapeTelnet) {
const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape
externalProc.write(Buffer.from(tmp, 'binary'));
} else {
externalProc.write(data);
}
});
this.client.setTemporaryDirectDataHandler(data => {
// needed for things like sz/rz
if(external.escapeTelnet) {
const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape
externalProc.write(Buffer.from(tmp, 'binary'));
} else {
externalProc.write(data);
}
});
externalProc.on('data', data => {
// needed for things like sz/rz
if(external.escapeTelnet) {
const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape
this.client.term.rawWrite(Buffer.from(tmp, 'binary'));
} else {
this.client.term.rawWrite(data);
}
});
externalProc.on('data', data => {
// needed for things like sz/rz
if(external.escapeTelnet) {
const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape
this.client.term.rawWrite(Buffer.from(tmp, 'binary'));
} else {
this.client.term.rawWrite(data);
}
});
externalProc.once('close', () => {
return this.restorePipeAfterExternalProc();
});
externalProc.once('close', () => {
return this.restorePipeAfterExternalProc();
});
externalProc.once('exit', (exitCode) => {
this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' );
externalProc.once('exit', (exitCode) => {
this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' );
this.restorePipeAfterExternalProc();
externalProc.removeAllListeners();
this.restorePipeAfterExternalProc();
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) {
if(!Array.isArray(filePaths)) {
filePaths = [ filePaths ];
}
executeExternalProtocolHandlerForSend(filePaths, cb) {
if(!Array.isArray(filePaths)) {
filePaths = [ filePaths ];
}
this.prepAndBuildSendArgs(filePaths, (err, args) => {
if(err) {
return cb(err);
}
this.prepAndBuildSendArgs(filePaths, (err, args) => {
if(err) {
return cb(err);
}
this.executeExternalProtocolHandler(args, err => {
return cb(err);
});
});
}
this.executeExternalProtocolHandler(args, err => {
return cb(err);
});
});
}
executeExternalProtocolHandlerForRecv(cb) {
this.prepAndBuildRecvArgs( (err, args) => {
if(err) {
return cb(err);
}
executeExternalProtocolHandlerForRecv(cb) {
this.prepAndBuildRecvArgs( (err, args) => {
if(err) {
return cb(err);
}
this.executeExternalProtocolHandler(args, err => {
return cb(err);
});
});
}
this.executeExternalProtocolHandler(args, err => {
return cb(err);
});
});
}
getMenuResult() {
if(this.isSending()) {
return { sentFileIds : this.sentFileIds };
} else {
return { recvFilePaths : this.recvFilePaths };
}
}
getMenuResult() {
if(this.isSending()) {
return { sentFileIds : this.sentFileIds };
} else {
return { recvFilePaths : this.recvFilePaths };
}
}
updateSendStats(cb) {
let downloadBytes = 0;
let downloadCount = 0;
let fileIds = [];
updateSendStats(cb) {
let downloadBytes = 0;
let downloadCount = 0;
let fileIds = [];
async.each(this.sendQueue, (queueItem, next) => {
if(!queueItem.sent) {
return next(null);
}
async.each(this.sendQueue, (queueItem, next) => {
if(!queueItem.sent) {
return next(null);
}
if(queueItem.fileId) {
fileIds.push(queueItem.fileId);
}
if(queueItem.fileId) {
fileIds.push(queueItem.fileId);
}
if(_.isNumber(queueItem.byteSize)) {
downloadCount += 1;
downloadBytes += queueItem.byteSize;
return next(null);
}
if(_.isNumber(queueItem.byteSize)) {
downloadCount += 1;
downloadBytes += queueItem.byteSize;
return next(null);
}
// we just have a path - figure it out
fs.stat(queueItem.path, (err, stats) => {
if(err) {
this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' );
} else {
downloadCount += 1;
downloadBytes += stats.size;
}
// we just have a path - figure it out
fs.stat(queueItem.path, (err, stats) => {
if(err) {
this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' );
} else {
downloadCount += 1;
downloadBytes += stats.size;
}
return next(null);
});
}, () => {
// 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_bytes', downloadBytes);
StatLog.incrementSystemStat('dl_total_count', downloadCount);
StatLog.incrementSystemStat('dl_total_bytes', downloadBytes);
return next(null);
});
}, () => {
// 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_bytes', downloadBytes);
StatLog.incrementSystemStat('dl_total_count', downloadCount);
StatLog.incrementSystemStat('dl_total_bytes', downloadBytes);
fileIds.forEach(fileId => {
FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1);
});
fileIds.forEach(fileId => {
FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1);
});
return cb(null);
});
}
return cb(null);
});
}
updateRecvStats(cb) {
let uploadBytes = 0;
let uploadCount = 0;
updateRecvStats(cb) {
let uploadBytes = 0;
let uploadCount = 0;
async.each(this.recvFilePaths, (filePath, next) => {
// we just have a path - figure it out
fs.stat(filePath, (err, stats) => {
if(err) {
this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' );
} else {
uploadCount += 1;
uploadBytes += stats.size;
}
async.each(this.recvFilePaths, (filePath, next) => {
// we just have a path - figure it out
fs.stat(filePath, (err, stats) => {
if(err) {
this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' );
} else {
uploadCount += 1;
uploadBytes += stats.size;
}
return next(null);
});
}, () => {
StatLog.incrementUserStat(this.client.user, 'ul_total_count', uploadCount);
StatLog.incrementUserStat(this.client.user, 'ul_total_bytes', uploadBytes);
StatLog.incrementSystemStat('ul_total_count', uploadCount);
StatLog.incrementSystemStat('ul_total_bytes', uploadBytes);
return next(null);
});
}, () => {
StatLog.incrementUserStat(this.client.user, 'ul_total_count', uploadCount);
StatLog.incrementUserStat(this.client.user, 'ul_total_bytes', uploadBytes);
StatLog.incrementSystemStat('ul_total_count', uploadCount);
StatLog.incrementSystemStat('ul_total_bytes', uploadBytes);
return cb(null);
});
}
return cb(null);
});
}
initSequence() {
const self = this;
initSequence() {
const self = this;
// :TODO: break this up to send|recv
// :TODO: break this up to send|recv
async.series(
[
function validateConfig(callback) {
if(self.isSending()) {
if(!Array.isArray(self.sendQueue)) {
self.sendQueue = [ self.sendQueue ];
}
}
async.series(
[
function validateConfig(callback) {
if(self.isSending()) {
if(!Array.isArray(self.sendQueue)) {
self.sendQueue = [ self.sendQueue ];
}
}
return callback(null);
},
function transferFiles(callback) {
if(self.isSending()) {
self.sendFiles( err => {
if(err) {
return callback(err);
}
return callback(null);
},
function transferFiles(callback) {
if(self.isSending()) {
self.sendFiles( err => {
if(err) {
return callback(err);
}
const sentFileIds = [];
self.sendQueue.forEach(queueItem => {
if(queueItem.sent && queueItem.fileId) {
sentFileIds.push(queueItem.fileId);
}
});
const sentFileIds = [];
self.sendQueue.forEach(queueItem => {
if(queueItem.sent && queueItem.fileId) {
sentFileIds.push(queueItem.fileId);
}
});
if(sentFileIds.length > 0) {
// remove items we sent from the D/L queue
const dlQueue = new DownloadQueue(self.client);
const dlFileEntries = dlQueue.removeItems(sentFileIds);
if(sentFileIds.length > 0) {
// remove items we sent from the D/L queue
const dlQueue = new DownloadQueue(self.client);
const dlFileEntries = dlQueue.removeItems(sentFileIds);
// fire event for downloaded entries
Events.emit(
Events.getSystemEvents().UserDownload,
{
user : self.client.user,
files : dlFileEntries
}
);
// fire event for downloaded entries
Events.emit(
Events.getSystemEvents().UserDownload,
{
user : self.client.user,
files : dlFileEntries
}
);
self.sentFileIds = sentFileIds;
}
self.sentFileIds = sentFileIds;
}
return callback(null);
});
} else {
self.recvFiles( err => {
return callback(err);
});
}
},
function cleanupTempFiles(callback) {
temptmp.cleanup( paths => {
Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' );
});
return callback(null);
});
} else {
self.recvFiles( err => {
return callback(err);
});
}
},
function cleanupTempFiles(callback) {
temptmp.cleanup( paths => {
Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' );
});
return callback(null);
},
function updateUserAndSystemStats(callback) {
if(self.isSending()) {
return self.updateSendStats(callback);
} else {
return self.updateRecvStats(callback);
}
}
],
err => {
if(err) {
self.client.log.warn( { error : err.message }, 'File transfer error');
}
return callback(null);
},
function updateUserAndSystemStats(callback) {
if(self.isSending()) {
return self.updateSendStats(callback);
} else {
return self.updateRecvStats(callback);
}
}
],
err => {
if(err) {
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');
exports.moduleInfo = {
name : 'File transfer protocol selection',
desc : 'Select protocol / method for file transfer',
author : 'NuSkooler',
name : 'File transfer protocol selection',
desc : 'Select protocol / method for file transfer',
author : 'NuSkooler',
};
const MciViewIds = {
protList : 1,
protList : 1,
};
exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
constructor(options) {
super(options);
constructor(options) {
super(options);
this.config = this.menuConfig.config || {};
this.config = this.menuConfig.config || {};
if(options.extraArgs) {
if(options.extraArgs.direction) {
this.config.direction = options.extraArgs.direction;
}
}
if(options.extraArgs) {
if(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')) {
this.sentFileIds = options.lastMenuResult.sentFileIds;
}
if(_.has(options, 'lastMenuResult.sentFileIds')) {
this.sentFileIds = options.lastMenuResult.sentFileIds;
}
if(_.has(options, 'lastMenuResult.recvFilePaths')) {
this.recvFilePaths = options.lastMenuResult.recvFilePaths;
}
if(_.has(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 = {
selectProtocol : (formData, extraArgs, cb) => {
const protocol = this.protocols[formData.value.protocol];
const finalExtraArgs = this.extraArgs || {};
Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs );
this.menuMethods = {
selectProtocol : (formData, extraArgs, cb) => {
const protocol = this.protocols[formData.value.protocol];
const finalExtraArgs = this.extraArgs || {};
Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs );
const modOpts = {
extraArgs : finalExtraArgs,
};
const modOpts = {
extraArgs : finalExtraArgs,
};
if('send' === this.config.direction) {
return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb);
} else {
return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb);
}
},
};
}
if('send' === this.config.direction) {
return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb);
} else {
return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb);
}
},
};
}
getMenuResult() {
if(this.sentFileIds) {
return { sentFileIds : this.sentFileIds };
}
getMenuResult() {
if(this.sentFileIds) {
return { sentFileIds : this.sentFileIds };
}
if(this.recvFilePaths) {
return { recvFilePaths : this.recvFilePaths };
}
}
if(this.recvFilePaths) {
return { recvFilePaths : this.recvFilePaths };
}
}
initSequence() {
if(this.sentFileIds || this.recvFilePaths) {
// nothing to do here; move along (we're just falling through)
this.prevMenu();
} else {
super.initSequence();
}
}
initSequence() {
if(this.sentFileIds || this.recvFilePaths) {
// nothing to do here; move along (we're just falling through)
this.prevMenu();
} else {
super.initSequence();
}
}
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
async.series(
[
function loadFromConfig(callback) {
const loadOpts = {
callingMenu : self,
mciMap : mciData.menu
};
async.series(
[
function loadFromConfig(callback) {
const loadOpts = {
callingMenu : self,
mciMap : mciData.menu
};
return vc.loadFromMenuConfig(loadOpts, callback);
},
function populateList(callback) {
const protListView = vc.getView(MciViewIds.protList);
return vc.loadFromMenuConfig(loadOpts, callback);
},
function populateList(callback) {
const protListView = vc.getView(MciViewIds.protList);
const protListFormat = self.config.protListFormat || '{name}';
const protListFocusFormat = self.config.protListFocusFormat || protListFormat;
const protListFormat = self.config.protListFormat || '{name}';
const protListFocusFormat = self.config.protListFocusFormat || protListFormat;
protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) );
protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) );
protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) );
protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) );
protListView.redraw();
protListView.redraw();
return callback(null);
}
],
err => {
return cb(err);
}
);
});
}
return callback(null);
}
],
err => {
return cb(err);
}
);
});
}
loadAvailProtocols() {
this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => {
return {
protocol : protocol,
name : protInfo.name,
hasBatch : _.has(protInfo, 'external.recvArgs'),
hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'),
sort : protInfo.sort,
};
});
loadAvailProtocols() {
this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => {
return {
protocol : protocol,
name : protInfo.name,
hasBatch : _.has(protInfo, 'external.recvArgs'),
hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'),
sort : protInfo.sort,
};
});
// Filter out batch vs non-batch only protocols
if(this.extraArgs.recvFileName) { // non-batch aka non-blind
this.protocols = this.protocols.filter( prot => prot.hasNonBatch );
} else {
this.protocols = this.protocols.filter( prot => prot.hasBatch );
}
// Filter out batch vs non-batch only protocols
if(this.extraArgs.recvFileName) { // non-batch aka non-blind
this.protocols = this.protocols.filter( prot => prot.hasNonBatch );
} else {
this.protocols = this.protocols.filter( prot => prot.hasBatch );
}
// natural sort taking explicit orders into consideration
this.protocols.sort( (a, b) => {
if(_.isNumber(a.sort) && _.isNumber(b.sort)) {
return a.sort - b.sort;
} else {
return a.name.localeCompare(b.name, { sensitivity : false, numeric : true } );
}
});
}
// natural sort taking explicit orders into consideration
this.protocols.sort( (a, b) => {
if(_.isNumber(a.sort) && _.isNumber(b.sort)) {
return a.sort - b.sort;
} else {
return a.name.localeCompare(b.name, { sensitivity : false, numeric : true } );
}
});
}
};

View File

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

View File

@ -5,46 +5,46 @@ let _ = require('lodash');
// FNV-1a based on work here: https://github.com/wiedi/node-fnv
module.exports = class FNV1a {
constructor(data) {
this.hash = 0x811c9dc5;
constructor(data) {
this.hash = 0x811c9dc5;
if(!_.isUndefined(data)) {
this.update(data);
}
}
if(!_.isUndefined(data)) {
this.update(data);
}
}
update(data) {
if(_.isNumber(data)) {
data = data.toString();
}
update(data) {
if(_.isNumber(data)) {
data = data.toString();
}
if(_.isString(data)) {
data = Buffer.from(data);
}
if(_.isString(data)) {
data = Buffer.from(data);
}
if(!Buffer.isBuffer(data)) {
throw new Error('data must be String or Buffer!');
}
if(!Buffer.isBuffer(data)) {
throw new Error('data must be String or Buffer!');
}
for(let b of data) {
this.hash = this.hash ^ b;
this.hash +=
for(let b of data) {
this.hash = this.hash ^ b;
this.hash +=
(this.hash << 24) + (this.hash << 8) + (this.hash << 7) +
(this.hash << 4) + (this.hash << 1);
}
}
return this;
}
return this;
}
digest(encoding) {
encoding = encoding || 'binary';
const buf = Buffer.alloc(4);
buf.writeInt32BE(this.hash & 0xffffffff, 0);
return buf.toString(encoding);
}
digest(encoding) {
encoding = encoding || 'binary';
const buf = Buffer.alloc(4);
buf.writeInt32BE(this.hash & 0xffffffff, 0);
return buf.toString(encoding);
}
get value() {
return this.hash & 0xffffffff;
}
get value() {
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;
module.exports = class Address {
constructor(addr) {
if(addr) {
if(_.isObject(addr)) {
Object.assign(this, addr);
} else if(_.isString(addr)) {
const temp = Address.fromString(addr);
if(temp) {
Object.assign(this, temp);
}
}
}
}
constructor(addr) {
if(addr) {
if(_.isObject(addr)) {
Object.assign(this, addr);
} else if(_.isString(addr)) {
const temp = Address.fromString(addr);
if(temp) {
Object.assign(this, temp);
}
}
}
}
static isValidAddress(addr) {
return addr && addr.isValid();
}
static isValidAddress(addr) {
return addr && addr.isValid();
}
isValid() {
// FTN address is valid if we have at least a net/node
return _.isNumber(this.net) && _.isNumber(this.node);
}
isValid() {
// FTN address is valid if we have at least a net/node
return _.isNumber(this.net) && _.isNumber(this.node);
}
isEqual(other) {
if(_.isString(other)) {
other = Address.fromString(other);
}
isEqual(other) {
if(_.isString(other)) {
other = Address.fromString(other);
}
return (
this.net === other.net &&
return (
this.net === other.net &&
this.node === other.node &&
this.zone === other.zone &&
this.point === other.point &&
this.domain === other.domain
);
}
);
}
getMatchAddr(pattern) {
const m = FTN_PATTERN_REGEXP.exec(pattern);
if(m) {
let addr = { };
getMatchAddr(pattern) {
const m = FTN_PATTERN_REGEXP.exec(pattern);
if(m) {
let addr = { };
if(m[1]) {
addr.zone = m[1].slice(0, -1);
if('*' !== addr.zone) {
addr.zone = parseInt(addr.zone);
}
} else {
addr.zone = '*';
}
if(m[1]) {
addr.zone = m[1].slice(0, -1);
if('*' !== addr.zone) {
addr.zone = parseInt(addr.zone);
}
} else {
addr.zone = '*';
}
if(m[2]) {
addr.net = m[2];
if('*' !== addr.net) {
addr.net = parseInt(addr.net);
}
} else {
addr.net = '*';
}
if(m[2]) {
addr.net = m[2];
if('*' !== addr.net) {
addr.net = parseInt(addr.net);
}
} else {
addr.net = '*';
}
if(m[3]) {
addr.node = m[3].substr(1);
if('*' !== addr.node) {
addr.node = parseInt(addr.node);
}
} else {
addr.node = '*';
}
if(m[3]) {
addr.node = m[3].substr(1);
if('*' !== addr.node) {
addr.node = parseInt(addr.node);
}
} else {
addr.node = '*';
}
if(m[4]) {
addr.point = m[4].substr(1);
if('*' !== addr.point) {
addr.point = parseInt(addr.point);
}
} else {
addr.point = '*';
}
if(m[4]) {
addr.point = m[4].substr(1);
if('*' !== addr.point) {
addr.point = parseInt(addr.point);
}
} else {
addr.point = '*';
}
if(m[5]) {
addr.domain = m[5].substr(1);
} else {
addr.domain = '*';
}
if(m[5]) {
addr.domain = m[5].substr(1);
} else {
addr.domain = '*';
}
return addr;
}
}
return addr;
}
}
/*
/*
getMatchScore(pattern) {
let score = 0;
const addr = this.getMatchAddr(pattern);
@ -116,92 +116,92 @@ module.exports = class Address {
}
*/
isPatternMatch(pattern) {
const addr = this.getMatchAddr(pattern);
if(addr) {
return (
('*' === addr.net || this.net === addr.net) &&
isPatternMatch(pattern) {
const addr = this.getMatchAddr(pattern);
if(addr) {
return (
('*' === addr.net || this.net === addr.net) &&
('*' === addr.node || this.node === addr.node) &&
('*' === addr.zone || this.zone === addr.zone) &&
('*' === addr.point || this.point === addr.point) &&
('*' === addr.domain || this.domain === addr.domain)
);
}
);
}
return false;
}
return false;
}
static fromString(addrStr) {
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
static fromString(addrStr) {
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
if(m) {
// start with a 2D
let addr = {
net : parseInt(m[2]),
node : parseInt(m[3].substr(1)),
};
if(m) {
// start with a 2D
let addr = {
net : parseInt(m[2]),
node : parseInt(m[3].substr(1)),
};
// 3D: Addition of zone if present
if(m[1]) {
addr.zone = parseInt(m[1].slice(0, -1));
}
// 3D: Addition of zone if present
if(m[1]) {
addr.zone = parseInt(m[1].slice(0, -1));
}
// 4D if optional point is present
if(m[4]) {
addr.point = parseInt(m[4].substr(1));
}
// 4D if optional point is present
if(m[4]) {
addr.point = parseInt(m[4].substr(1));
}
// 5D with @domain
if(m[5]) {
addr.domain = m[5].substr(1);
}
// 5D with @domain
if(m[5]) {
addr.domain = m[5].substr(1);
}
return new Address(addr);
}
}
return new Address(addr);
}
}
toString(dimensions) {
dimensions = dimensions || '5D';
toString(dimensions) {
dimensions = dimensions || '5D';
let addrStr = `${this.zone}:${this.net}`;
let addrStr = `${this.zone}:${this.net}`;
// allow for e.g. '4D' or 5
const dim = parseInt(dimensions.toString()[0]);
// allow for e.g. '4D' or 5
const dim = parseInt(dimensions.toString()[0]);
if(dim >= 3) {
addrStr += `/${this.node}`;
}
if(dim >= 3) {
addrStr += `/${this.node}`;
}
// missing & .0 are equiv for point
if(dim >= 4 && this.point) {
addrStr += `.${this.point}`;
}
// missing & .0 are equiv for point
if(dim >= 4 && this.point) {
addrStr += `.${this.point}`;
}
if(5 === dim && this.domain) {
addrStr += `@${this.domain.toLowerCase()}`;
}
if(5 === dim && this.domain) {
addrStr += `@${this.domain.toLowerCase()}`;
}
return addrStr;
}
return addrStr;
}
static getComparator() {
return function(left, right) {
let c = (left.zone || 0) - (right.zone || 0);
if(0 !== c) {
return c;
}
static getComparator() {
return function(left, right) {
let c = (left.zone || 0) - (right.zone || 0);
if(0 !== c) {
return c;
}
c = (left.net || 0) - (right.net || 0);
if(0 !== c) {
return c;
}
c = (left.net || 0) - (right.net || 0);
if(0 !== c) {
return c;
}
c = (left.node || 0) - (right.node || 0);
if(0 !== c) {
return c;
}
c = (left.node || 0) - (right.node || 0);
if(0 !== 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
function stringToNullPaddedBuffer(s, bufLen) {
let buffer = Buffer.alloc(bufLen);
let enc = iconv.encode(s, 'CP437').slice(0, bufLen);
for(let i = 0; i < enc.length; ++i) {
buffer[i] = enc[i];
}
return buffer;
let buffer = Buffer.alloc(bufLen);
let enc = iconv.encode(s, 'CP437').slice(0, bufLen);
for(let i = 0; i < enc.length; ++i) {
buffer[i] = enc[i];
}
return buffer;
}
//
@ -58,45 +58,45 @@ function stringToNullPaddedBuffer(s, bufLen) {
//
// :TODO: Name the next couple methods better - for FTN *packets*
function getDateFromFtnDateTime(dateTime) {
//
// Examples seen in the wild (Working):
// "12 Sep 88 18:17:59"
// "Tue 01 Jan 80 00:00"
// "27 Feb 15 00:00:03"
//
// :TODO: Use moment.js here
return moment(Date.parse(dateTime)); // Date.parse() allows funky formats
//
// Examples seen in the wild (Working):
// "12 Sep 88 18:17:59"
// "Tue 01 Jan 80 00:00"
// "27 Feb 15 00:00:03"
//
// :TODO: Use moment.js here
return moment(Date.parse(dateTime)); // Date.parse() allows funky formats
// return (new Date(Date.parse(dateTime))).toISOString();
}
function getDateTimeString(m) {
//
// From http://ftsc.org/docs/fts-0001.016:
// DateTime = (* a character string 20 characters long *)
// (* 01 Jan 86 02:34:56 *)
// DayOfMonth " " Month " " Year " "
// " " HH ":" MM ":" SS
// Null
//
// DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *)
// Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" |
// "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec"
// Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00"
// HH = "00" | .. | "23"
// MM = "00" | .. | "59"
// SS = "00" | .. | "59"
//
if(!moment.isMoment(m)) {
m = moment(m);
}
//
// From http://ftsc.org/docs/fts-0001.016:
// DateTime = (* a character string 20 characters long *)
// (* 01 Jan 86 02:34:56 *)
// DayOfMonth " " Month " " Year " "
// " " HH ":" MM ":" SS
// Null
//
// DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *)
// Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" |
// "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec"
// Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00"
// HH = "00" | .. | "23"
// MM = "00" | .. | "59"
// SS = "00" | .. | "59"
//
if(!moment.isMoment(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) {
const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1));
const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16);
return `00000000${hash}`.substr(-8);
const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1));
const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16);
return `00000000${hash}`.substr(-8);
}
//
@ -143,11 +143,11 @@ function getMessageSerialNumber(messageId) {
// format, but that will only help when using newer Mystic versions.
//
function getMessageIdentifier(message, address, isNetMail = false) {
const addrStr = new Address(address).toString('5D');
return isNetMail ?
`${addrStr} ${getMessageSerialNumber(message.messageId)}` :
`${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}`
;
const addrStr = new Address(address).toString('5D');
return isNetMail ?
`${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
//
function getProductIdentifier() {
const version = getCleanEnigmaVersion();
const nodeVer = process.version.substr(1); // remove 'v' prefix
const version = getCleanEnigmaVersion();
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
//
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
//
function getQuotePrefix(name) {
let initials;
let initials;
const parts = name.split(' ');
if(parts.length > 1) {
// First & Last initials - (Bryan Ashby -> BA)
initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase();
} else {
// Just use the first two - (NuSkooler -> Nu)
initials = _.capitalize(name.slice(0, 2));
}
const parts = name.split(' ');
if(parts.length > 1) {
// First & Last initials - (Bryan Ashby -> BA)
initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase();
} else {
// Just use the first two - (NuSkooler -> Nu)
initials = _.capitalize(name.slice(0, 2));
}
return ` ${initials}> `;
return ` ${initials}> `;
}
//
@ -198,18 +198,18 @@ function getQuotePrefix(name) {
// http://ftsc.org/docs/fts-0004.001
//
function getOrigin(address) {
const config = Config();
const origin = _.has(config, 'messageNetworks.originLine') ?
config.messageNetworks.originLine :
config.general.boardName;
const config = Config();
const origin = _.has(config, 'messageNetworks.originLine') ?
config.messageNetworks.originLine :
config.general.boardName;
const addrStr = new Address(address).toString('5D');
return ` * Origin: ${origin} (${addrStr})`;
const addrStr = new Address(address).toString('5D');
return ` * Origin: ${origin} (${addrStr})`;
}
function getTearLine() {
const nodeVer = process.version.substr(1); // remove 'v' prefix
return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
const nodeVer = process.version.substr(1); // remove 'v' prefix
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
//
function getVia(address) {
/*
/*
FRL-1005.001 states teh following format:
^AVia: <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone]
<Program Name> <Version> [Serial Number]<CR>
*/
const addrStr = new Address(address).toString('5D');
const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC');
const version = getCleanEnigmaVersion();
const addrStr = new Address(address).toString('5D');
const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC');
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
//
function getIntl(toAddress, fromAddress) {
//
// INTL differs from 'standard' kludges in that there is no ':' after "INTL"
//
// "<SOH>"INTL "<destination address>" "<origin address><CR>"
// "...These addresses shall be given on the form <zone>:<net>/<node>"
//
return `${toAddress.toString('3D')} ${fromAddress.toString('3D')}`;
//
// INTL differs from 'standard' kludges in that there is no ':' after "INTL"
//
// "<SOH>"INTL "<destination address>" "<origin address><CR>"
// "...These addresses shall be given on the form <zone>:<net>/<node>"
//
return `${toAddress.toString('3D')} ${fromAddress.toString('3D')}`;
}
function getAbbreviatedNetNodeList(netNodes) {
let abbrList = '';
let currNet;
netNodes.forEach(netNode => {
if(_.isString(netNode)) {
netNode = Address.fromString(netNode);
}
if(currNet !== netNode.net) {
abbrList += `${netNode.net}/`;
currNet = netNode.net;
}
abbrList += `${netNode.node} `;
});
let abbrList = '';
let currNet;
netNodes.forEach(netNode => {
if(_.isString(netNode)) {
netNode = Address.fromString(netNode);
}
if(currNet !== netNode.net) {
abbrList += `${netNode.net}/`;
currNet = netNode.net;
}
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
//
function parseAbbreviatedNetNodeList(netNodes) {
const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g;
let net;
let m;
let results = [];
while(null !== (m = re.exec(netNodes))) {
if(m[1] && m[2]) {
net = parseInt(m[1]);
results.push(new Address( { net : net, node : parseInt(m[2]) } ));
} else if(net) {
results.push(new Address( { net : net, node : parseInt(m[3]) } ));
}
}
const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g;
let net;
let m;
let results = [];
while(null !== (m = re.exec(netNodes))) {
if(m[1] && m[2]) {
net = parseInt(m[1]);
results.push(new Address( { net : net, node : parseInt(m[2]) } ));
} else if(net) {
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
//
function getUpdatedSeenByEntries(existingEntries, additions) {
/*
/*
From FTS-0004:
"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
programs."
*/
existingEntries = existingEntries || [];
if(!_.isArray(existingEntries)) {
existingEntries = [ existingEntries ];
}
existingEntries = existingEntries || [];
if(!_.isArray(existingEntries)) {
existingEntries = [ existingEntries ];
}
if(!_.isString(additions)) {
additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions));
}
if(!_.isString(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
//
// :TODO: we should at least try and update what is already there in a smart way
existingEntries.push(getAbbreviatedNetNodeList(additions));
return existingEntries;
//
// 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
existingEntries.push(getAbbreviatedNetNodeList(additions));
return existingEntries;
}
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 || [];
if(!_.isArray(existingEntries)) {
existingEntries = [ existingEntries ];
}
existingEntries = existingEntries || [];
if(!_.isArray(existingEntries)) {
existingEntries = [ existingEntries ];
}
existingEntries.push(getAbbreviatedNetNodeList(
parseAbbreviatedNetNodeList(localAddress)));
existingEntries.push(getAbbreviatedNetNodeList(
parseAbbreviatedNetNodeList(localAddress)));
return existingEntries;
return existingEntries;
}
//
@ -354,71 +354,71 @@ function getUpdatedPathEntries(existingEntries, localAddress) {
// http://ftsc.org/docs/fts-5003.001
//
const ENCODING_TO_FTS_5003_001_CHARS = {
// level 1 - generally should not be used
ascii : [ 'ASCII', 1 ],
'us-ascii' : [ 'ASCII', 1 ],
// level 1 - generally should not be used
ascii : [ 'ASCII', 1 ],
'us-ascii' : [ 'ASCII', 1 ],
// level 2 - 8 bit, ASCII based
cp437 : [ 'CP437', 2 ],
cp850 : [ 'CP850', 2 ],
// level 2 - 8 bit, ASCII based
cp437 : [ 'CP437', 2 ],
cp850 : [ 'CP850', 2 ],
// level 3 - reserved
// level 3 - reserved
// level 4
utf8 : [ 'UTF-8', 4 ],
'utf-8' : [ 'UTF-8', 4 ],
// level 4
utf8 : [ 'UTF-8', 4 ],
'utf-8' : [ 'UTF-8', 4 ],
};
function getCharacterSetIdentifierByEncoding(encodingName) {
const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()];
return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase();
const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()];
return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase();
}
function getEncodingFromCharacterSetIdentifier(chrs) {
const ident = chrs.split(' ')[0].toUpperCase();
const ident = chrs.split(' ')[0].toUpperCase();
// :TODO: fill in the rest!!!
return {
// level 1
'ASCII' : 'iso-646-1',
'DUTCH' : 'iso-646',
'FINNISH' : 'iso-646-10',
'FRENCH' : 'iso-646',
'CANADIAN' : 'iso-646',
'GERMAN' : 'iso-646',
'ITALIAN' : 'iso-646',
'NORWEIG' : 'iso-646',
'PORTU' : 'iso-646',
'SPANISH' : 'iso-656',
'SWEDISH' : 'iso-646-10',
'SWISS' : 'iso-646',
'UK' : 'iso-646',
'ISO-10' : 'iso-646-10',
// :TODO: fill in the rest!!!
return {
// level 1
'ASCII' : 'iso-646-1',
'DUTCH' : 'iso-646',
'FINNISH' : 'iso-646-10',
'FRENCH' : 'iso-646',
'CANADIAN' : 'iso-646',
'GERMAN' : 'iso-646',
'ITALIAN' : 'iso-646',
'NORWEIG' : 'iso-646',
'PORTU' : 'iso-646',
'SPANISH' : 'iso-656',
'SWEDISH' : 'iso-646-10',
'SWISS' : 'iso-646',
'UK' : 'iso-646',
'ISO-10' : 'iso-646-10',
// level 2
'CP437' : 'cp437',
'CP850' : 'cp850',
'CP852' : 'cp852',
'CP866' : 'cp866',
'CP848' : 'cp848',
'CP1250' : 'cp1250',
'CP1251' : 'cp1251',
'CP1252' : 'cp1252',
'CP10000' : 'macroman',
'LATIN-1' : 'iso-8859-1',
'LATIN-2' : 'iso-8859-2',
'LATIN-5' : 'iso-8859-9',
'LATIN-9' : 'iso-8859-15',
// level 2
'CP437' : 'cp437',
'CP850' : 'cp850',
'CP852' : 'cp852',
'CP866' : 'cp866',
'CP848' : 'cp848',
'CP1250' : 'cp1250',
'CP1251' : 'cp1251',
'CP1252' : 'cp1252',
'CP10000' : 'macroman',
'LATIN-1' : 'iso-8859-1',
'LATIN-2' : 'iso-8859-2',
'LATIN-5' : 'iso-8859-9',
'LATIN-9' : 'iso-8859-15',
// level 4
'UTF-8' : 'utf8',
// level 4
'UTF-8' : 'utf8',
// deprecated stuff
'IBMPC' : 'cp1250', // :TODO: validate
'+7_FIDO' : 'cp866',
'+7' : 'cp866',
'MAC' : 'macroman', // :TODO: validate
// deprecated stuff
'IBMPC' : 'cp1250', // :TODO: validate
'+7_FIDO' : 'cp866',
'+7' : 'cp866',
'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)
function HorizontalMenuView(options) {
options.cursor = options.cursor || 'hide';
options.cursor = options.cursor || 'hide';
if(!_.isNumber(options.itemSpacing)) {
options.itemSpacing = 1;
}
if(!_.isNumber(options.itemSpacing)) {
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() {
return new Array(self.itemSpacing + 1).join(' ');
};
this.getSpacer = function() {
return new Array(self.itemSpacing + 1).join(' ');
};
this.performAutoScale = function() {
if(self.autoScale.width) {
var spacer = self.getSpacer();
var width = self.items.join(spacer).length + (spacer.length * 2);
assert(width <= self.client.term.termWidth - self.position.col);
self.dimens.width = width;
}
};
this.performAutoScale = function() {
if(self.autoScale.width) {
var spacer = self.getSpacer();
var width = self.items.join(spacer).length + (spacer.length * 2);
assert(width <= self.client.term.termWidth - self.position.col);
self.dimens.width = width;
}
};
this.performAutoScale();
this.performAutoScale();
this.cachePositions = function() {
if(this.positionCacheExpired) {
var col = self.position.col;
var spacer = self.getSpacer();
this.cachePositions = function() {
if(this.positionCacheExpired) {
var col = self.position.col;
var spacer = self.getSpacer();
for(var i = 0; i < self.items.length; ++i) {
self.items[i].col = col;
col += spacer.length + self.items[i].text.length + spacer.length;
}
}
for(var i = 0; i < self.items.length; ++i) {
self.items[i].col = col;
col += spacer.length + self.items[i].text.length + spacer.length;
}
}
this.positionCacheExpired = false;
};
this.positionCacheExpired = false;
};
this.drawItem = function(index) {
assert(!this.positionCacheExpired);
this.drawItem = function(index) {
assert(!this.positionCacheExpired);
const item = self.items[index];
if(!item) {
return;
}
const item = self.items[index];
if(!item) {
return;
}
let text;
let sgr;
if(item.focused && self.hasFocusItems()) {
const focusItem = self.focusItems[index];
text = focusItem ? focusItem.text : item.text;
sgr = '';
} else if(this.complexItems) {
text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item));
sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR());
} else {
text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle);
sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR());
}
let text;
let sgr;
if(item.focused && self.hasFocusItems()) {
const focusItem = self.focusItems[index];
text = focusItem ? focusItem.text : item.text;
sgr = '';
} else if(this.complexItems) {
text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item));
sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR());
} else {
text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle);
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(
`${goto(self.position.row, item.col)}${sgr}${strUtil.pad(text, drawWidth, self.fillChar, 'center')}`
);
};
self.client.term.write(
`${goto(self.position.row, item.col)}${sgr}${strUtil.pad(text, drawWidth, self.fillChar, 'center')}`
);
};
}
require('util').inherits(HorizontalMenuView, MenuView);
HorizontalMenuView.prototype.setHeight = function(height) {
height = parseInt(height, 10);
assert(1 === height); // nothing else allowed here
HorizontalMenuView.super_.prototype.setHeight(this, height);
height = parseInt(height, 10);
assert(1 === height); // nothing else allowed here
HorizontalMenuView.super_.prototype.setHeight(this, height);
};
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) {
this.items[i].focused = this.focusedItemIndex === i;
this.drawItem(i);
}
for(var i = 0; i < this.items.length; ++i) {
this.items[i].focused = this.focusedItemIndex === i;
this.drawItem(i);
}
};
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.super_.prototype.setFocus.call(this, focused);
HorizontalMenuView.super_.prototype.setFocus.call(this, focused);
this.redraw();
this.redraw();
};
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() {
if(this.items.length - 1 === this.focusedItemIndex) {
this.focusedItemIndex = 0;
} else {
this.focusedItemIndex++;
}
if(this.items.length - 1 === this.focusedItemIndex) {
this.focusedItemIndex = 0;
} else {
this.focusedItemIndex++;
}
// :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes
this.redraw();
// :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes
this.redraw();
HorizontalMenuView.super_.prototype.focusNext.call(this);
HorizontalMenuView.super_.prototype.focusNext.call(this);
};
HorizontalMenuView.prototype.focusPrevious = function() {
if(0 === this.focusedItemIndex) {
this.focusedItemIndex = this.items.length - 1;
} else {
this.focusedItemIndex--;
}
if(0 === this.focusedItemIndex) {
this.focusedItemIndex = this.items.length - 1;
} else {
this.focusedItemIndex--;
}
// :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes
this.redraw();
// :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes
this.redraw();
HorizontalMenuView.super_.prototype.focusPrevious.call(this);
HorizontalMenuView.super_.prototype.focusPrevious.call(this);
};
HorizontalMenuView.prototype.onKeyPress = function(ch, key) {
if(key) {
if(this.isKeyMapped('left', key.name)) {
this.focusPrevious();
} else if(this.isKeyMapped('right', key.name)) {
this.focusNext();
}
}
if(key) {
if(this.isKeyMapped('left', key.name)) {
this.focusPrevious();
} else if(this.isKeyMapped('right', key.name)) {
this.focusNext();
}
}
HorizontalMenuView.super_.prototype.onKeyPress.call(this, ch, key);
HorizontalMenuView.super_.prototype.onKeyPress.call(this, ch, key);
};
HorizontalMenuView.prototype.getData = function() {
const item = this.getItem(this.focusedItemIndex);
return _.isString(item.data) ? item.data : this.focusedItemIndex;
const item = this.getItem(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');
module.exports = class KeyEntryView extends View {
constructor(options) {
options.acceptsFocus = valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = valueWithDefault(options.acceptsInput, true);
constructor(options) {
options.acceptsFocus = valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = valueWithDefault(options.acceptsInput, true);
super(options);
super(options);
this.eatTabKey = options.eatTabKey || true;
this.caseInsensitive = options.caseInsensitive || true;
this.eatTabKey = options.eatTabKey || true;
this.caseInsensitive = options.caseInsensitive || true;
if(Array.isArray(options.keys)) {
if(this.caseInsensitive) {
this.keys = options.keys.map( k => k.toUpperCase() );
} else {
this.keys = options.keys;
}
}
}
if(Array.isArray(options.keys)) {
if(this.caseInsensitive) {
this.keys = options.keys.map( k => k.toUpperCase() );
} else {
this.keys = options.keys;
}
}
}
onKeyPress(ch, key) {
const drawKey = ch;
onKeyPress(ch, key) {
const drawKey = ch;
if(ch && this.caseInsensitive) {
ch = ch.toUpperCase();
}
if(ch && this.caseInsensitive) {
ch = ch.toUpperCase();
}
if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) {
this.redraw(); // sets position
this.client.term.write(stylizeString(ch, this.textStyle));
}
if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) {
this.redraw(); // sets position
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) {
return this.emit('action', 'next', key);
}
if(key && 'tab' === key.name && !this.eatTabKey) {
return this.emit('action', 'next', key);
}
this.emit('action', 'accept');
// NOTE: we don't call super here. KeyEntryView is a special snowflake.
}
this.emit('action', 'accept');
// NOTE: we don't call super here. KeyEntryView is a special snowflake.
}
setPropertyValue(propName, propValue) {
switch(propName) {
case 'eatTabKey' :
if(_.isBoolean(propValue)) {
this.eatTabKey = propValue;
}
break;
setPropertyValue(propName, propValue) {
switch(propName) {
case 'eatTabKey' :
if(_.isBoolean(propValue)) {
this.eatTabKey = propValue;
}
break;
case 'caseInsensitive' :
if(_.isBoolean(propValue)) {
this.caseInsensitive = propValue;
}
break;
case 'caseInsensitive' :
if(_.isBoolean(propValue)) {
this.caseInsensitive = propValue;
}
break;
case 'keys' :
if(Array.isArray(propValue)) {
this.keys = propValue;
}
break;
}
case 'keys' :
if(Array.isArray(propValue)) {
this.keys = propValue;
}
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 = {
name : 'Last Callers',
desc : 'Last callers to the system',
author : 'NuSkooler',
packageName : 'codes.l33t.enigma.lastcallers'
name : 'Last Callers',
desc : 'Last callers to the system',
author : 'NuSkooler',
packageName : 'codes.l33t.enigma.lastcallers'
};
const MciCodeIds = {
CallerList : 1,
CallerList : 1,
};
exports.getModule = class LastCallersModule extends MenuModule {
constructor(options) {
super(options);
}
constructor(options) {
super(options);
}
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
let loginHistory;
let callersView;
let loginHistory;
let callersView;
async.series(
[
function loadFromConfig(callback) {
const loadOpts = {
callingMenu : self,
mciMap : mciData.menu,
noInput : true,
};
async.series(
[
function loadFromConfig(callback) {
const loadOpts = {
callingMenu : self,
mciMap : mciData.menu,
noInput : true,
};
vc.loadFromMenuConfig(loadOpts, callback);
},
function fetchHistory(callback) {
callersView = vc.getView(MciCodeIds.CallerList);
vc.loadFromMenuConfig(loadOpts, callback);
},
function fetchHistory(callback) {
callersView = vc.getView(MciCodeIds.CallerList);
// fetch up
StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => {
loginHistory = lh;
// fetch up
StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => {
loginHistory = lh;
if(self.menuConfig.config.hideSysOpLogin) {
const noOpLoginHistory = loginHistory.filter(lh => {
return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId
});
if(self.menuConfig.config.hideSysOpLogin) {
const noOpLoginHistory = loginHistory.filter(lh => {
return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId
});
//
// 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.
//
if(noOpLoginHistory.length >= callersView.dimens.height || 'always' === self.menuConfig.config.hideSysOpLogin) {
loginHistory = noOpLoginHistory;
}
}
//
// 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.
//
if(noOpLoginHistory.length >= callersView.dimens.height || 'always' === self.menuConfig.config.hideSysOpLogin) {
loginHistory = noOpLoginHistory;
}
}
//
// Finally, we need to trim up the list to the needed size
//
loginHistory = loginHistory.slice(0, callersView.dimens.height);
//
// Finally, we need to trim up the list to the needed size
//
loginHistory = loginHistory.slice(0, callersView.dimens.height);
return callback(err);
});
},
function getUserNamesAndProperties(callback) {
const getPropOpts = {
names : [ 'location', 'affiliation' ]
};
return callback(err);
});
},
function getUserNamesAndProperties(callback) {
const getPropOpts = {
names : [ 'location', 'affiliation' ]
};
const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD';
const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD';
async.each(
loginHistory,
(item, next) => {
item.userId = parseInt(item.log_value);
item.ts = moment(item.timestamp).format(dateTimeFormat);
async.each(
loginHistory,
(item, next) => {
item.userId = parseInt(item.log_value);
item.ts = moment(item.timestamp).format(dateTimeFormat);
User.getUserName(item.userId, (err, userName) => {
if(err) {
item.deleted = true;
return next(null);
} else {
item.userName = userName || 'N/A';
User.getUserName(item.userId, (err, userName) => {
if(err) {
item.deleted = true;
return next(null);
} else {
item.userName = userName || 'N/A';
User.loadProperties(item.userId, getPropOpts, (err, props) => {
if(!err && props) {
item.location = props.location || 'N/A';
item.affiliation = item.affils = (props.affiliation || 'N/A');
} else {
item.location = 'N/A';
item.affiliation = item.affils = 'N/A';
}
return next(null);
});
}
});
},
err => {
loginHistory = loginHistory.filter(lh => true !== lh.deleted);
return callback(err);
}
);
},
function populateList(callback) {
const listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affiliation} - {ts}';
User.loadProperties(item.userId, getPropOpts, (err, props) => {
if(!err && props) {
item.location = props.location || 'N/A';
item.affiliation = item.affils = (props.affiliation || 'N/A');
} else {
item.location = 'N/A';
item.affiliation = item.affils = 'N/A';
}
return next(null);
});
}
});
},
err => {
loginHistory = loginHistory.filter(lh => true !== lh.deleted);
return callback(err);
}
);
},
function populateList(callback) {
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();
return callback(null);
}
],
(err) => {
if(err) {
self.client.log.error( { error : err.toString() }, 'Error loading last callers');
}
cb(err);
}
);
});
}
callersView.redraw();
return callback(null);
}
],
(err) => {
if(err) {
self.client.log.error( { error : err.toString() }, 'Error loading last callers');
}
cb(err);
}
);
});
}
};

View File

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

View File

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

View File

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

View File

@ -8,29 +8,29 @@ var _ = require('lodash');
module.exports = MailPacket;
function MailPacket(options) {
events.EventEmitter.call(this);
events.EventEmitter.call(this);
// map of network name -> address obj ( { zone, net, node, point, domain } )
this.nodeAddresses = options.nodeAddresses || {};
// map of network name -> address obj ( { zone, net, node, point, domain } )
this.nodeAddresses = options.nodeAddresses || {};
}
require('util').inherits(MailPacket, events.EventEmitter);
MailPacket.prototype.read = function(options) {
//
// options.packetPath | opts.packetBuffer: supplies a path-to-file
// or a buffer containing packet data
//
// emits 'message' event per message read
//
assert(_.isString(options.packetPath) || Buffer.isBuffer(options.packetBuffer));
//
// options.packetPath | opts.packetBuffer: supplies a path-to-file
// or a buffer containing packet data
//
// emits 'message' event per message read
//
assert(_.isString(options.packetPath) || Buffer.isBuffer(options.packetBuffer));
};
MailPacket.prototype.write = function(options) {
//
// options.messages[]: array of message(s) to create packets from
//
// emits 'packet' event per packet constructed
//
assert(_.isArray(options.messages));
//
// options.messages[]: array of message(s) to create packets from
//
// emits 'packet' event per packet constructed
//
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' }
*/
function getAddressedToInfo(input) {
input = input.trim();
input = input.trim();
const firstAtPos = input.indexOf('@');
const firstAtPos = input.indexOf('@');
if(firstAtPos < 0) {
let addr = Address.fromString(input);
if(Address.isValidAddress(addr)) {
return { flavor : Message.AddressFlavor.FTN, remote : input };
}
if(firstAtPos < 0) {
let addr = Address.fromString(input);
if(Address.isValidAddress(addr)) {
return { flavor : Message.AddressFlavor.FTN, remote : input };
}
const lessThanPos = input.indexOf('<');
if(lessThanPos < 0) {
return { name : input, flavor : Message.AddressFlavor.Local };
}
const lessThanPos = input.indexOf('<');
if(lessThanPos < 0) {
return { name : input, flavor : Message.AddressFlavor.Local };
}
const greaterThanPos = input.indexOf('>');
if(greaterThanPos < lessThanPos) {
return { name : input, flavor : Message.AddressFlavor.Local };
}
const greaterThanPos = input.indexOf('>');
if(greaterThanPos < lessThanPos) {
return { name : input, flavor : Message.AddressFlavor.Local };
}
addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos));
if(Address.isValidAddress(addr)) {
return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() };
}
addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos));
if(Address.isValidAddress(addr)) {
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 greaterThanPos = input.indexOf('>');
if(lessThanPos > 0 && greaterThanPos > lessThanPos) {
const addr = input.slice(lessThanPos + 1, greaterThanPos);
const m = addr.match(EMAIL_REGEX);
if(m) {
return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.Email, remote : addr };
}
const lessThanPos = input.indexOf('<');
const greaterThanPos = input.indexOf('>');
if(lessThanPos > 0 && greaterThanPos > lessThanPos) {
const addr = input.slice(lessThanPos + 1, greaterThanPos);
const m = addr.match(EMAIL_REGEX);
if(m) {
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);
if(m) {
return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input };
}
let m = input.match(EMAIL_REGEX);
if(m) {
return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input };
}
let addr = Address.fromString(input); // 5D?
if(Address.isValidAddress(addr)) {
return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ;
}
let addr = Address.fromString(input); // 5D?
if(Address.isValidAddress(addr)) {
return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ;
}
addr = Address.fromString(input.slice(firstAtPos + 1).trim());
if(Address.isValidAddress(addr)) {
return { name : input.slice(0, firstAtPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() };
}
addr = Address.fromString(input.slice(firstAtPos + 1).trim());
if(Address.isValidAddress(addr)) {
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) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
options.resizable = false;
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
options.resizable = false;
TextView.call(this, options);
TextView.call(this, options);
this.cursorPos = { x : 0 };
this.patternArrayPos = 0;
this.cursorPos = { x : 0 };
this.patternArrayPos = 0;
var self = this;
var self = this;
this.maskPattern = options.maskPattern || '';
this.maskPattern = options.maskPattern || '';
this.clientBackspace = function() {
var fillCharSGR = this.getStyleSGR(3) || this.getSGR();
this.client.term.write('\b' + fillCharSGR + this.fillChar + '\b' + this.getFocusSGR());
};
this.clientBackspace = function() {
var fillCharSGR = this.getStyleSGR(3) || this.getSGR();
this.client.term.write('\b' + fillCharSGR + this.fillChar + '\b' + this.getFocusSGR());
};
this.drawText = function(s) {
var textToDraw = strUtil.stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle);
this.drawText = function(s) {
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
var i = 0;
var t = 0;
while(i < self.patternArray.length) {
if(_.isRegExp(self.patternArray[i])) {
if(t < textToDraw.length) {
self.client.term.write((self.hasFocus ? self.getFocusSGR() : self.getSGR()) + textToDraw[t]);
t++;
} else {
self.client.term.write((self.getStyleSGR(3) || '') + self.fillChar);
}
} else {
var styleSgr = this.hasFocus ? (self.getStyleSGR(2) || '') : (self.getStyleSGR(1) || '');
self.client.term.write(styleSgr + self.maskPattern[i]);
}
i++;
}
};
// draw out the text we have so far
var i = 0;
var t = 0;
while(i < self.patternArray.length) {
if(_.isRegExp(self.patternArray[i])) {
if(t < textToDraw.length) {
self.client.term.write((self.hasFocus ? self.getFocusSGR() : self.getSGR()) + textToDraw[t]);
t++;
} else {
self.client.term.write((self.getStyleSGR(3) || '') + self.fillChar);
}
} else {
var styleSgr = this.hasFocus ? (self.getStyleSGR(2) || '') : (self.getStyleSGR(1) || '');
self.client.term.write(styleSgr + self.maskPattern[i]);
}
i++;
}
};
this.buildPattern = function() {
self.patternArray = [];
self.maxLength = 0;
this.buildPattern = function() {
self.patternArray = [];
self.maxLength = 0;
for(var i = 0; i < self.maskPattern.length; i++) {
// :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark!
if(self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) {
self.patternArray.push(MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]);
++self.maxLength;
} else {
self.patternArray.push(self.maskPattern[i]);
}
}
};
for(var i = 0; i < self.maskPattern.length; i++) {
// :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark!
if(self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) {
self.patternArray.push(MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]);
++self.maxLength;
} else {
self.patternArray.push(self.maskPattern[i]);
}
}
};
this.getEndOfTextColumn = function() {
return this.position.col + this.patternArrayPos;
};
this.getEndOfTextColumn = function() {
return this.position.col + this.patternArrayPos;
};
this.buildPattern();
this.buildPattern();
}
require('util').inherits(MaskEditTextView, TextView);
MaskEditTextView.maskPatternCharacterRegEx = {
'#' : /[0-9]/, // Numeric
'A' : /[a-zA-Z]/, // Alpha
'@' : /[0-9a-zA-Z]/, // Alphanumeric
'&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255
'#' : /[0-9]/, // Numeric
'A' : /[a-zA-Z]/, // Alpha
'@' : /[0-9a-zA-Z]/, // Alphanumeric
'&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255
};
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()
this.patternArrayPos = this.patternArray.length;
}
if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText()
this.patternArrayPos = this.patternArray.length;
}
};
MaskEditTextView.prototype.setMaskPattern = function(pattern) {
this.dimens.width = pattern.length;
this.dimens.width = pattern.length;
this.maskPattern = pattern;
this.buildPattern();
this.maskPattern = pattern;
this.buildPattern();
};
MaskEditTextView.prototype.onKeyPress = function(ch, key) {
if(key) {
if(this.isKeyMapped('backspace', key.name)) {
if(this.text.length > 0) {
this.patternArrayPos--;
assert(this.patternArrayPos >= 0);
if(key) {
if(this.isKeyMapped('backspace', key.name)) {
if(this.text.length > 0) {
this.patternArrayPos--;
assert(this.patternArrayPos >= 0);
if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
this.text = this.text.substr(0, this.text.length - 1);
this.clientBackspace();
} else {
while(this.patternArrayPos > 0) {
if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
this.text = this.text.substr(0, this.text.length - 1);
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1));
this.clientBackspace();
break;
}
this.patternArrayPos--;
}
}
}
if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
this.text = this.text.substr(0, this.text.length - 1);
this.clientBackspace();
} else {
while(this.patternArrayPos > 0) {
if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
this.text = this.text.substr(0, this.text.length - 1);
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1));
this.clientBackspace();
break;
}
this.patternArrayPos--;
}
}
}
return;
} else if(this.isKeyMapped('clearLine', key.name)) {
this.text = '';
this.patternArrayPos = 0;
this.setFocus(true); // redraw + adjust cursor
return;
} else if(this.isKeyMapped('clearLine', key.name)) {
this.text = '';
this.patternArrayPos = 0;
this.setFocus(true); // redraw + adjust cursor
return;
}
}
return;
}
}
if(ch && strUtil.isPrintable(ch)) {
if(this.text.length < this.maxLength) {
ch = strUtil.stylizeString(ch, this.textStyle);
if(ch && strUtil.isPrintable(ch)) {
if(this.text.length < this.maxLength) {
ch = strUtil.stylizeString(ch, this.textStyle);
if(!ch.match(this.patternArray[this.patternArrayPos])) {
return;
}
if(!ch.match(this.patternArray[this.patternArrayPos])) {
return;
}
this.text += ch;
this.patternArrayPos++;
this.text += ch;
this.patternArrayPos++;
while(this.patternArrayPos < this.patternArray.length &&
while(this.patternArrayPos < this.patternArray.length &&
!_.isRegExp(this.patternArray[this.patternArrayPos]))
{
this.patternArrayPos++;
}
{
this.patternArrayPos++;
}
this.redraw();
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn()));
}
}
this.redraw();
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) {
switch(propName) {
case 'maskPattern' : this.setMaskPattern(value); break;
}
switch(propName) {
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() {
var rawData = MaskEditTextView.super_.prototype.getData.call(this);
var rawData = MaskEditTextView.super_.prototype.getData.call(this);
if(!rawData || 0 === rawData.length) {
return rawData;
}
if(!rawData || 0 === rawData.length) {
return rawData;
}
var data = '';
var data = '';
assert(rawData.length <= this.patternArray.length);
assert(rawData.length <= this.patternArray.length);
var p = 0;
for(var i = 0; i < this.patternArray.length; ++i) {
if(_.isRegExp(this.patternArray[i])) {
data += rawData[p++];
} else {
data += this.patternArray[i];
}
}
var p = 0;
for(var i = 0; i < this.patternArray.length; ++i) {
if(_.isRegExp(this.patternArray[i])) {
data += rawData[p++];
} else {
data += this.patternArray[i];
}
}
return data;
return data;
};

View File

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

View File

@ -19,358 +19,358 @@ const _ = require('lodash');
exports.MenuModule = class MenuModule extends PluginModule {
constructor(options) {
super(options);
constructor(options) {
super(options);
this.menuName = options.menuName;
this.menuConfig = options.menuConfig;
this.client = options.client;
this.menuConfig.options = options.menuConfig.options || {};
this.menuMethods = {}; // methods called from @method's
this.menuConfig.config = this.menuConfig.config || {};
this.menuName = options.menuName;
this.menuConfig = options.menuConfig;
this.client = options.client;
this.menuConfig.options = options.menuConfig.options || {};
this.menuMethods = {}; // methods called from @method's
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() {
this.initSequence();
}
enter() {
this.initSequence();
}
leave() {
this.detachViewControllers();
}
leave() {
this.detachViewControllers();
}
initSequence() {
const self = this;
const mciData = {};
let pausePosition;
initSequence() {
const self = this;
const mciData = {};
let pausePosition;
async.series(
[
function beforeDisplayArt(callback) {
self.beforeArt(callback);
},
function displayMenuArt(callback) {
if(!_.isString(self.menuConfig.art)) {
return callback(null);
}
async.series(
[
function beforeDisplayArt(callback) {
self.beforeArt(callback);
},
function displayMenuArt(callback) {
if(!_.isString(self.menuConfig.art)) {
return callback(null);
}
self.displayAsset(
self.menuConfig.art,
self.menuConfig.options,
(err, artData) => {
if(err) {
self.client.log.trace('Could not display art', { art : self.menuConfig.art, reason : err.message } );
} else {
mciData.menu = artData.mciMap;
}
self.displayAsset(
self.menuConfig.art,
self.menuConfig.options,
(err, artData) => {
if(err) {
self.client.log.trace('Could not display art', { art : self.menuConfig.art, reason : err.message } );
} else {
mciData.menu = artData.mciMap;
}
return callback(null); // any errors are non-fatal
}
);
},
function moveToPromptLocation(callback) {
if(self.menuConfig.prompt) {
// :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements
}
return callback(null); // any errors are non-fatal
}
);
},
function moveToPromptLocation(callback) {
if(self.menuConfig.prompt) {
// :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements
}
return callback(null);
},
function displayPromptArt(callback) {
if(!_.isString(self.menuConfig.prompt)) {
return callback(null);
}
return callback(null);
},
function displayPromptArt(callback) {
if(!_.isString(self.menuConfig.prompt)) {
return callback(null);
}
if(!_.isObject(self.menuConfig.promptConfig)) {
return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found'));
}
if(!_.isObject(self.menuConfig.promptConfig)) {
return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found'));
}
self.displayAsset(
self.menuConfig.promptConfig.art,
self.menuConfig.options,
(err, artData) => {
if(artData) {
mciData.prompt = artData.mciMap;
}
return callback(err); // pass err here; prompts *must* have art
}
);
},
function recordCursorPosition(callback) {
if(!self.shouldPause()) {
return callback(null); // cursor position not needed
}
self.displayAsset(
self.menuConfig.promptConfig.art,
self.menuConfig.options,
(err, artData) => {
if(artData) {
mciData.prompt = artData.mciMap;
}
return callback(err); // pass err here; prompts *must* have art
}
);
},
function recordCursorPosition(callback) {
if(!self.shouldPause()) {
return callback(null); // cursor position not needed
}
self.client.once('cursor position report', pos => {
pausePosition = { row : pos[0], col : 1 };
self.client.log.trace('After art position recorded', pausePosition );
return callback(null);
});
self.client.once('cursor position report', pos => {
pausePosition = { row : pos[0], col : 1 };
self.client.log.trace('After art position recorded', pausePosition );
return callback(null);
});
self.client.term.rawWrite(ansi.queryPos());
},
function afterArtDisplayed(callback) {
return self.mciReady(mciData, callback);
},
function displayPauseIfRequested(callback) {
if(!self.shouldPause()) {
return callback(null);
}
self.client.term.rawWrite(ansi.queryPos());
},
function afterArtDisplayed(callback) {
return self.mciReady(mciData, callback);
},
function displayPauseIfRequested(callback) {
if(!self.shouldPause()) {
return callback(null);
}
return self.pausePrompt(pausePosition, callback);
},
function finishAndNext(callback) {
self.finishedLoading();
return self.autoNextMenu(callback);
}
],
err => {
if(err) {
self.client.log.warn('Error during init sequence', { error : err.message } );
return self.pausePrompt(pausePosition, callback);
},
function finishAndNext(callback) {
self.finishedLoading();
return self.autoNextMenu(callback);
}
],
err => {
if(err) {
self.client.log.warn('Error during init sequence', { error : err.message } );
return self.prevMenu( () => { /* dummy */ } );
}
}
);
}
return self.prevMenu( () => { /* dummy */ } );
}
}
);
}
beforeArt(cb) {
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
this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate));
}
beforeArt(cb) {
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
this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate));
}
if(this.cls) {
this.client.term.rawWrite(ansi.resetScreen());
}
if(this.cls) {
this.client.term.rawWrite(ansi.resetScreen());
}
return cb(null);
}
return cb(null);
}
mciReady(mciData, cb) {
// available for sub-classes
return cb(null);
}
mciReady(mciData, cb) {
// available for sub-classes
return cb(null);
}
finishedLoading() {
// nothing in base
}
finishedLoading() {
// nothing in base
}
getSaveState() {
// nothing in base
}
getSaveState() {
// nothing in base
}
restoreSavedState(/*savedState*/) {
// nothing in base
}
restoreSavedState(/*savedState*/) {
// nothing in base
}
getMenuResult() {
// default to the formData that was provided @ a submit, if any
return this.submitFormData;
}
getMenuResult() {
// default to the formData that was provided @ a submit, if any
return this.submitFormData;
}
nextMenu(cb) {
if(!this.haveNext()) {
return this.prevMenu(cb); // no next, go to prev
}
nextMenu(cb) {
if(!this.haveNext()) {
return this.prevMenu(cb); // no next, go to prev
}
return this.client.menuStack.next(cb);
}
return this.client.menuStack.next(cb);
}
prevMenu(cb) {
return this.client.menuStack.prev(cb);
}
prevMenu(cb) {
return this.client.menuStack.prev(cb);
}
gotoMenu(name, options, cb) {
return this.client.menuStack.goto(name, options, cb);
}
gotoMenu(name, options, cb) {
return this.client.menuStack.goto(name, options, cb);
}
addViewController(name, vc) {
assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`);
addViewController(name, vc) {
assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`);
this.viewControllers[name] = vc;
return vc;
}
this.viewControllers[name] = vc;
return vc;
}
detachViewControllers() {
Object.keys(this.viewControllers).forEach( name => {
this.viewControllers[name].detachClientEvents();
});
}
detachViewControllers() {
Object.keys(this.viewControllers).forEach( name => {
this.viewControllers[name].detachClientEvents();
});
}
shouldPause() {
return ('end' === this.menuConfig.options.pause || true === this.menuConfig.options.pause);
}
shouldPause() {
return ('end' === this.menuConfig.options.pause || true === this.menuConfig.options.pause);
}
hasNextTimeout() {
return _.isNumber(this.menuConfig.options.nextTimeout);
}
hasNextTimeout() {
return _.isNumber(this.menuConfig.options.nextTimeout);
}
haveNext() {
return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next));
}
haveNext() {
return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next));
}
autoNextMenu(cb) {
const self = this;
autoNextMenu(cb) {
const self = this;
function gotoNextMenu() {
if(self.haveNext()) {
return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb);
} else {
return self.prevMenu(cb);
}
}
function gotoNextMenu() {
if(self.haveNext()) {
return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb);
} else {
return self.prevMenu(cb);
}
}
if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) {
if(this.hasNextTimeout()) {
setTimeout( () => {
return gotoNextMenu();
}, this.menuConfig.options.nextTimeout);
} else {
return gotoNextMenu();
}
}
}
if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) {
if(this.hasNextTimeout()) {
setTimeout( () => {
return gotoNextMenu();
}, this.menuConfig.options.nextTimeout);
} else {
return gotoNextMenu();
}
}
}
standardMCIReadyHandler(mciData, cb) {
//
// A quick rundown:
// * We may have mciData.menu, mciData.prompt, or both.
// * 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)
//
const self = this;
standardMCIReadyHandler(mciData, cb) {
//
// A quick rundown:
// * We may have mciData.menu, mciData.prompt, or both.
// * 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)
//
const self = this;
async.series(
[
function addViewControllers(callback) {
_.forEach(mciData, (mciMap, name) => {
assert('menu' === name || 'prompt' === name);
self.addViewController(name, new ViewController( { client : self.client } ) );
});
async.series(
[
function addViewControllers(callback) {
_.forEach(mciData, (mciMap, name) => {
assert('menu' === name || 'prompt' === name);
self.addViewController(name, new ViewController( { client : self.client } ) );
});
return callback(null);
},
function createMenu(callback) {
if(!self.viewControllers.menu) {
return callback(null);
}
return callback(null);
},
function createMenu(callback) {
if(!self.viewControllers.menu) {
return callback(null);
}
const menuLoadOpts = {
mciMap : mciData.menu,
callingMenu : self,
withoutForm : _.isObject(mciData.prompt),
};
const menuLoadOpts = {
mciMap : mciData.menu,
callingMenu : self,
withoutForm : _.isObject(mciData.prompt),
};
self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => {
return callback(err);
});
},
function createPrompt(callback) {
if(!self.viewControllers.prompt) {
return callback(null);
}
self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => {
return callback(err);
});
},
function createPrompt(callback) {
if(!self.viewControllers.prompt) {
return callback(null);
}
const promptLoadOpts = {
callingMenu : self,
mciMap : mciData.prompt,
};
const promptLoadOpts = {
callingMenu : self,
mciMap : mciData.prompt,
};
self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => {
return callback(err);
});
}
],
err => {
return cb(err);
}
);
}
self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => {
return callback(err);
});
}
],
err => {
return cb(err);
}
);
}
displayAsset(name, options, cb) {
if(_.isFunction(options)) {
cb = options;
options = {};
}
displayAsset(name, options, cb) {
if(_.isFunction(options)) {
cb = options;
options = {};
}
if(options.clearScreen) {
this.client.term.rawWrite(ansi.resetScreen());
}
if(options.clearScreen) {
this.client.term.rawWrite(ansi.resetScreen());
}
return theme.displayThemedAsset(
name,
this.client,
Object.assign( { font : this.menuConfig.config.font }, options ),
(err, artData) => {
if(cb) {
return cb(err, artData);
}
}
);
}
return theme.displayThemedAsset(
name,
this.client,
Object.assign( { font : this.menuConfig.config.font }, options ),
(err, artData) => {
if(cb) {
return cb(err, artData);
}
}
);
}
prepViewController(name, formId, mciMap, cb) {
if(_.isUndefined(this.viewControllers[name])) {
const vcOpts = {
client : this.client,
formId : formId,
};
prepViewController(name, formId, mciMap, cb) {
if(_.isUndefined(this.viewControllers[name])) {
const vcOpts = {
client : this.client,
formId : formId,
};
const vc = this.addViewController(name, new ViewController(vcOpts));
const vc = this.addViewController(name, new ViewController(vcOpts));
const loadOpts = {
callingMenu : this,
mciMap : mciMap,
formId : formId,
};
const loadOpts = {
callingMenu : this,
mciMap : mciMap,
formId : formId,
};
return vc.loadFromMenuConfig(loadOpts, err => {
return cb(err, vc);
});
}
return vc.loadFromMenuConfig(loadOpts, err => {
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) {
this.displayAsset(
this.menuConfig.config.art[name],
options,
(err, artData) => {
if(err) {
return cb(err);
}
prepViewControllerWithArt(name, formId, options, cb) {
this.displayAsset(
this.menuConfig.config.art[name],
options,
(err, artData) => {
if(err) {
return cb(err);
}
return this.prepViewController(name, formId, artData.mciMap, cb);
}
);
}
return this.prepViewController(name, formId, artData.mciMap, cb);
}
);
}
optionalMoveToPosition(position) {
if(position) {
position.x = position.row || position.x || 1;
position.y = position.col || position.y || 1;
optionalMoveToPosition(position) {
if(position) {
position.x = position.row || position.x || 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) {
if(!cb && _.isFunction(position)) {
cb = position;
position = null;
}
pausePrompt(position, cb) {
if(!cb && _.isFunction(position)) {
cb = position;
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) => ... )
promptForInput(formName, name, options, cb) {
if(!cb && _.isFunction(options)) {
@ -386,55 +386,55 @@ exports.MenuModule = class MenuModule extends PluginModule {
}
*/
setViewText(formName, mciId, text, appendMultiLine) {
const view = this.viewControllers[formName].getView(mciId);
if(!view) {
return;
}
setViewText(formName, mciId, text, appendMultiLine) {
const view = this.viewControllers[formName].getView(mciId);
if(!view) {
return;
}
if(appendMultiLine && (view instanceof MultiLineEditTextView)) {
view.addText(text);
} else {
view.setText(text);
}
}
if(appendMultiLine && (view instanceof MultiLineEditTextView)) {
view.addText(text);
} else {
view.setText(text);
}
}
updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) {
options = options || {};
updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) {
options = options || {};
let textView;
let customMciId = startId;
const config = this.menuConfig.config;
const endId = options.endId || 99; // we'll fail to get a view before 99
let textView;
let customMciId = startId;
const config = this.menuConfig.config;
const endId = options.endId || 99; // we'll fail to get a view before 99
while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) {
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10"
const format = config[key];
while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) {
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10"
const format = config[key];
if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) {
const text = stringFormat(format, fmtObj);
if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) {
const text = stringFormat(format, fmtObj);
if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) {
textView.addText(text);
} else {
textView.setText(text);
}
}
if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) {
textView.addText(text);
} else {
textView.setText(text);
}
}
++customMciId;
}
}
++customMciId;
}
}
refreshPredefinedMciViewsByCode(formName, mciCodes) {
const form = _.get(this, [ 'viewControllers', formName] );
if(form) {
form.getViewsByMciCode(mciCodes).forEach(v => {
if(!v.setText) {
return;
}
refreshPredefinedMciViewsByCode(formName, mciCodes) {
const form = _.get(this, [ 'viewControllers', formName] );
if(form) {
form.getViewsByMciCode(mciCodes).forEach(v => {
if(!v.setText) {
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! :)
module.exports = class MenuStack {
constructor(client) {
this.client = client;
this.stack = [];
}
constructor(client) {
this.client = client;
this.stack = [];
}
push(moduleInfo) {
return this.stack.push(moduleInfo);
}
push(moduleInfo) {
return this.stack.push(moduleInfo);
}
pop() {
return this.stack.pop();
}
pop() {
return this.stack.pop();
}
peekPrev() {
if(this.stackSize > 1) {
return this.stack[this.stack.length - 2];
}
}
peekPrev() {
if(this.stackSize > 1) {
return this.stack[this.stack.length - 2];
}
}
top() {
if(this.stackSize > 0) {
return this.stack[this.stack.length - 1];
}
}
top() {
if(this.stackSize > 0) {
return this.stack[this.stack.length - 1];
}
}
get stackSize() {
return this.stack.length;
}
get stackSize() {
return this.stack.length;
}
get currentModule() {
const top = this.top();
if(top) {
return top.instance;
}
}
get currentModule() {
const top = this.top();
if(top) {
return top.instance;
}
}
next(cb) {
const currentModuleInfo = this.top();
assert(currentModuleInfo, 'Empty menu stack!');
next(cb) {
const currentModuleInfo = this.top();
assert(currentModuleInfo, 'Empty menu stack!');
const menuConfig = currentModuleInfo.instance.menuConfig;
const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next');
if(!nextMenu) {
return cb(Array.isArray(menuConfig.next) ?
Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH') :
Errors.MenuStack('Invalid or missing "next" member in menu config', 'BADNEXT')
);
}
const menuConfig = currentModuleInfo.instance.menuConfig;
const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next');
if(!nextMenu) {
return cb(Array.isArray(menuConfig.next) ?
Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH') :
Errors.MenuStack('Invalid or missing "next" member in menu config', 'BADNEXT')
);
}
if(nextMenu === currentModuleInfo.name) {
return cb(Errors.MenuStack('Menu config "next" specifies current menu', 'ALREADYTHERE'));
}
if(nextMenu === currentModuleInfo.name) {
return cb(Errors.MenuStack('Menu config "next" specifies current menu', 'ALREADYTHERE'));
}
this.goto(nextMenu, { }, cb);
}
this.goto(nextMenu, { }, cb);
}
prev(cb) {
const menuResult = this.top().instance.getMenuResult();
prev(cb) {
const menuResult = this.top().instance.getMenuResult();
// :TODO: leave() should really take a cb...
this.pop().instance.leave(); // leave & remove current
// :TODO: leave() should really take a cb...
this.pop().instance.leave(); // leave & remove current
const previousModuleInfo = this.pop(); // get previous
const previousModuleInfo = this.pop(); // get previous
if(previousModuleInfo) {
const opts = {
extraArgs : previousModuleInfo.extraArgs,
savedState : previousModuleInfo.savedState,
lastMenuResult : menuResult,
};
if(previousModuleInfo) {
const opts = {
extraArgs : previousModuleInfo.extraArgs,
savedState : previousModuleInfo.savedState,
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) {
const currentModuleInfo = this.top();
goto(name, options, cb) {
const currentModuleInfo = this.top();
if(!cb && _.isFunction(options)) {
cb = options;
options = {};
}
if(!cb && _.isFunction(options)) {
cb = options;
options = {};
}
options = options || {};
const self = this;
options = options || {};
const self = this;
if(currentModuleInfo && name === currentModuleInfo.name) {
if(cb) {
cb(Errors.MenuStack('Already at supplied menu', 'ALREADYTHERE'));
}
return;
}
if(currentModuleInfo && name === currentModuleInfo.name) {
if(cb) {
cb(Errors.MenuStack('Already at supplied menu', 'ALREADYTHERE'));
}
return;
}
const loadOpts = {
name : name,
client : self.client,
};
const loadOpts = {
name : name,
client : self.client,
};
if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) {
loadOpts.extraArgs = currentModuleInfo.extraArgs;
} else {
loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value');
}
loadOpts.lastMenuResult = options.lastMenuResult;
if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) {
loadOpts.extraArgs = currentModuleInfo.extraArgs;
} else {
loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value');
}
loadOpts.lastMenuResult = options.lastMenuResult;
loadMenu(loadOpts, (err, modInst) => {
if(err) {
// :TODO: probably should just require a cb...
const errCb = cb || self.client.defaultHandlerMissingMod();
errCb(err);
} else {
self.client.log.debug( { menuName : name }, 'Goto menu module');
loadMenu(loadOpts, (err, modInst) => {
if(err) {
// :TODO: probably should just require a cb...
const errCb = cb || self.client.defaultHandlerMissingMod();
errCb(err);
} else {
self.client.log.debug( { menuName : name }, 'Goto menu module');
//
// If menuFlags were supplied in menu.hjson, they should win over
// anything supplied in code.
//
let menuFlags;
if(0 === modInst.menuConfig.options.menuFlags.length) {
menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : [];
} else {
menuFlags = modInst.menuConfig.options.menuFlags;
//
// If menuFlags were supplied in menu.hjson, they should win over
// anything supplied in code.
//
let menuFlags;
if(0 === modInst.menuConfig.options.menuFlags.length) {
menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : [];
} else {
menuFlags = modInst.menuConfig.options.menuFlags;
// in code we can ask to merge in
if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) {
menuFlags = _.uniq(menuFlags.concat(options.menuFlags));
}
}
// in code we can ask to merge in
if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) {
menuFlags = _.uniq(menuFlags.concat(options.menuFlags));
}
}
if(currentModuleInfo) {
// save stack state
currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState();
if(currentModuleInfo) {
// save stack state
currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState();
currentModuleInfo.instance.leave();
currentModuleInfo.instance.leave();
if(currentModuleInfo.menuFlags.includes('noHistory')) {
this.pop();
}
if(currentModuleInfo.menuFlags.includes('noHistory')) {
this.pop();
}
if(menuFlags.includes('popParent')) {
this.pop().instance.leave(); // leave & remove current
}
}
if(menuFlags.includes('popParent')) {
this.pop().instance.leave(); // leave & remove current
}
}
self.push({
name : name,
instance : modInst,
extraArgs : loadOpts.extraArgs,
menuFlags : menuFlags,
});
self.push({
name : name,
instance : modInst,
extraArgs : loadOpts.extraArgs,
menuFlags : menuFlags,
});
// restore previous state if requested
if(options.savedState) {
modInst.restoreSavedState(options.savedState);
}
// restore previous state if requested
if(options.savedState) {
modInst.restoreSavedState(options.savedState);
}
const stackEntries = self.stack.map(stackEntry => {
let name = stackEntry.name;
if(stackEntry.instance.menuConfig.options.menuFlags.length > 0) {
name += ` (${stackEntry.instance.menuConfig.options.menuFlags.join(', ')})`;
}
return name;
});
const stackEntries = self.stack.map(stackEntry => {
let name = stackEntry.name;
if(stackEntry.instance.menuConfig.options.menuFlags.length > 0) {
name += ` (${stackEntry.instance.menuConfig.options.menuFlags.join(', ')})`;
}
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) {
cb(null);
}
}
});
}
if(cb) {
cb(null);
}
}
});
}
};

View File

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

View File

@ -14,264 +14,264 @@ const _ = require('lodash');
exports.MenuView = MenuView;
function MenuView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, 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) {
this.setItems(options.items);
} else {
this.items = [];
}
if(options.items) {
this.setItems(options.items);
} else {
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 = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0;
this.focusedItemIndex = options.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
this.focusPrefix = options.focusPrefix || '';
this.focusSuffix = options.focusSuffix || '';
// :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization
this.focusPrefix = options.focusPrefix || '';
this.focusSuffix = options.focusSuffix || '';
this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1);
this.justify = options.justify || 'none';
this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1);
this.justify = options.justify || 'none';
this.hasFocusItems = function() {
return !_.isUndefined(self.focusItems);
};
this.hasFocusItems = function() {
return !_.isUndefined(self.focusItems);
};
this.getHotKeyItemIndex = function(ch) {
if(ch && self.hotKeys) {
const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch];
if(_.isNumber(keyIndex)) {
return keyIndex;
}
}
return -1;
};
this.getHotKeyItemIndex = function(ch) {
if(ch && self.hotKeys) {
const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch];
if(_.isNumber(keyIndex)) {
return keyIndex;
}
}
return -1;
};
this.emitIndexUpdate = function() {
self.emit('index update', self.focusedItemIndex);
}
this.emitIndexUpdate = function() {
self.emit('index update', self.focusedItemIndex);
};
}
util.inherits(MenuView, View);
MenuView.prototype.setItems = function(items) {
if(Array.isArray(items)) {
this.sorted = false;
this.renderCache = {};
if(Array.isArray(items)) {
this.sorted = false;
this.renderCache = {};
//
// Items can be an array of strings or an array of objects.
//
// In the case of objects, items are considered complex and
// may have one or more members that can later be formatted
// against. The default member is 'text'. The member 'data'
// may be overridden to provide a form value other than the
// item's index.
//
// Items can be formatted with 'itemFormat' and 'focusItemFormat'
//
let text;
let stringItem;
this.items = items.map(item => {
stringItem = _.isString(item);
if(stringItem) {
text = item;
} else {
text = item.text || '';
this.complexItems = true;
}
//
// Items can be an array of strings or an array of objects.
//
// In the case of objects, items are considered complex and
// may have one or more members that can later be formatted
// against. The default member is 'text'. The member 'data'
// may be overridden to provide a form value other than the
// item's index.
//
// Items can be formatted with 'itemFormat' and 'focusItemFormat'
//
let text;
let stringItem;
this.items = items.map(item => {
stringItem = _.isString(item);
if(stringItem) {
text = item;
} else {
text = item.text || '';
this.complexItems = true;
}
text = this.disablePipe ? text : pipeToAnsi(text, this.client);
return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others
});
text = this.disablePipe ? text : pipeToAnsi(text, this.client);
return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others
});
if(this.complexItems) {
this.itemFormat = this.itemFormat || '{text}';
}
}
if(this.complexItems) {
this.itemFormat = this.itemFormat || '{text}';
}
}
};
MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) {
const item = this.renderCache[index];
return item && item[focusItem ? 'focus' : 'standard'];
const item = this.renderCache[index];
return item && item[focusItem ? 'focus' : 'standard'];
};
MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) {
this.renderCache[index] = this.renderCache[index] || {};
this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered;
this.renderCache[index] = this.renderCache[index] || {};
this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered;
};
MenuView.prototype.setSort = function(sort) {
if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) {
return;
}
if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) {
return;
}
const key = true === sort ? 'text' : sort;
if('text' !== sort && !this.complexItems) {
return; // need a valid sort key
}
const key = true === sort ? 'text' : sort;
if('text' !== sort && !this.complexItems) {
return; // need a valid sort key
}
this.items.sort( (a, b) => {
const a1 = a[key];
const b1 = b[key];
if(!a1) {
return -1;
}
if(!b1) {
return 1;
}
return a1.localeCompare( b1, { sensitivity : false, numeric : true } );
});
this.items.sort( (a, b) => {
const a1 = a[key];
const b1 = b[key];
if(!a1) {
return -1;
}
if(!b1) {
return 1;
}
return a1.localeCompare( b1, { sensitivity : false, numeric : true } );
});
this.sorted = true;
this.sorted = true;
};
MenuView.prototype.removeItem = function(index) {
this.sorted = false;
this.items.splice(index, 1);
this.sorted = false;
this.items.splice(index, 1);
if(this.focusItems) {
this.focusItems.splice(index, 1);
}
if(this.focusItems) {
this.focusItems.splice(index, 1);
}
if(this.focusedItemIndex >= index) {
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0);
}
if(this.focusedItemIndex >= index) {
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0);
}
this.positionCacheExpired = true;
this.positionCacheExpired = true;
};
MenuView.prototype.getCount = function() {
return this.items.length;
return this.items.length;
};
MenuView.prototype.getItems = function() {
if(this.complexItems) {
return this.items;
}
if(this.complexItems) {
return this.items;
}
return this.items.map( item => {
return item.text;
});
return this.items.map( item => {
return item.text;
});
};
MenuView.prototype.getItem = function(index) {
if(this.complexItems) {
return this.items[index];
}
if(this.complexItems) {
return this.items[index];
}
return this.items[index].text;
return this.items[index].text;
};
MenuView.prototype.focusNext = function() {
this.emitIndexUpdate();
this.emitIndexUpdate();
};
MenuView.prototype.focusPrevious = function() {
this.emitIndexUpdate();
this.emitIndexUpdate();
};
MenuView.prototype.focusNextPageItem = function() {
this.emitIndexUpdate();
this.emitIndexUpdate();
};
MenuView.prototype.focusPreviousPageItem = function() {
this.emitIndexUpdate();
this.emitIndexUpdate();
};
MenuView.prototype.focusFirst = function() {
this.emitIndexUpdate();
this.emitIndexUpdate();
};
MenuView.prototype.focusLast = function() {
this.emitIndexUpdate();
this.emitIndexUpdate();
};
MenuView.prototype.setFocusItemIndex = function(index) {
this.focusedItemIndex = index;
this.focusedItemIndex = index;
};
MenuView.prototype.onKeyPress = function(ch, key) {
const itemIndex = this.getHotKeyItemIndex(ch);
if(itemIndex >= 0) {
this.setFocusItemIndex(itemIndex);
const itemIndex = this.getHotKeyItemIndex(ch);
if(itemIndex >= 0) {
this.setFocusItemIndex(itemIndex);
if(true === this.hotKeySubmit) {
this.emit('action', 'accept');
}
}
if(true === this.hotKeySubmit) {
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) {
const self = this;
const self = this;
if(items) {
this.focusItems = [];
items.forEach( itemText => {
this.focusItems.push(
{
text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client)
}
);
});
}
if(items) {
this.focusItems = [];
items.forEach( itemText => {
this.focusItems.push(
{
text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client)
}
);
});
}
};
MenuView.prototype.setItemSpacing = function(itemSpacing) {
itemSpacing = parseInt(itemSpacing);
assert(_.isNumber(itemSpacing));
itemSpacing = parseInt(itemSpacing);
assert(_.isNumber(itemSpacing));
this.itemSpacing = itemSpacing;
this.positionCacheExpired = true;
this.itemSpacing = itemSpacing;
this.positionCacheExpired = true;
};
MenuView.prototype.setPropertyValue = function(propName, value) {
switch(propName) {
case 'itemSpacing' : this.setItemSpacing(value); break;
case 'items' : this.setItems(value); break;
case 'focusItems' : this.setFocusItems(value); break;
case 'hotKeys' : this.setHotKeys(value); break;
case 'hotKeySubmit' : this.hotKeySubmit = value; break;
case 'justify' : this.justify = value; break;
case 'focusItemIndex' : this.focusedItemIndex = value; break;
switch(propName) {
case 'itemSpacing' : this.setItemSpacing(value); break;
case 'items' : this.setItems(value); break;
case 'focusItems' : this.setFocusItems(value); break;
case 'hotKeys' : this.setHotKeys(value); break;
case 'hotKeySubmit' : this.hotKeySubmit = value; break;
case 'justify' : this.justify = value; break;
case 'focusItemIndex' : this.focusedItemIndex = value; break;
case 'itemFormat' :
case 'focusItemFormat' :
this[propName] = value;
break;
case 'itemFormat' :
case 'focusItemFormat' :
this[propName] = value;
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) {
if(_.isObject(hotKeys)) {
if(this.caseInsensitiveHotKeys) {
this.hotKeys = {};
for(var key in hotKeys) {
this.hotKeys[key.toLowerCase()] = hotKeys[key];
}
} else {
this.hotKeys = hotKeys;
}
}
if(_.isObject(hotKeys)) {
if(this.caseInsensitiveHotKeys) {
this.hotKeys = {};
for(var key in hotKeys) {
this.hotKeys[key.toLowerCase()] = hotKeys[key];
}
} else {
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½
const MenuModule = require('./menu_module.js').MenuModule;
const {
getSortedAvailMessageConferences,
getAvailableMessageAreasByConfTag,
getSortedAvailMessageAreasByConfTag,
getSortedAvailMessageConferences,
getAvailableMessageAreasByConfTag,
getSortedAvailMessageAreasByConfTag,
} = require('./message_area.js');
const Errors = require('./enig_error.js').Errors;
const Message = require('./message.js');
@ -15,134 +15,134 @@ const Message = require('./message.js');
const _ = require('lodash');
exports.moduleInfo = {
name : 'Message Base Search',
desc : 'Module for quickly searching the message base',
author : 'NuSkooler',
name : 'Message Base Search',
desc : 'Module for quickly searching the message base',
author : 'NuSkooler',
};
const MciViewIds = {
search : {
searchTerms : 1,
search : 2,
conf : 3,
area : 4,
to : 5,
from : 6,
advSearch : 7,
}
search : {
searchTerms : 1,
search : 2,
conf : 3,
area : 4,
to : 5,
from : 6,
advSearch : 7,
}
};
exports.getModule = class MessageBaseSearch extends MenuModule {
constructor(options) {
super(options);
constructor(options) {
super(options);
this.menuMethods = {
search : (formData, extraArgs, cb) => {
return this.searchNow(formData, cb);
}
};
}
this.menuMethods = {
search : (formData, extraArgs, cb) => {
return this.searchNow(formData, cb);
}
};
}
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
this.prepViewController('search', 0, mciData.menu, (err, vc) => {
if(err) {
return cb(err);
}
this.prepViewController('search', 0, mciData.menu, (err, vc) => {
if(err) {
return cb(err);
}
const confView = vc.getView(MciViewIds.search.conf);
const areaView = vc.getView(MciViewIds.search.area);
const confView = vc.getView(MciViewIds.search.conf);
const areaView = vc.getView(MciViewIds.search.area);
if(!confView || !areaView) {
return cb(Errors.DoesNotExist('Missing one or more required views'));
}
if(!confView || !areaView) {
return cb(Errors.DoesNotExist('Missing one or more required views'));
}
const availConfs = [ { text : '-ALL-', data : '' } ].concat(
getSortedAvailMessageConferences(this.client).map(conf => Object.assign(conf, { text : conf.conf.name, data : conf.confTag } )) || []
);
const availConfs = [ { text : '-ALL-', data : '' } ].concat(
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);
areaView.setItems(availAreas);
confView.setItems(availConfs);
areaView.setItems(availAreas);
confView.setFocusItemIndex(0);
areaView.setFocusItemIndex(0);
confView.setFocusItemIndex(0);
areaView.setFocusItemIndex(0);
confView.on('index update', idx => {
availAreas = [ { text : '-ALL-', data : '' } ].concat(
getSortedAvailMessageAreasByConfTag(availConfs[idx].confTag, { client : this.client }).map(
area => Object.assign(area, { text : area.area.name, data : area.areaTag } )
)
);
areaView.setItems(availAreas);
areaView.setFocusItemIndex(0);
});
confView.on('index update', idx => {
availAreas = [ { text : '-ALL-', data : '' } ].concat(
getSortedAvailMessageAreasByConfTag(availConfs[idx].confTag, { client : this.client }).map(
area => Object.assign(area, { text : area.area.name, data : area.areaTag } )
)
);
areaView.setItems(availAreas);
areaView.setFocusItemIndex(0);
});
vc.switchFocus(MciViewIds.search.searchTerms);
return cb(null);
});
});
}
vc.switchFocus(MciViewIds.search.searchTerms);
return cb(null);
});
});
}
searchNow(formData, cb) {
const isAdvanced = formData.submitId === MciViewIds.search.advSearch;
const value = formData.value;
searchNow(formData, cb) {
const isAdvanced = formData.submitId === MciViewIds.search.advSearch;
const value = formData.value;
const filter = {
resultType : 'messageList',
sort : 'modTimestamp',
terms : value.searchTerms,
//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
};
const filter = {
resultType : 'messageList',
sort : 'modTimestamp',
terms : value.searchTerms,
//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
};
if(isAdvanced) {
filter.toUserName = value.toUserName;
filter.fromUserName = value.fromUserName;
if(isAdvanced) {
filter.toUserName = value.toUserName;
filter.fromUserName = value.fromUserName;
if(value.confTag && !value.areaTag) {
// areaTag may be a string or array of strings
// getAvailableMessageAreasByConfTag() returns a obj - we only need tags
filter.areaTag = _.map(
getAvailableMessageAreasByConfTag(value.confTag, { client : this.client } ),
(area, areaTag) => areaTag
);
} else if(value.areaTag) {
filter.areaTag = value.areaTag; // specific conf + area
}
}
if(value.confTag && !value.areaTag) {
// areaTag may be a string or array of strings
// getAvailableMessageAreasByConfTag() returns a obj - we only need tags
filter.areaTag = _.map(
getAvailableMessageAreasByConfTag(value.confTag, { client : this.client } ),
(area, areaTag) => areaTag
);
} else if(value.areaTag) {
filter.areaTag = value.areaTag; // specific conf + area
}
}
Message.findMessages(filter, (err, messageList) => {
if(err) {
return cb(err);
}
Message.findMessages(filter, (err, messageList) => {
if(err) {
return cb(err);
}
if(0 === messageList.length) {
return this.gotoMenu(
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
{ menuFlags : [ 'popParent' ] },
cb
);
}
if(0 === messageList.length) {
return this.gotoMenu(
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
{ menuFlags : [ 'popParent' ] },
cb
);
}
const menuOpts = {
extraArgs : {
messageList,
noUpdateLastReadId : true
},
menuFlags : [ 'popParent' ],
};
const menuOpts = {
extraArgs : {
messageList,
noUpdateLastReadId : true
},
menuFlags : [ 'popParent' ],
};
return this.gotoMenu(
this.menuConfig.config.messageListMenu || 'messageAreaMessageList',
menuOpts,
cb
);
});
}
return this.gotoMenu(
this.menuConfig.config.messageListMenu || 'messageAreaMessageList',
menuOpts,
cb
);
});
}
};

View File

@ -10,33 +10,33 @@ exports.startup = startup;
exports.resolveMimeType = resolveMimeType;
function startup(cb) {
//
// Add in types (not yet) supported by mime-db -- and therefor, mime-types
//
const ADDITIONAL_EXT_MIMETYPES = {
ans : 'text/x-ansi',
gz : 'application/gzip', // not in mime-types 2.1.15 :(
lzx : 'application/x-lzx', // :TODO: submit to mime-types
};
//
// Add in types (not yet) supported by mime-db -- and therefor, mime-types
//
const ADDITIONAL_EXT_MIMETYPES = {
ans : 'text/x-ansi',
gz : 'application/gzip', // not in mime-types 2.1.15 :(
lzx : 'application/x-lzx', // :TODO: submit to mime-types
};
_.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => {
// don't override any entries
if(!_.isString(mimeTypes.types[ext])) {
mimeTypes[ext] = mimeType;
}
_.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => {
// don't override any entries
if(!_.isString(mimeTypes.types[ext])) {
mimeTypes[ext] = mimeType;
}
if(!mimeTypes.extensions[mimeType]) {
mimeTypes.extensions[mimeType] = [ ext ];
}
});
if(!mimeTypes.extensions[mimeType]) {
mimeTypes.extensions[mimeType] = [ ext ];
}
});
return cb(null);
return cb(null);
}
function resolveMimeType(query) {
if(mimeTypes.extensions[query]) {
return query; // alreaed a mime-type
}
if(mimeTypes.extensions[query]) {
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;
function isProduction() {
var env = process.env.NODE_ENV || 'dev';
return 'production' === env;
var env = process.env.NODE_ENV || 'dev';
return 'production' === env;
}
function isDevelopment() {
return (!(isProduction()));
return (!(isProduction()));
}
function valueWithDefault(val, defVal) {
return (typeof val !== 'undefined' ? val : defVal);
return (typeof val !== 'undefined' ? val : defVal);
}
function resolvePath(path) {
if(path.substr(0, 2) === '~/') {
var mswCombined = process.env.HOMEDRIVE + process.env.HOMEPATH;
path = (process.env.HOME || mswCombined || process.env.HOMEPATH || process.env.HOMEDIR || process.cwd()) + path.substr(1);
}
return paths.resolve(path);
if(path.substr(0, 2) === '~/') {
var mswCombined = process.env.HOMEDRIVE + process.env.HOMEPATH;
path = (process.env.HOME || mswCombined || process.env.HOMEPATH || process.env.HOMEDIR || process.cwd()) + path.substr(1);
}
return paths.resolve(path);
}
function getCleanEnigmaVersion() {
return packageJson.version
.replace(/-/g, '.')
.replace(/alpha/,'a')
.replace(/beta/,'b')
;
return packageJson.version
.replace(/-/g, '.')
.replace(/alpha/,'a')
.replace(/beta/,'b')
;
}
// See also ftn_util.js getTearLine() & getProductIdentifier()
function getEnigmaUserAgent() {
// can't have 1/2 or ½ in User-Agent according to RFC 1945 :(
const version = getCleanEnigmaVersion();
const nodeVer = process.version.substr(1); // remove 'v' prefix
// can't have 1/2 or ½ in User-Agent according to RFC 1945 :(
const version = getCleanEnigmaVersion();
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 {
tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) {
messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag);
if(!messageAreaTag) {
return; // nothing to do!
}
tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) {
messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag);
if(!messageAreaTag) {
return; // nothing to do!
}
if(recordPrevious) {
this.prevMessageConfAndArea = {
confTag : this.client.user.properties.message_conf_tag,
areaTag : this.client.user.properties.message_area_tag,
};
}
if(recordPrevious) {
this.prevMessageConfAndArea = {
confTag : this.client.user.properties.message_conf_tag,
areaTag : this.client.user.properties.message_area_tag,
};
}
if(!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) {
this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch');
}
}
if(!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) {
this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch');
}
}
tempMessageConfAndAreaRestore() {
if(this.prevMessageConfAndArea) {
this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag;
this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag;
}
}
tempMessageConfAndAreaRestore() {
if(this.prevMessageConfAndArea) {
this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag;
this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag;
}
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,83 +16,83 @@ exports.getAreaAndStorage = getAreaAndStorage;
exports.looksLikePattern = looksLikePattern;
const exitCodes = exports.ExitCodes = {
SUCCESS : 0,
ERROR : -1,
BAD_COMMAND : -2,
BAD_ARGS : -3,
SUCCESS : 0,
ERROR : -1,
BAD_COMMAND : -2,
BAD_ARGS : -3,
};
const argv = exports.argv = require('minimist')(process.argv.slice(2), {
alias : {
h : 'help',
v : 'version',
c : 'config',
n : 'no-prompt',
}
alias : {
h : 'help',
v : 'version',
c : 'config',
n : 'no-prompt',
}
});
function printUsageAndSetExitCode(errMsg, exitCode) {
if(_.isUndefined(exitCode)) {
exitCode = exitCodes.ERROR;
}
if(_.isUndefined(exitCode)) {
exitCode = exitCodes.ERROR;
}
process.exitCode = exitCode;
process.exitCode = exitCode;
if(errMsg) {
console.error(errMsg);
}
if(errMsg) {
console.error(errMsg);
}
}
function getDefaultConfigPath() {
return './config/';
return './config/';
}
function getConfigPath() {
const baseConfigPath = argv.config ? argv.config : config.getDefaultPath();
return baseConfigPath + 'config.hjson';
const baseConfigPath = argv.config ? argv.config : config.getDefaultPath();
return baseConfigPath + 'config.hjson';
}
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) {
async.series(
[
function init(callback) {
initConfig(callback);
},
function initDb(callback) {
db.initializeDatabases(callback);
},
],
err => {
return cb(err);
}
);
async.series(
[
function init(callback) {
initConfig(callback);
},
function initDb(callback) {
db.initializeDatabases(callback);
},
],
err => {
return cb(err);
}
);
}
function getAreaAndStorage(tags) {
return tags.map(tag => {
const parts = tag.toString().split('@');
const entry = {
areaTag : parts[0],
};
entry.pattern = entry.areaTag; // handy
if(parts[1]) {
entry.storageTag = parts[1];
}
return entry;
});
return tags.map(tag => {
const parts = tag.toString().split('@');
const entry = {
areaTag : parts[0],
};
entry.pattern = entry.areaTag; // handy
if(parts[1]) {
entry.storageTag = parts[1];
}
return entry;
});
}
function looksLikePattern(tag) {
// globs can start with @
if(tag.indexOf('@') > 0) {
return false;
}
// globs can start with @
if(tag.indexOf('@') > 0) {
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;
const usageHelp = exports.USAGE_HELP = {
General :
General :
`usage: optutil.js [--version] [--help]
<command> [<args>]
@ -21,7 +21,7 @@ commands:
fb file base management
mb message base management
`,
User :
User :
`usage: optutil.js user <action> [<args>]
actions:
@ -33,7 +33,7 @@ actions:
group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP
`,
Config :
Config :
`usage: optutil.js config <action> [<args>]
actions:
@ -46,7 +46,7 @@ import-areas args:
--uplinks UL1,UL2,... specify one or more comma separated uplinks
--type TYPE specifies area import type. valid options are "bbs" and "na"
`,
FileBase :
FileBase :
`usage: oputil.js fb <action> [<args>]
actions:
@ -80,7 +80,7 @@ info args:
remove args:
--phys-file also remove underlying physical file
`,
FileOpsInfo :
FileOpsInfo :
`
general information:
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
FILE_ID a file identifier. see file.sqlite3
`,
MessageBase :
MessageBase :
`usage: oputil.js mb <action> [<args>]
actions:
@ -101,5 +101,5 @@ general information:
};
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() {
process.exitCode = ExitCodes.SUCCESS;
process.exitCode = ExitCodes.SUCCESS;
if(true === argv.version) {
return console.info(require('../package.json').version);
}
if(true === argv.version) {
return console.info(require('../package.json').version);
}
if(0 === argv._.length ||
if(0 === argv._.length ||
'help' === argv._[0])
{
return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.SUCCESS);
}
{
return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.SUCCESS);
}
switch(argv._[0]) {
case 'user' : return handleUserCommand();
case 'config' : return handleConfigCommand();
case 'fb' : return handleFileBaseCommand();
case 'mb' : return handleMessageBaseCommand();
default : return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.BAD_COMMAND);
}
switch(argv._[0]) {
case 'user' : return handleUserCommand();
case 'config' : return handleConfigCommand();
case 'fb' : return handleFileBaseCommand();
case 'mb' : return handleMessageBaseCommand();
default : return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.BAD_COMMAND);
}
};

View File

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

View File

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

View File

@ -21,230 +21,230 @@ exports.getPredefinedMCIValue = getPredefinedMCIValue;
exports.init = init;
function init(cb) {
setNextRandomRumor(cb);
setNextRandomRumor(cb);
}
function setNextRandomRumor(cb) {
StatLog.getSystemLogEntries('system_rumorz', StatLog.Order.Random, 1, (err, entry) => {
if(entry) {
entry = entry[0];
}
const randRumor = entry && entry.log_value ? entry.log_value : '';
StatLog.setNonPeristentSystemStat('random_rumor', randRumor);
if(cb) {
return cb(null);
}
});
StatLog.getSystemLogEntries('system_rumorz', StatLog.Order.Random, 1, (err, entry) => {
if(entry) {
entry = entry[0];
}
const randRumor = entry && entry.log_value ? entry.log_value : '';
StatLog.setNonPeristentSystemStat('random_rumor', randRumor);
if(cb) {
return cb(null);
}
});
}
function getUserRatio(client, propA, propB) {
const a = StatLog.getUserStatNum(client.user, propA);
const b = StatLog.getUserStatNum(client.user, propB);
const ratio = ~~((a / b) * 100);
return `${ratio}%`;
const a = StatLog.getUserStatNum(client.user, propA);
const b = StatLog.getUserStatNum(client.user, propB);
const ratio = ~~((a / b) * 100);
return `${ratio}%`;
}
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) {
return (StatLog.getSystemStat(statName) || defaultValue).toLocaleString();
return (StatLog.getSystemStat(statName) || defaultValue).toLocaleString();
}
const PREDEFINED_MCI_GENERATORS = {
//
// Board
//
BN : function boardName() { return Config().general.boardName; },
//
// Board
//
BN : function boardName() { return Config().general.boardName; },
// ENiGMA
VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; },
VN : function version() { return packageJson.version; },
// ENiGMA
VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; },
VN : function version() { return packageJson.version; },
// +op info
SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); },
SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); },
SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); },
SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); },
SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); },
SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); },
// :TODO: op age, web, ?????
// +op info
SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); },
SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); },
SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); },
SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); },
SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); },
SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); },
// :TODO: op age, web, ?????
//
// Current user / session
//
UN : function userName(client) { return client.user.username; },
UI : function userId(client) { return client.user.userId.toString(); },
UG : function groups(client) { return _.values(client.user.groups).join(', '); },
UR : function realName(client) { return userStatAsString(client, 'real_name', ''); },
LO : function location(client) { return userStatAsString(client, 'location', ''); },
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
US : function sex(client) { return userStatAsString(client, 'sex', ''); },
UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); },
UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); },
UF : function affils(client) { return userStatAsString(client, 'affiliation', ''); },
UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); },
UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); },
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
ST : function serverName(client) { return client.session.serverName; },
FN : function activeFileBaseFilterName(client) {
const activeFilter = FileBaseFilters.getActiveFilter(client);
return activeFilter ? activeFilter.name : '';
},
DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2
DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes
const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr
},
UP : function userNumUploads(client) { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2
UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes
const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr
},
NR : function userUpDownRatio(client) { // Obv/2
return getUserRatio(client, 'ul_total_count', 'dl_total_count');
},
KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio
return getUserRatio(client, 'ul_total_bytes', 'dl_total_bytes');
},
//
// Current user / session
//
UN : function userName(client) { return client.user.username; },
UI : function userId(client) { return client.user.userId.toString(); },
UG : function groups(client) { return _.values(client.user.groups).join(', '); },
UR : function realName(client) { return userStatAsString(client, 'real_name', ''); },
LO : function location(client) { return userStatAsString(client, 'location', ''); },
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
US : function sex(client) { return userStatAsString(client, 'sex', ''); },
UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); },
UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); },
UF : function affils(client) { return userStatAsString(client, 'affiliation', ''); },
UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); },
UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); },
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
ST : function serverName(client) { return client.session.serverName; },
FN : function activeFileBaseFilterName(client) {
const activeFilter = FileBaseFilters.getActiveFilter(client);
return activeFilter ? activeFilter.name : '';
},
DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2
DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes
const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr
},
UP : function userNumUploads(client) { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2
UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes
const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr
},
NR : function userUpDownRatio(client) { // Obv/2
return getUserRatio(client, 'ul_total_count', 'dl_total_count');
},
KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio
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()); },
PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); },
PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); },
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); },
PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); },
MD : function currentMenuDescription(client) {
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
},
MD : function currentMenuDescription(client) {
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
},
MA : function messageAreaName(client) {
const area = getMessageAreaByTag(client.user.properties.message_area_tag);
return area ? area.name : '';
},
MC : function messageConfName(client) {
const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag);
return conf ? conf.name : '';
},
ML : function messageAreaDescription(client) {
const area = getMessageAreaByTag(client.user.properties.message_area_tag);
return area ? area.desc : '';
},
CM : function messageConfDescription(client) {
const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag);
return conf ? conf.desc : '';
},
MA : function messageAreaName(client) {
const area = getMessageAreaByTag(client.user.properties.message_area_tag);
return area ? area.name : '';
},
MC : function messageConfName(client) {
const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag);
return conf ? conf.name : '';
},
ML : function messageAreaDescription(client) {
const area = getMessageAreaByTag(client.user.properties.message_area_tag);
return area ? area.desc : '';
},
CM : function messageConfDescription(client) {
const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag);
return conf ? conf.desc : '';
},
SH : function termHeight(client) { return client.term.termHeight.toString(); },
SW : function termWidth(client) { return client.term.termWidth.toString(); },
SH : function termHeight(client) { return client.term.termHeight.toString(); },
SW : function termWidth(client) { return client.term.termWidth.toString(); },
//
// Date/Time
//
// :TODO: change to CD for 'Current Date'
DT : function date(client) { return moment().format(client.currentTheme.helpers.getDateFormat()); },
CT : function time(client) { return moment().format(client.currentTheme.helpers.getTimeFormat()) ;},
//
// Date/Time
//
// :TODO: change to CD for 'Current Date'
DT : function date(client) { return moment().format(client.currentTheme.helpers.getDateFormat()); },
CT : function time(client) { return moment().format(client.currentTheme.helpers.getTimeFormat()) ;},
//
// OS/System Info
//
OS : function operatingSystem() {
return {
linux : 'Linux',
darwin : 'Mac OS X',
win32 : 'Windows',
sunos : 'SunOS',
freebsd : 'FreeBSD',
}[os.platform()] || os.type();
},
//
// OS/System Info
//
OS : function operatingSystem() {
return {
linux : 'Linux',
darwin : 'Mac OS X',
win32 : 'Windows',
sunos : 'SunOS',
freebsd : 'FreeBSD',
}[os.platform()] || os.type();
},
OA : function systemArchitecture() { return os.arch(); },
OA : function systemArchitecture() { return os.arch(); },
SC : function systemCpuModel() {
//
// Clean up CPU strings a bit for better display
//
return os.cpus()[0].model
.replace(/\(R\)|\(TM\)|processor|CPU/g, '')
.replace(/\s+(?= )/g, '');
},
SC : function systemCpuModel() {
//
// Clean up CPU strings a bit for better display
//
return os.cpus()[0].model
.replace(/\(R\)|\(TM\)|processor|CPU/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
NV : function nodeVersion() { return process.version; },
// :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; },
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() {
// start the process of picking another random one
setNextRandomRumor();
RR : function randomRumor() {
// start the process of picking another random one
setNextRandomRumor();
return StatLog.getSystemStat('random_rumor');
},
return StatLog.getSystemStat('random_rumor');
},
//
// System File Base, Up/Download Info
//
// :TODO: DD - Today's # of downloads (iNiQUiTY)
//
SD : function systemNumDownloads() { return sysStatAsString('dl_total_count', 0); },
SO : function systemByteDownload() {
const byteSize = StatLog.getSystemStatNum('dl_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr
},
SU : function systemNumUploads() { return sysStatAsString('ul_total_count', 0); },
SP : function systemByteUpload() {
const byteSize = StatLog.getSystemStatNum('ul_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr
},
TF : function totalFilesOnSystem() {
const areaStats = StatLog.getSystemStat('file_base_area_stats');
return _.get(areaStats, 'totalFiles', 0).toLocaleString();
},
TB : function totalBytesOnSystem() {
const areaStats = StatLog.getSystemStat('file_base_area_stats');
const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0));
return formatByteSize(totalBytes, true); // true=withAbbr
},
//
// System File Base, Up/Download Info
//
// :TODO: DD - Today's # of downloads (iNiQUiTY)
//
SD : function systemNumDownloads() { return sysStatAsString('dl_total_count', 0); },
SO : function systemByteDownload() {
const byteSize = StatLog.getSystemStatNum('dl_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr
},
SU : function systemNumUploads() { return sysStatAsString('ul_total_count', 0); },
SP : function systemByteUpload() {
const byteSize = StatLog.getSystemStatNum('ul_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr
},
TF : function totalFilesOnSystem() {
const areaStats = StatLog.getSystemStat('file_base_area_stats');
return _.get(areaStats, 'totalFiles', 0).toLocaleString();
},
TB : function totalBytesOnSystem() {
const areaStats = StatLog.getSystemStat('file_base_area_stats');
const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0));
return formatByteSize(totalBytes, true); // true=withAbbr
},
// :TODO: PT - Messages posted *today* (Obv/2)
// -> Include FTN/etc.
// :TODO: NT - New users today (Obv/2)
// :TODO: CT - Calls *today* (Obv/2)
// :TODO: FT - Files uploaded/added *today* (Obv/2)
// :TODO: DD - Files downloaded *today* (iNiQUiTY)
// :TODO: TP - total message/posts on the system (Obv/2)
// -> Include FTN/etc.
// :TODO: LC - name of last caller to system (Obv/2)
// :TODO: TZ - Average *system* post/call ratio (iNiQUiTY)
// :TODO: PT - Messages posted *today* (Obv/2)
// -> Include FTN/etc.
// :TODO: NT - New users today (Obv/2)
// :TODO: CT - Calls *today* (Obv/2)
// :TODO: FT - Files uploaded/added *today* (Obv/2)
// :TODO: DD - Files downloaded *today* (iNiQUiTY)
// :TODO: TP - total message/posts on the system (Obv/2)
// -> Include FTN/etc.
// :TODO: LC - name of last caller to system (Obv/2)
// :TODO: TZ - Average *system* post/call ratio (iNiQUiTY)
//
// Special handling for XY
//
XY : function xyHack() { return; /* nothing */ },
//
// Special handling for XY
//
XY : function xyHack() { return; /* nothing */ },
};
function getPredefinedMCIValue(client, code) {
if(!client || !code) {
return;
}
if(!client || !code) {
return;
}
const generator = PREDEFINED_MCI_GENERATORS[code];
const generator = PREDEFINED_MCI_GENERATORS[code];
if(generator) {
let value;
try {
value = generator(client);
} catch(e) {
Log.error( { code : code, exception : e.message }, 'Exception caught generating predefined MCI value' );
}
if(generator) {
let value;
try {
value = generator(client);
} catch(e) {
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');
exports.moduleInfo = {
name : 'Rumorz',
desc : 'Standard local rumorz',
author : 'NuSkooler',
packageName : 'codes.l33t.enigma.rumorz',
name : 'Rumorz',
desc : 'Standard local rumorz',
author : 'NuSkooler',
packageName : 'codes.l33t.enigma.rumorz',
};
const STATLOG_KEY_RUMORZ = 'system_rumorz';
const FormIds = {
View : 0,
Add : 1,
View : 0,
Add : 1,
};
const MciCodeIds = {
ViewForm : {
Entries : 1,
AddPrompt : 2,
},
AddForm : {
NewEntry : 1,
EntryPreview : 2,
AddPrompt : 3,
}
ViewForm : {
Entries : 1,
AddPrompt : 2,
},
AddForm : {
NewEntry : 1,
EntryPreview : 2,
AddPrompt : 3,
}
};
exports.getModule = class RumorzModule extends MenuModule {
constructor(options) {
super(options);
constructor(options) {
super(options);
this.menuMethods = {
viewAddScreen : (formData, extraArgs, cb) => {
return this.displayAddScreen(cb);
},
this.menuMethods = {
viewAddScreen : (formData, extraArgs, cb) => {
return this.displayAddScreen(cb);
},
addEntry : (formData, extraArgs, cb) => {
if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) {
const rumor = formData.value.rumor.trim(); // remove any trailing ws
addEntry : (formData, extraArgs, cb) => {
if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) {
const rumor = formData.value.rumor.trim(); // remove any trailing ws
StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => {
this.clearAddForm();
return this.displayViewScreen(true, cb); // true=cls
});
} else {
// empty message - treat as if cancel was hit
return this.displayViewScreen(true, cb); // true=cls
}
},
StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => {
this.clearAddForm();
return this.displayViewScreen(true, cb); // true=cls
});
} else {
// empty message - treat as if cancel was hit
return this.displayViewScreen(true, cb); // true=cls
}
},
cancelAdd : (formData, extraArgs, cb) => {
this.clearAddForm();
return this.displayViewScreen(true, cb); // true=cls
}
};
}
cancelAdd : (formData, extraArgs, cb) => {
this.clearAddForm();
return this.displayViewScreen(true, cb); // true=cls
}
};
}
get config() { return this.menuConfig.config; }
get config() { return this.menuConfig.config; }
clearAddForm() {
const newEntryView = this.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
clearAddForm() {
const newEntryView = this.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
newEntryView.setText('');
newEntryView.setText('');
// preview is optional
if(previewView) {
previewView.setText('');
}
}
// preview is optional
if(previewView) {
previewView.setText('');
}
}
initSequence() {
const self = this;
initSequence() {
const self = this;
async.series(
[
function beforeDisplayArt(callback) {
self.beforeArt(callback);
},
function display(callback) {
self.displayViewScreen(false, callback);
}
],
err => {
if(err) {
// :TODO: Handle me -- initSequence() should really take a completion callback
}
self.finishedLoading();
}
);
}
async.series(
[
function beforeDisplayArt(callback) {
self.beforeArt(callback);
},
function display(callback) {
self.displayViewScreen(false, callback);
}
],
err => {
if(err) {
// :TODO: Handle me -- initSequence() should really take a completion callback
}
self.finishedLoading();
}
);
}
displayViewScreen(clearScreen, cb) {
const self = this;
async.waterfall(
[
function clearAndDisplayArt(callback) {
if(self.viewControllers.add) {
self.viewControllers.add.setFocus(false);
}
displayViewScreen(clearScreen, cb) {
const self = this;
async.waterfall(
[
function clearAndDisplayArt(callback) {
if(self.viewControllers.add) {
self.viewControllers.add.setFocus(false);
}
if(clearScreen) {
self.client.term.rawWrite(resetScreen());
}
if(clearScreen) {
self.client.term.rawWrite(resetScreen());
}
theme.displayThemedAsset(
self.config.art.entries,
self.client,
{ font : self.menuConfig.font, trailingLF : false },
(err, artData) => {
return callback(err, artData);
}
);
},
function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController(
'view',
new ViewController( { client : self.client, formId : FormIds.View } )
);
theme.displayThemedAsset(
self.config.art.entries,
self.client,
{ font : self.menuConfig.font, trailingLF : false },
(err, artData) => {
return callback(err, artData);
}
);
},
function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController(
'view',
new ViewController( { client : self.client, formId : FormIds.View } )
);
const loadOpts = {
callingMenu : self,
mciMap : artData.mciMap,
formId : FormIds.View,
};
const loadOpts = {
callingMenu : self,
mciMap : artData.mciMap,
formId : FormIds.View,
};
return vc.loadFromMenuConfig(loadOpts, callback);
} else {
self.viewControllers.view.setFocus(true);
self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw();
return callback(null);
}
},
function fetchEntries(callback) {
const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries);
return vc.loadFromMenuConfig(loadOpts, callback);
} else {
self.viewControllers.view.setFocus(true);
self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw();
return callback(null);
}
},
function fetchEntries(callback) {
const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries);
StatLog.getSystemLogEntries(STATLOG_KEY_RUMORZ, StatLog.Order.Timestamp, (err, entries) => {
return callback(err, entriesView, entries);
});
},
function populateEntries(entriesView, entries, callback) {
const config = self.config;
const listFormat = config.listFormat || '{rumor}';
const focusListFormat = config.focusListFormat || listFormat;
StatLog.getSystemLogEntries(STATLOG_KEY_RUMORZ, StatLog.Order.Timestamp, (err, entries) => {
return callback(err, entriesView, entries);
});
},
function populateEntries(entriesView, entries, callback) {
const config = self.config;
const listFormat = config.listFormat || '{rumor}';
const focusListFormat = config.focusListFormat || listFormat;
entriesView.setItems(entries.map( e => stringFormat(listFormat, { rumor : e.log_value } ) ) );
entriesView.setFocusItems(entries.map(e => stringFormat(focusListFormat, { rumor : e.log_value } ) ) );
entriesView.redraw();
entriesView.setItems(entries.map( e => stringFormat(listFormat, { rumor : e.log_value } ) ) );
entriesView.setFocusItems(entries.map(e => stringFormat(focusListFormat, { rumor : e.log_value } ) ) );
entriesView.redraw();
return callback(null);
},
function finalPrep(callback) {
const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt);
promptView.setFocusItemIndex(1); // default to NO
return callback(null);
}
],
err => {
if(cb) {
return cb(err);
}
}
);
}
return callback(null);
},
function finalPrep(callback) {
const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt);
promptView.setFocusItemIndex(1); // default to NO
return callback(null);
}
],
err => {
if(cb) {
return cb(err);
}
}
);
}
displayAddScreen(cb) {
const self = this;
displayAddScreen(cb) {
const self = this;
async.waterfall(
[
function clearAndDisplayArt(callback) {
self.viewControllers.view.setFocus(false);
self.client.term.rawWrite(resetScreen());
async.waterfall(
[
function clearAndDisplayArt(callback) {
self.viewControllers.view.setFocus(false);
self.client.term.rawWrite(resetScreen());
theme.displayThemedAsset(
self.config.art.add,
self.client,
{ font : self.menuConfig.font },
(err, artData) => {
return callback(err, artData);
}
);
},
function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController(
'add',
new ViewController( { client : self.client, formId : FormIds.Add } )
);
theme.displayThemedAsset(
self.config.art.add,
self.client,
{ font : self.menuConfig.font },
(err, artData) => {
return callback(err, artData);
}
);
},
function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController(
'add',
new ViewController( { client : self.client, formId : FormIds.Add } )
);
const loadOpts = {
callingMenu : self,
mciMap : artData.mciMap,
formId : FormIds.Add,
};
const loadOpts = {
callingMenu : self,
mciMap : artData.mciMap,
formId : FormIds.Add,
};
return vc.loadFromMenuConfig(loadOpts, callback);
} else {
self.viewControllers.add.setFocus(true);
self.viewControllers.add.redrawAll();
self.viewControllers.add.switchFocus(MciCodeIds.AddForm.NewEntry);
return callback(null);
}
},
function initPreviewUpdates(callback) {
const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
if(previewView) {
let timerId;
entryView.on('key press', () => {
clearTimeout(timerId);
timerId = setTimeout( () => {
const focused = self.viewControllers.add.getFocusedView();
if(focused === entryView) {
previewView.setText(entryView.getData());
focused.setFocus(true);
}
}, 500);
});
}
return callback(null);
}
],
err => {
if(cb) {
return cb(err);
}
}
);
}
return vc.loadFromMenuConfig(loadOpts, callback);
} else {
self.viewControllers.add.setFocus(true);
self.viewControllers.add.redrawAll();
self.viewControllers.add.switchFocus(MciCodeIds.AddForm.NewEntry);
return callback(null);
}
},
function initPreviewUpdates(callback) {
const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
if(previewView) {
let timerId;
entryView.on('key press', () => {
clearTimeout(timerId);
timerId = setTimeout( () => {
const focused = self.viewControllers.add.getFocusedView();
if(focused === entryView) {
previewView.setText(entryView.getData());
focused.setFocus(true);
}
}, 500);
});
}
return callback(null);
}
],
err => {
if(cb) {
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 ];
function readSAUCE(data, cb) {
if(data.length < SAUCE_SIZE) {
return cb(Errors.DoesNotExist('No SAUCE record present'));
}
if(data.length < SAUCE_SIZE) {
return cb(Errors.DoesNotExist('No SAUCE record present'));
}
let sauceRec;
try {
sauceRec = new Parser()
.buffer('id', { length : 5 } )
.buffer('version', { length : 2 } )
.buffer('title', { length: 35 } )
.buffer('author', { length : 20 } )
.buffer('group', { length: 20 } )
.buffer('date', { length: 8 } )
.uint32le('fileSize')
.int8('dataType')
.int8('fileType')
.uint16le('tinfo1')
.uint16le('tinfo2')
.uint16le('tinfo3')
.uint16le('tinfo4')
.int8('numComments')
.int8('flags')
// :TODO: does this need to be optional?
.buffer('tinfos', { length: 22 } ) // SAUCE 00.5
.parse(data.slice(data.length - SAUCE_SIZE));
} catch(e) {
return cb(Errors.Invalid('Invalid SAUCE record'));
}
let sauceRec;
try {
sauceRec = new Parser()
.buffer('id', { length : 5 } )
.buffer('version', { length : 2 } )
.buffer('title', { length: 35 } )
.buffer('author', { length : 20 } )
.buffer('group', { length: 20 } )
.buffer('date', { length: 8 } )
.uint32le('fileSize')
.int8('dataType')
.int8('fileType')
.uint16le('tinfo1')
.uint16le('tinfo2')
.uint16le('tinfo3')
.uint16le('tinfo4')
.int8('numComments')
.int8('flags')
// :TODO: does this need to be optional?
.buffer('tinfos', { length: 22 } ) // SAUCE 00.5
.parse(data.slice(data.length - SAUCE_SIZE));
} catch(e) {
return cb(Errors.Invalid('Invalid SAUCE record'));
}
if(!SAUCE_ID.equals(sauceRec.id)) {
return cb(Errors.DoesNotExist('No SAUCE record present'));
}
if(!SAUCE_ID.equals(sauceRec.id)) {
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) {
return cb(Errors.Invalid(`Unsupported SAUCE version: ${ver}`));
}
if('00' !== ver) {
return cb(Errors.Invalid(`Unsupported SAUCE version: ${ver}`));
}
if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(sauceRec.dataType)) {
return cb(Errors.Invalid(`Unsupported SAUCE DataType: ${sauceRec.dataType}`));
}
if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(sauceRec.dataType)) {
return cb(Errors.Invalid(`Unsupported SAUCE DataType: ${sauceRec.dataType}`));
}
const sauce = {
id : iconv.decode(sauceRec.id, 'cp437'),
version : iconv.decode(sauceRec.version, 'cp437').trim(),
title : iconv.decode(sauceRec.title, 'cp437').trim(),
author : iconv.decode(sauceRec.author, 'cp437').trim(),
group : iconv.decode(sauceRec.group, 'cp437').trim(),
date : iconv.decode(sauceRec.date, 'cp437').trim(),
fileSize : sauceRec.fileSize,
dataType : sauceRec.dataType,
fileType : sauceRec.fileType,
tinfo1 : sauceRec.tinfo1,
tinfo2 : sauceRec.tinfo2,
tinfo3 : sauceRec.tinfo3,
tinfo4 : sauceRec.tinfo4,
numComments : sauceRec.numComments,
flags : sauceRec.flags,
tinfos : sauceRec.tinfos,
};
const sauce = {
id : iconv.decode(sauceRec.id, 'cp437'),
version : iconv.decode(sauceRec.version, 'cp437').trim(),
title : iconv.decode(sauceRec.title, 'cp437').trim(),
author : iconv.decode(sauceRec.author, 'cp437').trim(),
group : iconv.decode(sauceRec.group, 'cp437').trim(),
date : iconv.decode(sauceRec.date, 'cp437').trim(),
fileSize : sauceRec.fileSize,
dataType : sauceRec.dataType,
fileType : sauceRec.fileType,
tinfo1 : sauceRec.tinfo1,
tinfo2 : sauceRec.tinfo2,
tinfo3 : sauceRec.tinfo3,
tinfo4 : sauceRec.tinfo4,
numComments : sauceRec.numComments,
flags : sauceRec.flags,
tinfos : sauceRec.tinfos,
};
const dt = SAUCE_DATA_TYPES[sauce.dataType];
if(dt && dt.parser) {
sauce[dt.name] = dt.parser(sauce);
}
const dt = SAUCE_DATA_TYPES[sauce.dataType];
if(dt && dt.parser) {
sauce[dt.name] = dt.parser(sauce);
}
return cb(null, sauce);
return cb(null, sauce);
}
// :TODO: These need completed:
const SAUCE_DATA_TYPES = {
0 : { name : 'None' },
1 : { name : 'Character', parser : parseCharacterSAUCE },
2 : 'Bitmap',
3 : 'Vector',
4 : 'Audio',
5 : 'BinaryText',
6 : 'XBin',
7 : 'Archive',
8 : 'Executable',
0 : { name : 'None' },
1 : { name : 'Character', parser : parseCharacterSAUCE },
2 : 'Bitmap',
3 : 'Vector',
4 : 'Audio',
5 : 'BinaryText',
6 : 'XBin',
7 : 'Archive',
8 : 'Executable',
};
const SAUCE_CHARACTER_FILE_TYPES = {
0 : 'ASCII',
1 : 'ANSi',
2 : 'ANSiMation',
3 : 'RIP script',
4 : 'PCBoard',
5 : 'Avatar',
6 : 'HTML',
7 : 'Source',
8 : 'TundraDraw',
0 : 'ASCII',
1 : 'ANSi',
2 : 'ANSiMation',
3 : 'RIP script',
4 : 'PCBoard',
5 : 'Avatar',
6 : 'HTML',
7 : 'Source',
8 : 'TundraDraw',
};
//
@ -129,53 +129,53 @@ const SAUCE_CHARACTER_FILE_TYPES = {
// Note that this is the same mapping that x84 uses. Be compatible!
//
const SAUCE_FONT_TO_ENCODING_HINT = {
'Amiga MicroKnight' : 'amiga',
'Amiga MicroKnight+' : 'amiga',
'Amiga mOsOul' : 'amiga',
'Amiga P0T-NOoDLE' : 'amiga',
'Amiga Topaz 1' : 'amiga',
'Amiga Topaz 1+' : 'amiga',
'Amiga Topaz 2' : 'amiga',
'Amiga Topaz 2+' : 'amiga',
'Atari ATASCII' : 'atari',
'IBM EGA43' : 'cp437',
'IBM EGA' : 'cp437',
'IBM VGA25G' : 'cp437',
'IBM VGA50' : 'cp437',
'IBM VGA' : 'cp437',
'Amiga MicroKnight' : 'amiga',
'Amiga MicroKnight+' : 'amiga',
'Amiga mOsOul' : 'amiga',
'Amiga P0T-NOoDLE' : 'amiga',
'Amiga Topaz 1' : 'amiga',
'Amiga Topaz 1+' : 'amiga',
'Amiga Topaz 2' : 'amiga',
'Amiga Topaz 2+' : 'amiga',
'Atari ATASCII' : 'atari',
'IBM EGA43' : 'cp437',
'IBM EGA' : 'cp437',
'IBM VGA25G' : 'cp437',
'IBM VGA50' : 'cp437',
'IBM VGA' : 'cp437',
};
[
'437', '720', '737', '775', '819', '850', '852', '855', '857', '858',
'860', '861', '862', '863', '864', '865', '866', '869', '872'
'437', '720', '737', '775', '819', '850', '852', '855', '857', '858',
'860', '861', '862', '863', '864', '865', '866', '869', '872'
].forEach( page => {
const codec = 'cp' + page;
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + 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 VGA50 ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec;
const codec = 'cp' + page;
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + 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 VGA50 ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec;
});
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) {
// convience: create ansiFlags
sauce.ansiFlags = sauce.flags;
if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) {
// convience: create ansiFlags
sauce.ansiFlags = sauce.flags;
let i = 0;
while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) {
++i;
}
let i = 0;
while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) {
++i;
}
const fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437');
if(fontName.length > 0) {
result.fontName = fontName;
}
}
const fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437');
if(fontName.length > 0) {
result.fontName = fontName;
}
}
return result;
return result;
}

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