Pardon the noise. More tab to space conversion!

This commit is contained in:
Bryan Ashby 2018-06-22 21:26:46 -06:00
parent c3635bb26b
commit 1d8be6b014
128 changed files with 8017 additions and 8017 deletions

View File

@ -1,62 +1,62 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const DropFile = require('./dropfile.js').DropFile; const DropFile = require('./dropfile.js').DropFile;
const door = require('./door.js'); const door = require('./door.js');
const theme = require('./theme.js'); const theme = require('./theme.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const async = require('async'); const async = require('async');
const assert = require('assert'); const assert = require('assert');
const paths = require('path'); const paths = require('path');
const _ = require('lodash'); const _ = require('lodash');
const mkdirs = require('fs-extra').mkdirs; const mkdirs = require('fs-extra').mkdirs;
// :TODO: This should really be a system module... needs a little work to allow for such // :TODO: This should really be a system module... needs a little work to allow for such
const activeDoorNodeInstances = {}; const activeDoorNodeInstances = {};
exports.moduleInfo = { exports.moduleInfo = {
name : 'Abracadabra', name : 'Abracadabra',
desc : 'External BBS Door Module', desc : 'External BBS Door Module',
author : 'NuSkooler', author : 'NuSkooler',
}; };
/* /*
Example configuration for LORD under DOSEMU: Example configuration for LORD under DOSEMU:
{ {
config: { config: {
name: PimpWars name: PimpWars
dropFileType: DORINFO dropFileType: DORINFO
cmd: qemu-system-i386 cmd: qemu-system-i386
args: [ args: [
"-localtime", "-localtime",
"freedos.img", "freedos.img",
"-chardev", "-chardev",
"socket,port={srvPort},nowait,host=localhost,id=s0", "socket,port={srvPort},nowait,host=localhost,id=s0",
"-device", "-device",
"isa-serial,chardev=s0" "isa-serial,chardev=s0"
] ]
io: socket io: socket
} }
} }
listen: socket | stdio listen: socket | stdio
{ {
"config" : { "config" : {
"name" : "LORD", "name" : "LORD",
"dropFileType" : "DOOR", "dropFileType" : "DOOR",
"cmd" : "/usr/bin/dosemu", "cmd" : "/usr/bin/dosemu",
"args" : [ "-quiet", "-f", "/etc/dosemu/dosemu.conf", "X:\\PW\\START.BAT {dropfile} {node}" ] ], "args" : [ "-quiet", "-f", "/etc/dosemu/dosemu.conf", "X:\\PW\\START.BAT {dropfile} {node}" ] ],
"nodeMax" : 32, "nodeMax" : 32,
"tooManyArt" : "toomany-lord.ans" "tooManyArt" : "toomany-lord.ans"
} }
} }
:TODO: See Mystic & others for other arg options that we may need to support :TODO: See Mystic & others for other arg options that we may need to support
*/ */
exports.getModule = class AbracadabraModule extends MenuModule { exports.getModule = class AbracadabraModule extends MenuModule {
@ -64,21 +64,21 @@ exports.getModule = class AbracadabraModule extends MenuModule {
super(options); super(options);
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... } // :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
assert(_.isString(this.config.name, 'Config \'name\' is required')); assert(_.isString(this.config.name, 'Config \'name\' is required'));
assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required')); assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required'));
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required')); assert(_.isString(this.config.cmd, 'Config \'cmd\' is required'));
this.config.nodeMax = this.config.nodeMax || 0; this.config.nodeMax = this.config.nodeMax || 0;
this.config.args = this.config.args || []; this.config.args = this.config.args || [];
} }
/* /*
:TODO: :TODO:
* disconnecting wile door is open leaves dosemu * disconnecting wile door is open leaves dosemu
* http://bbslink.net/sysop.php support * http://bbslink.net/sysop.php support
* Font support ala all other menus... or does this just work? * Font support ala all other menus... or does this just work?
*/ */
initSequence() { initSequence() {
const self = this; const self = this;
@ -87,12 +87,12 @@ exports.getModule = class AbracadabraModule extends MenuModule {
[ [
function validateNodeCount(callback) { function validateNodeCount(callback) {
if(self.config.nodeMax > 0 && if(self.config.nodeMax > 0 &&
_.isNumber(activeDoorNodeInstances[self.config.name]) && _.isNumber(activeDoorNodeInstances[self.config.name]) &&
activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax) activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax)
{ {
self.client.log.info( self.client.log.info(
{ {
name : self.config.name, name : self.config.name,
activeCount : activeDoorNodeInstances[self.config.name] activeCount : activeDoorNodeInstances[self.config.name]
}, },
'Too many active instances'); 'Too many active instances');
@ -106,13 +106,13 @@ exports.getModule = class AbracadabraModule extends MenuModule {
} else { } else {
self.client.term.write('\nToo many active instances. Try again later.\n'); self.client.term.write('\nToo many active instances. Try again later.\n');
// :TODO: Use MenuModule.pausePrompt() // :TODO: Use MenuModule.pausePrompt()
self.pausePrompt( () => { self.pausePrompt( () => {
callback(new Error('Too many active instances')); callback(new Error('Too many active instances'));
}); });
} }
} else { } else {
// :TODO: JS elegant way to do this? // :TODO: JS elegant way to do this?
if(activeDoorNodeInstances[self.config.name]) { if(activeDoorNodeInstances[self.config.name]) {
activeDoorNodeInstances[self.config.name] += 1; activeDoorNodeInstances[self.config.name] += 1;
} else { } else {
@ -123,8 +123,8 @@ exports.getModule = class AbracadabraModule extends MenuModule {
} }
}, },
function generateDropfile(callback) { function generateDropfile(callback) {
self.dropFile = new DropFile(self.client, self.config.dropFileType); self.dropFile = new DropFile(self.client, self.config.dropFileType);
var fullPath = self.dropFile.fullPath; var fullPath = self.dropFile.fullPath;
mkdirs(paths.dirname(fullPath), function dirCreated(err) { mkdirs(paths.dirname(fullPath), function dirCreated(err) {
if(err) { if(err) {
@ -152,28 +152,28 @@ exports.getModule = class AbracadabraModule extends MenuModule {
runDoor() { runDoor() {
const exeInfo = { const exeInfo = {
cmd : this.config.cmd, cmd : this.config.cmd,
args : this.config.args, args : this.config.args,
io : this.config.io || 'stdio', io : this.config.io || 'stdio',
encoding : this.config.encoding || this.client.term.outputEncoding, encoding : this.config.encoding || this.client.term.outputEncoding,
dropFile : this.dropFile.fileName, dropFile : this.dropFile.fileName,
node : this.client.node, node : this.client.node,
//inhSocket : this.client.output._handle.fd, //inhSocket : this.client.output._handle.fd,
}; };
const doorInstance = new door.Door(this.client, exeInfo); const doorInstance = new door.Door(this.client, exeInfo);
doorInstance.once('finished', () => { doorInstance.once('finished', () => {
// //
// Try to clean up various settings such as scroll regions that may // Try to clean up various settings such as scroll regions that may
// have been set within the door // have been set within the door
// //
this.client.term.rawWrite( this.client.term.rawWrite(
ansi.normal() + ansi.normal() +
ansi.goto(this.client.term.termHeight, this.client.term.termWidth) + ansi.goto(this.client.term.termHeight, this.client.term.termWidth) +
ansi.setScrollRegion() + ansi.setScrollRegion() +
ansi.goto(this.client.term.termHeight, 0) + ansi.goto(this.client.term.termHeight, 0) +
'\r\n\r\n' '\r\n\r\n'
); );
this.prevMenu(); this.prevMenu();

View File

@ -1,13 +1,13 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const checkAcs = require('./acs_parser.js').parse; const checkAcs = require('./acs_parser.js').parse;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
// deps // deps
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
class ACS { class ACS {
constructor(client) { constructor(client) {
@ -26,7 +26,7 @@ class ACS {
} }
// //
// Message Conferences & Areas // Message Conferences & Areas
// //
hasMessageConfRead(conf) { hasMessageConfRead(conf) {
return this.check(conf.acs, 'read', ACS.Defaults.MessageConfRead); return this.check(conf.acs, 'read', ACS.Defaults.MessageConfRead);
@ -37,7 +37,7 @@ class ACS {
} }
// //
// File Base / Areas // File Base / Areas
// //
hasFileAreaRead(area) { hasFileAreaRead(area) {
return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead); return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead);
@ -53,7 +53,7 @@ class ACS {
getConditionalValue(condArray, memberName) { getConditionalValue(condArray, memberName) {
if(!Array.isArray(condArray)) { if(!Array.isArray(condArray)) {
// no cond array, just use the value // no cond array, just use the value
return condArray; return condArray;
} }
@ -68,7 +68,7 @@ class ACS {
return false; return false;
} }
} else { } else {
return true; // no acs check req. return true; // no acs check req.
} }
}); });
@ -79,12 +79,12 @@ class ACS {
} }
ACS.Defaults = { ACS.Defaults = {
MessageAreaRead : 'GM[users]', MessageAreaRead : 'GM[users]',
MessageConfRead : 'GM[users]', MessageConfRead : 'GM[users]',
FileAreaRead : 'GM[users]', FileAreaRead : 'GM[users]',
FileAreaWrite : 'GM[sysops]', FileAreaWrite : 'GM[sysops]',
FileAreaDownload : 'GM[users]', FileAreaDownload : 'GM[users]',
}; };
module.exports = ACS; module.exports = ACS;

View File

@ -1,16 +1,16 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
// deps // deps
const events = require('events'); const events = require('events');
const util = require('util'); const util = require('util');
const _ = require('lodash'); const _ = require('lodash');
exports.ANSIEscapeParser = ANSIEscapeParser; exports.ANSIEscapeParser = ANSIEscapeParser;
const CR = 0x0d; const CR = 0x0d;
const LF = 0x0a; const LF = 0x0a;
@ -20,76 +20,76 @@ function ANSIEscapeParser(options) {
events.EventEmitter.call(this); events.EventEmitter.call(this);
this.column = 1; this.column = 1;
this.row = 1; this.row = 1;
this.scrollBack = 0; this.scrollBack = 0;
this.graphicRendition = {}; this.graphicRendition = {};
this.parseState = { this.parseState = {
re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
}; };
options = miscUtil.valueWithDefault(options, { options = miscUtil.valueWithDefault(options, {
mciReplaceChar : '', mciReplaceChar : '',
termHeight : 25, termHeight : 25,
termWidth : 80, termWidth : 80,
trailingLF : 'default', // default|omit|no|yes, ... trailingLF : 'default', // default|omit|no|yes, ...
}); });
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, ''); this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25); this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80); this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default'); this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
self.moveCursor = function(cols, rows) { self.moveCursor = function(cols, rows) {
self.column += cols; self.column += cols;
self.row += rows; self.row += rows;
self.column = Math.max(self.column, 1); self.column = Math.max(self.column, 1);
self.column = Math.min(self.column, self.termWidth); // can't move past term width self.column = Math.min(self.column, self.termWidth); // can't move past term width
self.row = Math.max(self.row, 1); self.row = Math.max(self.row, 1);
self.positionUpdated(); self.positionUpdated();
}; };
self.saveCursorPosition = function() { self.saveCursorPosition = function() {
self.savedPosition = { self.savedPosition = {
row : self.row, row : self.row,
column : self.column column : self.column
}; };
}; };
self.restoreCursorPosition = function() { self.restoreCursorPosition = function() {
self.row = self.savedPosition.row; self.row = self.savedPosition.row;
self.column = self.savedPosition.column; self.column = self.savedPosition.column;
delete self.savedPosition; delete self.savedPosition;
self.positionUpdated(); self.positionUpdated();
// self.rowUpdated(); // self.rowUpdated();
}; };
self.clearScreen = function() { self.clearScreen = function() {
// :TODO: should be doing something with row/column? // :TODO: should be doing something with row/column?
self.emit('clear screen'); self.emit('clear screen');
}; };
/* /*
self.rowUpdated = function() { self.rowUpdated = function() {
self.emit('row update', self.row + self.scrollBack); self.emit('row update', self.row + self.scrollBack);
};*/ };*/
self.positionUpdated = function() { self.positionUpdated = function() {
self.emit('position update', self.row, self.column); self.emit('position update', self.row, self.column);
}; };
function literal(text) { function literal(text) {
const len = text.length; const len = text.length;
let pos = 0; let pos = 0;
let start = 0; let start = 0;
let charCode; let charCode;
while(pos < len) { while(pos < len) {
charCode = text.charCodeAt(pos) & 0xff; // 8bit clean charCode = text.charCodeAt(pos) & 0xff; // 8bit clean
switch(charCode) { switch(charCode) {
case CR : case CR :
@ -116,7 +116,7 @@ function ANSIEscapeParser(options) {
start = pos + 1; start = pos + 1;
self.column = 1; self.column = 1;
self.row += 1; self.row += 1;
self.positionUpdated(); self.positionUpdated();
} else { } else {
@ -129,11 +129,11 @@ function ANSIEscapeParser(options) {
} }
// //
// Finalize this chunk // Finalize this chunk
// //
if(self.column > self.termWidth) { if(self.column > self.termWidth) {
self.column = 1; self.column = 1;
self.row += 1; self.row += 1;
self.positionUpdated(); self.positionUpdated();
} }
@ -145,7 +145,7 @@ function ANSIEscapeParser(options) {
} }
function parseMCI(buffer) { function parseMCI(buffer) {
// :TODO: move this to "constants" seciton @ top // :TODO: move this to "constants" seciton @ top
var mciRe = /%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g; var mciRe = /%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g;
var pos = 0; var pos = 0;
var match; var match;
@ -154,16 +154,16 @@ function ANSIEscapeParser(options) {
var id; var id;
do { do {
pos = mciRe.lastIndex; pos = mciRe.lastIndex;
match = mciRe.exec(buffer); match = mciRe.exec(buffer);
if(null !== match) { if(null !== match) {
if(match.index > pos) { if(match.index > pos) {
literal(buffer.slice(pos, match.index)); literal(buffer.slice(pos, match.index));
} }
mciCode = match[1]; mciCode = match[1];
id = match[2] || null; id = match[2] || null;
if(match[3]) { if(match[3]) {
args = match[3].split(','); args = match[3].split(',');
@ -171,7 +171,7 @@ function ANSIEscapeParser(options) {
args = []; args = [];
} }
// if MCI codes are changing, save off the current color // if MCI codes are changing, save off the current color
var fullMciCode = mciCode + (id || ''); var fullMciCode = mciCode + (id || '');
if(self.lastMciCode !== fullMciCode) { if(self.lastMciCode !== fullMciCode) {
@ -182,10 +182,10 @@ function ANSIEscapeParser(options) {
self.emit('mci', { self.emit('mci', {
mci : mciCode, mci : mciCode,
id : id ? parseInt(id, 10) : null, id : id ? parseInt(id, 10) : null,
args : args, args : args,
SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true)
}); });
if(self.mciReplaceChar.length > 0) { if(self.mciReplaceChar.length > 0) {
@ -208,10 +208,10 @@ function ANSIEscapeParser(options) {
self.reset = function(input) { self.reset = function(input) {
self.parseState = { self.parseState = {
// ignore anything past EOF marker, if any // ignore anything past EOF marker, if any
buffer : input.split(String.fromCharCode(0x1a), 1)[0], buffer : input.split(String.fromCharCode(0x1a), 1)[0],
re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
stop : false, stop : false,
}; };
}; };
@ -224,13 +224,13 @@ function ANSIEscapeParser(options) {
self.reset(input); self.reset(input);
} }
// :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc.
var pos; var pos;
var match; var match;
var opCode; var opCode;
var args; var args;
var re = self.parseState.re; var re = self.parseState.re;
var buffer = self.parseState.buffer; var buffer = self.parseState.buffer;
self.parseState.stop = false; self.parseState.stop = false;
@ -239,16 +239,16 @@ function ANSIEscapeParser(options) {
return; return;
} }
pos = re.lastIndex; pos = re.lastIndex;
match = re.exec(buffer); match = re.exec(buffer);
if(null !== match) { if(null !== match) {
if(match.index > pos) { if(match.index > pos) {
parseMCI(buffer.slice(pos, match.index)); parseMCI(buffer.slice(pos, match.index));
} }
opCode = match[2]; opCode = match[2];
args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints
escape(opCode, args); escape(opCode, args);
@ -260,13 +260,13 @@ function ANSIEscapeParser(options) {
if(pos < buffer.length) { if(pos < buffer.length) {
var lastBit = buffer.slice(pos); var lastBit = buffer.slice(pos);
// :TODO: check for various ending LF's, not just DOS \r\n // :TODO: check for various ending LF's, not just DOS \r\n
if('\r\n' === lastBit.slice(-2).toString()) { if('\r\n' === lastBit.slice(-2).toString()) {
switch(self.trailingLF) { switch(self.trailingLF) {
case 'default' : case 'default' :
// //
// Default is to *not* omit the trailing LF // Default is to *not* omit the trailing LF
// if we're going to end on termHeight // if we're going to end on termHeight
// //
if(this.termHeight === self.row) { if(this.termHeight === self.row) {
lastBit = lastBit.slice(0, -2); lastBit = lastBit.slice(0, -2);
@ -288,100 +288,100 @@ function ANSIEscapeParser(options) {
}; };
/* /*
self.parse = function(buffer, savedRe) { self.parse = function(buffer, savedRe) {
// :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc.
// :TODO: move this to "constants" section @ top // :TODO: move this to "constants" section @ top
var re = /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g; var re = /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g;
var pos = 0; var pos = 0;
var match; var match;
var opCode; var opCode;
var args; var args;
// ignore anything past EOF marker, if any // ignore anything past EOF marker, if any
buffer = buffer.split(String.fromCharCode(0x1a), 1)[0]; buffer = buffer.split(String.fromCharCode(0x1a), 1)[0];
do { do {
pos = re.lastIndex; pos = re.lastIndex;
match = re.exec(buffer); match = re.exec(buffer);
if(null !== match) { if(null !== match) {
if(match.index > pos) { if(match.index > pos) {
parseMCI(buffer.slice(pos, match.index)); parseMCI(buffer.slice(pos, match.index));
} }
opCode = match[2]; opCode = match[2];
args = getArgArray(match[1].split(';')); args = getArgArray(match[1].split(';'));
escape(opCode, args); escape(opCode, args);
self.emit('chunk', match[0]); self.emit('chunk', match[0]);
} }
} while(0 !== re.lastIndex); } while(0 !== re.lastIndex);
if(pos < buffer.length) { if(pos < buffer.length) {
parseMCI(buffer.slice(pos)); parseMCI(buffer.slice(pos));
} }
self.emit('complete'); self.emit('complete');
}; };
*/ */
function escape(opCode, args) { function escape(opCode, args) {
let arg; let arg;
switch(opCode) { switch(opCode) {
// cursor up // cursor up
case 'A' : case 'A' :
//arg = args[0] || 1; //arg = args[0] || 1;
arg = isNaN(args[0]) ? 1 : args[0]; arg = isNaN(args[0]) ? 1 : args[0];
self.moveCursor(0, -arg); self.moveCursor(0, -arg);
break; break;
// cursor down // cursor down
case 'B' : case 'B' :
//arg = args[0] || 1; //arg = args[0] || 1;
arg = isNaN(args[0]) ? 1 : args[0]; arg = isNaN(args[0]) ? 1 : args[0];
self.moveCursor(0, arg); self.moveCursor(0, arg);
break; break;
// cursor forward/right // cursor forward/right
case 'C' : case 'C' :
//arg = args[0] || 1; //arg = args[0] || 1;
arg = isNaN(args[0]) ? 1 : args[0]; arg = isNaN(args[0]) ? 1 : args[0];
self.moveCursor(arg, 0); self.moveCursor(arg, 0);
break; break;
// cursor back/left // cursor back/left
case 'D' : case 'D' :
//arg = args[0] || 1; //arg = args[0] || 1;
arg = isNaN(args[0]) ? 1 : args[0]; arg = isNaN(args[0]) ? 1 : args[0];
self.moveCursor(-arg, 0); self.moveCursor(-arg, 0);
break; break;
case 'f' : // horiz & vertical case 'f' : // horiz & vertical
case 'H' : // cursor position case 'H' : // cursor position
//self.row = args[0] || 1; //self.row = args[0] || 1;
//self.column = args[1] || 1; //self.column = args[1] || 1;
self.row = isNaN(args[0]) ? 1 : args[0]; self.row = isNaN(args[0]) ? 1 : args[0];
self.column = isNaN(args[1]) ? 1 : args[1]; self.column = isNaN(args[1]) ? 1 : args[1];
//self.rowUpdated(); //self.rowUpdated();
self.positionUpdated(); self.positionUpdated();
break; break;
// save position // save position
case 's' : case 's' :
self.saveCursorPosition(); self.saveCursorPosition();
break; break;
// restore position // restore position
case 'u' : case 'u' :
self.restoreCursorPosition(); self.restoreCursorPosition();
break; break;
// set graphic rendition // set graphic rendition
case 'm' : case 'm' :
self.graphicRendition.reset = false; self.graphicRendition.reset = false;
@ -395,7 +395,7 @@ function ANSIEscapeParser(options) {
} else if(ANSIEscapeParser.styles[arg]) { } else if(ANSIEscapeParser.styles[arg]) {
switch(arg) { switch(arg) {
case 0 : case 0 :
// clear out everything // clear out everything
delete self.graphicRendition.intensity; delete self.graphicRendition.intensity;
delete self.graphicRendition.underline; delete self.graphicRendition.underline;
delete self.graphicRendition.blink; delete self.graphicRendition.blink;
@ -445,13 +445,13 @@ function ANSIEscapeParser(options) {
} }
self.emit('sgr update', self.graphicRendition); self.emit('sgr update', self.graphicRendition);
break; // m break; // m
// :TODO: s, u, K // :TODO: s, u, K
// erase display/screen // erase display/screen
case 'J' : case 'J' :
// :TODO: Handle other 'J' types! // :TODO: Handle other 'J' types!
if(2 === args[0]) { if(2 === args[0]) {
self.clearScreen(); self.clearScreen();
} }
@ -463,62 +463,62 @@ function ANSIEscapeParser(options) {
util.inherits(ANSIEscapeParser, events.EventEmitter); util.inherits(ANSIEscapeParser, events.EventEmitter);
ANSIEscapeParser.foregroundColors = { ANSIEscapeParser.foregroundColors = {
30 : 'black', 30 : 'black',
31 : 'red', 31 : 'red',
32 : 'green', 32 : 'green',
33 : 'yellow', 33 : 'yellow',
34 : 'blue', 34 : 'blue',
35 : 'magenta', 35 : 'magenta',
36 : 'cyan', 36 : 'cyan',
37 : 'white', 37 : 'white',
39 : 'default', // same as white for most implementations 39 : 'default', // same as white for most implementations
90 : 'grey' 90 : 'grey'
}; };
Object.freeze(ANSIEscapeParser.foregroundColors); Object.freeze(ANSIEscapeParser.foregroundColors);
ANSIEscapeParser.backgroundColors = { ANSIEscapeParser.backgroundColors = {
40 : 'black', 40 : 'black',
41 : 'red', 41 : 'red',
42 : 'green', 42 : 'green',
43 : 'yellow', 43 : 'yellow',
44 : 'blue', 44 : 'blue',
45 : 'magenta', 45 : 'magenta',
46 : 'cyan', 46 : 'cyan',
47 : 'white', 47 : 'white',
49 : 'default', // same as black for most implementations 49 : 'default', // same as black for most implementations
}; };
Object.freeze(ANSIEscapeParser.backgroundColors); Object.freeze(ANSIEscapeParser.backgroundColors);
// :TODO: ensure these names all align with that of ansi_term.js // :TODO: ensure these names all align with that of ansi_term.js
// //
// See the following specs: // See the following specs:
// * http://www.ansi-bbs.org/ansi-bbs-core-server.html // * http://www.ansi-bbs.org/ansi-bbs-core-server.html
// * http://www.vt100.net/docs/vt510-rm/SGR // * http://www.vt100.net/docs/vt510-rm/SGR
// * https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // * https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
// //
// Note that these are intentionally not in order such that they // Note that these are intentionally not in order such that they
// can be grouped by concept here in code. // can be grouped by concept here in code.
// //
ANSIEscapeParser.styles = { ANSIEscapeParser.styles = {
0 : 'default', // Everything disabled 0 : 'default', // Everything disabled
1 : 'intensityBright', // aka bold 1 : 'intensityBright', // aka bold
2 : 'intensityDim', 2 : 'intensityDim',
22 : 'intensityNormal', 22 : 'intensityNormal',
4 : 'underlineOn', // Not supported by most BBS-like terminals 4 : 'underlineOn', // Not supported by most BBS-like terminals
24 : 'underlineOff', // Not supported by most BBS-like terminals 24 : 'underlineOff', // Not supported by most BBS-like terminals
5 : 'blinkSlow', // blinkSlow & blinkFast are generally treated the same 5 : 'blinkSlow', // blinkSlow & blinkFast are generally treated the same
6 : 'blinkFast', // blinkSlow & blinkFast are generally treated the same 6 : 'blinkFast', // blinkSlow & blinkFast are generally treated the same
25 : 'blinkOff', 25 : 'blinkOff',
7 : 'negativeImageOn', // Generally not supported or treated as "reverse FG & BG" 7 : 'negativeImageOn', // Generally not supported or treated as "reverse FG & BG"
27 : 'negativeImageOff', // Generally not supported or treated as "reverse FG & BG" 27 : 'negativeImageOff', // Generally not supported or treated as "reverse FG & BG"
8 : 'invisibleOn', // FG set to BG 8 : 'invisibleOn', // FG set to BG
28 : 'invisibleOff', // Not supported by most BBS-like terminals 28 : 'invisibleOff', // Not supported by most BBS-like terminals
}; };
Object.freeze(ANSIEscapeParser.styles); Object.freeze(ANSIEscapeParser.styles);

View File

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

View File

@ -2,191 +2,191 @@
'use strict'; 'use strict';
// //
// ANSI Terminal Support Resources // ANSI Terminal Support Resources
// //
// ANSI-BBS // ANSI-BBS
// * http://ansi-bbs.org/ // * http://ansi-bbs.org/
// //
// CTerm / SyncTERM // CTerm / SyncTERM
// * https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // * https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
// //
// BananaCom // BananaCom
// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt // * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt
// //
// ANSI.SYS // ANSI.SYS
// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/ansisys.txt // * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/ansisys.txt
// * http://academic.evergreen.edu/projects/biophysics/technotes/program/ansi_esc.htm // * http://academic.evergreen.edu/projects/biophysics/technotes/program/ansi_esc.htm
// //
// VTX // VTX
// * https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt // * https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
// //
// General // General
// * http://en.wikipedia.org/wiki/ANSI_escape_code // * http://en.wikipedia.org/wiki/ANSI_escape_code
// * http://www.inwap.com/pdp10/ansicode.txt // * http://www.inwap.com/pdp10/ansicode.txt
// //
// Other Implementations // Other Implementations
// * https://github.com/chjj/term.js/blob/master/src/term.js // * https://github.com/chjj/term.js/blob/master/src/term.js
// //
// //
// For a board, we need to support the semi-standard ANSI-BBS "spec" which // For a board, we need to support the semi-standard ANSI-BBS "spec" which
// is bastardized mix of DOS ANSI.SYS, cterm.txt, bansi.txt and a little other. // is bastardized mix of DOS ANSI.SYS, cterm.txt, bansi.txt and a little other.
// This gives us NetRunner, SyncTERM, EtherTerm, most *nix terminals, compatibilitiy // This gives us NetRunner, SyncTERM, EtherTerm, most *nix terminals, compatibilitiy
// with legit oldschool DOS terminals, and so on. // with legit oldschool DOS terminals, and so on.
// //
// ENiGMA½ // ENiGMA½
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
// deps // deps
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
exports.getFullMatchRegExp = getFullMatchRegExp; exports.getFullMatchRegExp = getFullMatchRegExp;
exports.getFGColorValue = getFGColorValue; exports.getFGColorValue = getFGColorValue;
exports.getBGColorValue = getBGColorValue; exports.getBGColorValue = getBGColorValue;
exports.sgr = sgr; exports.sgr = sgr;
exports.getSGRFromGraphicRendition = getSGRFromGraphicRendition; exports.getSGRFromGraphicRendition = getSGRFromGraphicRendition;
exports.clearScreen = clearScreen; exports.clearScreen = clearScreen;
exports.resetScreen = resetScreen; exports.resetScreen = resetScreen;
exports.normal = normal; exports.normal = normal;
exports.goHome = goHome; exports.goHome = goHome;
exports.disableVT100LineWrapping = disableVT100LineWrapping; exports.disableVT100LineWrapping = disableVT100LineWrapping;
exports.setSyncTERMFont = setSyncTERMFont; exports.setSyncTERMFont = setSyncTERMFont;
exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias; exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias;
exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias; exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias;
exports.setCursorStyle = setCursorStyle; exports.setCursorStyle = setCursorStyle;
exports.setEmulatedBaudRate = setEmulatedBaudRate; exports.setEmulatedBaudRate = setEmulatedBaudRate;
exports.vtxHyperlink = vtxHyperlink; exports.vtxHyperlink = vtxHyperlink;
// //
// See also // See also
// https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js // https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js
const ESC_CSI = '\u001b['; const ESC_CSI = '\u001b[';
const CONTROL = { const CONTROL = {
up : 'A', up : 'A',
down : 'B', down : 'B',
forward : 'C', forward : 'C',
right : 'C', right : 'C',
back : 'D', back : 'D',
left : 'D', left : 'D',
nextLine : 'E', nextLine : 'E',
prevLine : 'F', prevLine : 'F',
horizAbsolute : 'G', horizAbsolute : 'G',
// //
// CSI [ p1 ] J // CSI [ p1 ] J
// Erase in Page / Erase Data // Erase in Page / Erase Data
// Defaults: p1 = 0 // Defaults: p1 = 0
// Erases from the current screen according to the value of p1 // Erases from the current screen according to the value of p1
// 0 - Erase from the current position to the end of the screen. // 0 - Erase from the current position to the end of the screen.
// 1 - Erase from the current position to the start of the screen. // 1 - Erase from the current position to the start of the screen.
// 2 - Erase entire screen. As a violation of ECMA-048, also moves // 2 - Erase entire screen. As a violation of ECMA-048, also moves
// the cursor to position 1/1 as a number of BBS programs assume // the cursor to position 1/1 as a number of BBS programs assume
// this behaviour. // this behaviour.
// Erased characters are set to the current attribute. // Erased characters are set to the current attribute.
// //
// Support: // Support:
// * SyncTERM: Works as expected // * SyncTERM: Works as expected
// * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1 // * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1
// and screen remainder // and screen remainder
// //
eraseData : 'J', eraseData : 'J',
eraseLine : 'K', eraseLine : 'K',
insertLine : 'L', insertLine : 'L',
// //
// CSI [ p1 ] M // CSI [ p1 ] M
// Delete Line(s) / "ANSI" Music // Delete Line(s) / "ANSI" Music
// Defaults: p1 = 1 // Defaults: p1 = 1
// Deletes the current line and the p1 - 1 lines after it scrolling the // Deletes the current line and the p1 - 1 lines after it scrolling the
// first non-deleted line up to the current line and filling the newly // first non-deleted line up to the current line and filling the newly
// empty lines at the end of the screen with the current attribute. // empty lines at the end of the screen with the current attribute.
// If "ANSI" Music is fully enabled (CSI = 2 M), performs "ANSI" music // If "ANSI" Music is fully enabled (CSI = 2 M), performs "ANSI" music
// instead. // instead.
// See "ANSI" MUSIC section for more details. // See "ANSI" MUSIC section for more details.
// //
// Support: // Support:
// * SyncTERM: Works as expected // * SyncTERM: Works as expected
// * NetRunner: // * NetRunner:
// //
// General Notes: // General Notes:
// See also notes in bansi.txt and cterm.txt about the various // See also notes in bansi.txt and cterm.txt about the various
// incompatibilities & oddities around this sequence. ANSI-BBS // incompatibilities & oddities around this sequence. ANSI-BBS
// states that it *should* work with any value of p1. // states that it *should* work with any value of p1.
// //
deleteLine : 'M', deleteLine : 'M',
ansiMusic : 'M', ansiMusic : 'M',
scrollUp : 'S', scrollUp : 'S',
scrollDown : 'T', scrollDown : 'T',
setScrollRegion : 'r', setScrollRegion : 'r',
savePos : 's', savePos : 's',
restorePos : 'u', restorePos : 'u',
queryPos : '6n', queryPos : '6n',
queryScreenSize : '255n', // See bansi.txt queryScreenSize : '255n', // See bansi.txt
goto : 'H', // row Pr, column Pc -- same as f goto : 'H', // row Pr, column Pc -- same as f
gotoAlt : 'f', // same as H gotoAlt : 'f', // same as H
blinkToBrightIntensity : '?33h', blinkToBrightIntensity : '?33h',
blinkNormal : '?33l', blinkNormal : '?33l',
emulationSpeed : '*r', // Set output emulation speed. See cterm.txt emulationSpeed : '*r', // Set output emulation speed. See cterm.txt
hideCursor : '?25l', // Nonstandard - cterm.txt hideCursor : '?25l', // Nonstandard - cterm.txt
showCursor : '?25h', // Nonstandard - cterm.txt showCursor : '?25h', // Nonstandard - cterm.txt
queryDeviceAttributes : 'c', // Nonstandard - cterm.txt queryDeviceAttributes : 'c', // Nonstandard - cterm.txt
// :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes // :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes
// apparently some terms can report screen size and text area via 18t and 19t // apparently some terms can report screen size and text area via 18t and 19t
}; };
// //
// Select Graphics Rendition // Select Graphics Rendition
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt // See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
// //
const SGRValues = { const SGRValues = {
reset : 0, reset : 0,
bold : 1, bold : 1,
dim : 2, dim : 2,
blink : 5, blink : 5,
fastBlink : 6, fastBlink : 6,
negative : 7, negative : 7,
hidden : 8, hidden : 8,
normal : 22, // normal : 22, //
steady : 25, steady : 25,
positive : 27, positive : 27,
black : 30, black : 30,
red : 31, red : 31,
green : 32, green : 32,
yellow : 33, yellow : 33,
blue : 34, blue : 34,
magenta : 35, magenta : 35,
cyan : 36, cyan : 36,
white : 37, white : 37,
blackBG : 40, blackBG : 40,
redBG : 41, redBG : 41,
greenBG : 42, greenBG : 42,
yellowBG : 43, yellowBG : 43,
blueBG : 44, blueBG : 44,
magentaBG : 45, magentaBG : 45,
cyanBG : 46, cyanBG : 46,
whiteBG : 47, whiteBG : 47,
}; };
function getFullMatchRegExp(flags = 'g') { function getFullMatchRegExp(flags = 'g') {
// :TODO: expand this a bit - see strip-ansi/etc. // :TODO: expand this a bit - see strip-ansi/etc.
// :TODO: \u009b ? // :TODO: \u009b ?
return new RegExp(/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/, flags); // eslint-disable-line no-control-regex return new RegExp(/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/, flags); // eslint-disable-line no-control-regex
} }
function getFGColorValue(name) { function getFGColorValue(name) {
@ -198,20 +198,20 @@ function getBGColorValue(name) {
} }
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt // See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
// :TODO: document // :TODO: document
// :TODO: Create mappings for aliases... maybe make this a map to values instead // :TODO: Create mappings for aliases... maybe make this a map to values instead
// :TODO: Break this up in to two parts: // :TODO: Break this up in to two parts:
// 1) FONT_AND_CODE_PAGES (e.g. SyncTERM/cterm) // 1) FONT_AND_CODE_PAGES (e.g. SyncTERM/cterm)
// 2) SAUCE_FONT_MAP: Sauce name(s) -> items in FONT_AND_CODE_PAGES. // 2) SAUCE_FONT_MAP: Sauce name(s) -> items in FONT_AND_CODE_PAGES.
// ...we can then have getFontFromSAUCEName(sauceFontName) // ...we can then have getFontFromSAUCEName(sauceFontName)
// Also, create a SAUCE_ENCODING_MAP: SAUCE font name -> encodings // Also, create a SAUCE_ENCODING_MAP: SAUCE font name -> encodings
// //
// An array of CTerm/SyncTERM font/encoding values. Each entry's index // An array of CTerm/SyncTERM font/encoding values. Each entry's index
// corresponds to it's escape sequence value (e.g. cp437 = 0) // corresponds to it's escape sequence value (e.g. cp437 = 0)
// //
// See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
// //
const SYNCTERM_FONT_AND_ENCODING_TABLE = [ const SYNCTERM_FONT_AND_ENCODING_TABLE = [
'cp437', 'cp437',
@ -260,54 +260,54 @@ const SYNCTERM_FONT_AND_ENCODING_TABLE = [
]; ];
// //
// A map of various font name/aliases such as those used // A map of various font name/aliases such as those used
// in SAUCE records to SyncTERM/CTerm names // in SAUCE records to SyncTERM/CTerm names
// //
// This table contains lowercased entries with any spaces // This table contains lowercased entries with any spaces
// replaced with '_' for lookup purposes. // replaced with '_' for lookup purposes.
// //
const FONT_ALIAS_TO_SYNCTERM_MAP = { const FONT_ALIAS_TO_SYNCTERM_MAP = {
'cp437' : 'cp437', 'cp437' : 'cp437',
'ibm_vga' : 'cp437', 'ibm_vga' : 'cp437',
'ibmpc' : 'cp437', 'ibmpc' : 'cp437',
'ibm_pc' : 'cp437', 'ibm_pc' : 'cp437',
'pc' : 'cp437', 'pc' : 'cp437',
'cp437_art' : 'cp437', 'cp437_art' : 'cp437',
'ibmpcart' : 'cp437', 'ibmpcart' : 'cp437',
'ibmpc_art' : 'cp437', 'ibmpc_art' : 'cp437',
'ibm_pc_art' : 'cp437', 'ibm_pc_art' : 'cp437',
'msdos_art' : 'cp437', 'msdos_art' : 'cp437',
'msdosart' : 'cp437', 'msdosart' : 'cp437',
'pc_art' : 'cp437', 'pc_art' : 'cp437',
'pcart' : 'cp437', 'pcart' : 'cp437',
'ibm_vga50' : 'cp437', 'ibm_vga50' : 'cp437',
'ibm_vga25g' : 'cp437', 'ibm_vga25g' : 'cp437',
'ibm_ega' : 'cp437', 'ibm_ega' : 'cp437',
'ibm_ega43' : 'cp437', 'ibm_ega43' : 'cp437',
'topaz' : 'topaz', 'topaz' : 'topaz',
'amiga_topaz_1' : 'topaz', 'amiga_topaz_1' : 'topaz',
'amiga_topaz_1+' : 'topaz_plus', 'amiga_topaz_1+' : 'topaz_plus',
'topazplus' : 'topaz_plus', 'topazplus' : 'topaz_plus',
'topaz_plus' : 'topaz_plus', 'topaz_plus' : 'topaz_plus',
'amiga_topaz_2' : 'topaz', 'amiga_topaz_2' : 'topaz',
'amiga_topaz_2+' : 'topaz_plus', 'amiga_topaz_2+' : 'topaz_plus',
'topaz2plus' : 'topaz_plus', 'topaz2plus' : 'topaz_plus',
'pot_noodle' : 'pot_noodle', 'pot_noodle' : 'pot_noodle',
'p0tnoodle' : 'pot_noodle', 'p0tnoodle' : 'pot_noodle',
'amiga_p0t-noodle' : 'pot_noodle', 'amiga_p0t-noodle' : 'pot_noodle',
'mo_soul' : 'mo_soul', 'mo_soul' : 'mo_soul',
'mosoul' : 'mo_soul', 'mosoul' : 'mo_soul',
'mO\'sOul' : 'mo_soul', 'mO\'sOul' : 'mo_soul',
'amiga_microknight' : 'microknight', 'amiga_microknight' : 'microknight',
'amiga_microknight+' : 'microknight_plus', 'amiga_microknight+' : 'microknight_plus',
'atari' : 'atari', 'atari' : 'atari',
'atarist' : 'atari', 'atarist' : 'atari',
}; };
@ -334,13 +334,13 @@ function setSyncTermFontWithAlias(nameOrAlias) {
} }
const DEC_CURSOR_STYLE = { const DEC_CURSOR_STYLE = {
'blinking block' : 0, 'blinking block' : 0,
'default' : 1, 'default' : 1,
'steady block' : 2, 'steady block' : 2,
'blinking underline' : 3, 'blinking underline' : 3,
'steady underline' : 4, 'steady underline' : 4,
'blinking bar' : 5, 'blinking bar' : 5,
'steady bar' : 6, 'steady bar' : 6,
}; };
function setCursorStyle(cursorStyle) { function setCursorStyle(cursorStyle) {
@ -352,21 +352,21 @@ function setCursorStyle(cursorStyle) {
} }
// Create methods such as up(), nextLine(),... // Create methods such as up(), nextLine(),...
Object.keys(CONTROL).forEach(function onControlName(name) { Object.keys(CONTROL).forEach(function onControlName(name) {
const code = CONTROL[name]; const code = CONTROL[name];
exports[name] = function() { exports[name] = function() {
let c = code; let c = code;
if(arguments.length > 0) { if(arguments.length > 0) {
// arguments are array like -- we want an array // arguments are array like -- we want an array
c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code; c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code;
} }
return `${ESC_CSI}${c}`; return `${ESC_CSI}${c}`;
}; };
}); });
// Create various color methods such as white(), yellowBG(), reset(), ... // Create various color methods such as white(), yellowBG(), reset(), ...
Object.keys(SGRValues).forEach( name => { Object.keys(SGRValues).forEach( name => {
const code = SGRValues[name]; const code = SGRValues[name];
@ -377,16 +377,16 @@ Object.keys(SGRValues).forEach( name => {
function sgr() { function sgr() {
// //
// - Allow an single array or variable number of arguments // - Allow an single array or variable number of arguments
// - Each element can be either a integer or string found in SGRValues // - Each element can be either a integer or string found in SGRValues
// which in turn maps to a integer // which in turn maps to a integer
// //
if(arguments.length <= 0) { if(arguments.length <= 0) {
return ''; return '';
} }
let result = []; let result = [];
const args = Array.isArray(arguments[0]) ? arguments[0] : arguments; const args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
for(let i = 0; i < args.length; ++i) { for(let i = 0; i < args.length; ++i) {
const arg = args[i]; const arg = args[i];
@ -401,12 +401,12 @@ function sgr() {
} }
// //
// Converts a Graphic Rendition object used elsewhere // Converts a Graphic Rendition object used elsewhere
// to a ANSI SGR sequence. // to a ANSI SGR sequence.
// //
function getSGRFromGraphicRendition(graphicRendition, initialReset) { function getSGRFromGraphicRendition(graphicRendition, initialReset) {
let sgrSeq = []; let sgrSeq = [];
let styleCount = 0; let styleCount = 0;
[ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => { [ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => {
if(graphicRendition[s]) { if(graphicRendition[s]) {
@ -431,7 +431,7 @@ function getSGRFromGraphicRendition(graphicRendition, initialReset) {
} }
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Shortcuts for common functions // Shortcuts for common functions
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
function clearScreen() { function clearScreen() {
@ -447,20 +447,20 @@ function normal() {
} }
function goHome() { function goHome() {
return exports.goto(); // no params = home = 1,1 return exports.goto(); // no params = home = 1,1
} }
// //
// Disable auto line wraping @ termWidth // Disable auto line wraping @ termWidth
// //
// See: // See:
// http://stjarnhimlen.se/snippets/vt100.txt // http://stjarnhimlen.se/snippets/vt100.txt
// https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
// //
// WARNING: // WARNING:
// * Not honored by all clients // * Not honored by all clients
// * If it is honored, ANSI's that rely on this (e.g. do not have \r\n endings // * If it is honored, ANSI's that rely on this (e.g. do not have \r\n endings
// and use term width -- generally 80 columns -- will display garbled! // and use term width -- generally 80 columns -- will display garbled!
// //
function disableVT100LineWrapping() { function disableVT100LineWrapping() {
return `${ESC_CSI}?7l`; return `${ESC_CSI}?7l`;
@ -468,20 +468,20 @@ function disableVT100LineWrapping() {
function setEmulatedBaudRate(rate) { function setEmulatedBaudRate(rate) {
const speed = { const speed = {
unlimited : 0, unlimited : 0,
off : 0, off : 0,
0 : 0, 0 : 0,
300 : 1, 300 : 1,
600 : 2, 600 : 2,
1200 : 3, 1200 : 3,
2400 : 4, 2400 : 4,
4800 : 5, 4800 : 5,
9600 : 6, 9600 : 6,
19200 : 7, 19200 : 7,
38400 : 8, 38400 : 8,
57600 : 9, 57600 : 9,
76800 : 10, 76800 : 10,
115200 : 11, 115200 : 11,
}[rate] || 0; }[rate] || 0;
return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed); return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed);
} }

View File

@ -1,26 +1,26 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const resolveMimeType = require('./mime_util.js').resolveMimeType; const resolveMimeType = require('./mime_util.js').resolveMimeType;
// base/modules // base/modules
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const _ = require('lodash'); const _ = require('lodash');
const pty = require('node-pty'); const pty = require('node-pty');
const paths = require('path'); const paths = require('path');
let archiveUtil; let archiveUtil;
class Archiver { class Archiver {
constructor(config) { constructor(config) {
this.compress = config.compress; this.compress = config.compress;
this.decompress = config.decompress; this.decompress = config.decompress;
this.list = config.list; this.list = config.list;
this.extract = config.extract; this.extract = config.extract;
} }
ok() { ok() {
@ -37,7 +37,7 @@ class Archiver {
canCompress() { return this.can('compress'); } canCompress() { return this.can('compress'); }
canDecompress() { return this.can('decompress'); } canDecompress() { return this.can('decompress'); }
canList() { return this.can('list'); } // :TODO: validate entryMatch canList() { return this.can('list'); } // :TODO: validate entryMatch
canExtract() { return this.can('extract'); } canExtract() { return this.can('extract'); }
} }
@ -48,7 +48,7 @@ module.exports = class ArchiveUtil {
this.longestSignature = 0; this.longestSignature = 0;
} }
// singleton access // singleton access
static getInstance() { static getInstance() {
if(!archiveUtil) { if(!archiveUtil) {
archiveUtil = new ArchiveUtil(); archiveUtil = new ArchiveUtil();
@ -59,17 +59,17 @@ module.exports = class ArchiveUtil {
init() { init() {
// //
// Load configuration // Load configuration
// //
const config = Config(); const config = Config();
if(_.has(config, 'archives.archivers')) { if(_.has(config, 'archives.archivers')) {
Object.keys(config.archives.archivers).forEach(archKey => { Object.keys(config.archives.archivers).forEach(archKey => {
const archConfig = config.archives.archivers[archKey]; const archConfig = config.archives.archivers[archKey];
const archiver = new Archiver(archConfig); const archiver = new Archiver(archConfig);
if(!archiver.ok()) { if(!archiver.ok()) {
// :TODO: Log warning - bad archiver/config // :TODO: Log warning - bad archiver/config
} }
this.archivers[archKey] = archiver; this.archivers[archKey] = archiver;
@ -78,10 +78,10 @@ module.exports = class ArchiveUtil {
if(_.isObject(config.fileTypes)) { if(_.isObject(config.fileTypes)) {
const updateSig = (ft) => { const updateSig = (ft) => {
ft.sig = Buffer.from(ft.sig, 'hex'); ft.sig = Buffer.from(ft.sig, 'hex');
ft.offset = ft.offset || 0; ft.offset = ft.offset || 0;
// :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well
const sigLen = ft.offset + ft.sig.length; const sigLen = ft.offset + ft.sig.length;
if(sigLen > this.longestSignature) { if(sigLen > this.longestSignature) {
this.longestSignature = sigLen; this.longestSignature = sigLen;
@ -106,7 +106,7 @@ module.exports = class ArchiveUtil {
getArchiver(mimeTypeOrExtension, justExtention) { getArchiver(mimeTypeOrExtension, justExtention) {
const mimeType = resolveMimeType(mimeTypeOrExtension); const mimeType = resolveMimeType(mimeTypeOrExtension);
if(!mimeType) { // lookup returns false on failure if(!mimeType) { // lookup returns false on failure
return; return;
} }
@ -115,10 +115,10 @@ module.exports = class ArchiveUtil {
if(Array.isArray(fileType)) { if(Array.isArray(fileType)) {
if(!justExtention) { if(!justExtention) {
// need extention for lookup; ambiguous as-is :( // need extention for lookup; ambiguous as-is :(
return; return;
} }
// further refine by extention // further refine by extention
fileType = fileType.find(ft => justExtention === ft.ext); fileType = fileType.find(ft => justExtention === ft.ext);
} }
@ -135,11 +135,11 @@ module.exports = class ArchiveUtil {
return this.getArchiver(archType) ? true : false; return this.getArchiver(archType) ? true : false;
} }
// :TODO: implement me: // :TODO: implement me:
/* /*
detectTypeWithBuf(buf, cb) { detectTypeWithBuf(buf, cb) {
} }
*/ */
detectType(path, cb) { detectType(path, cb) {
fs.open(path, 'r', (err, fd) => { fs.open(path, 'r', (err, fd) => {
@ -177,8 +177,8 @@ module.exports = class ArchiveUtil {
} }
spawnHandler(proc, action, cb) { spawnHandler(proc, action, cb) {
// pty.js doesn't currently give us a error when things fail, // pty.js doesn't currently give us a error when things fail,
// so we have this horrible, horrible hack: // so we have this horrible, horrible hack:
let err; let err;
proc.once('data', d => { proc.once('data', d => {
if(_.isString(d) && d.startsWith('execvp(3) failed.')) { if(_.isString(d) && d.startsWith('execvp(3) failed.')) {
@ -199,8 +199,8 @@ module.exports = class ArchiveUtil {
} }
const fmtObj = { const fmtObj = {
archivePath : archivePath, archivePath : archivePath,
fileList : files.join(' '), // :TODO: probably need same hack as extractTo here! fileList : files.join(' '), // :TODO: probably need same hack as extractTo here!
}; };
const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) ); const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) );
@ -233,25 +233,25 @@ module.exports = class ArchiveUtil {
} }
const fmtObj = { const fmtObj = {
archivePath : archivePath, archivePath : archivePath,
extractPath : extractPath, extractPath : extractPath,
}; };
let action = haveFileList ? 'extract' : 'decompress'; let action = haveFileList ? 'extract' : 'decompress';
if('extract' === action && !_.isObject(archiver[action])) { if('extract' === action && !_.isObject(archiver[action])) {
// we're forced to do a full decompress // we're forced to do a full decompress
action = 'decompress'; action = 'decompress';
haveFileList = false; haveFileList = false;
} }
// we need to treat {fileList} special in that it should be broken up to 0:n args // we need to treat {fileList} special in that it should be broken up to 0:n args
const args = archiver[action].args.map( arg => { const args = archiver[action].args.map( arg => {
return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj); return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj);
}); });
const fileListPos = args.indexOf('{fileList}'); const fileListPos = args.indexOf('{fileList}');
if(fileListPos > -1) { if(fileListPos > -1) {
// replace {fileList} with 0:n sep file list arguments // replace {fileList} with 0:n sep file list arguments
args.splice.apply(args, [fileListPos, 1].concat(fileList)); args.splice.apply(args, [fileListPos, 1].concat(fileList));
} }
@ -273,10 +273,10 @@ module.exports = class ArchiveUtil {
} }
const fmtObj = { const fmtObj = {
archivePath : archivePath, archivePath : archivePath,
}; };
const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) ); const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) );
let proc; let proc;
try { try {
@ -287,7 +287,7 @@ module.exports = class ArchiveUtil {
let output = ''; let output = '';
proc.on('data', data => { proc.on('data', data => {
// :TODO: hack for: execvp(3) failed.: No such file or directory // :TODO: hack for: execvp(3) failed.: No such file or directory
output += data; output += data;
}); });
@ -304,8 +304,8 @@ module.exports = class ArchiveUtil {
let m; let m;
while((m = entryMatchRe.exec(output))) { while((m = entryMatchRe.exec(output))) {
entries.push({ entries.push({
byteSize : parseInt(m[entryGroupOrder.byteSize]), byteSize : parseInt(m[entryGroupOrder.byteSize]),
fileName : m[entryGroupOrder.fileName].trim(), fileName : m[entryGroupOrder.fileName].trim(),
}); });
} }
@ -315,15 +315,15 @@ module.exports = class ArchiveUtil {
getPtyOpts(extractPath) { getPtyOpts(extractPath) {
const opts = { const opts = {
name : 'enigma-archiver', name : 'enigma-archiver',
cols : 80, cols : 80,
rows : 24, rows : 24,
env : process.env, env : process.env,
}; };
if(extractPath) { if(extractPath) {
opts.cwd = extractPath; opts.cwd = extractPath;
} }
// :TODO: set cwd to supplied temp path if not sepcific extract // :TODO: set cwd to supplied temp path if not sepcific extract
return opts; return opts;
} }
}; };

View File

@ -1,43 +1,43 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const aep = require('./ansi_escape_parser.js'); const aep = require('./ansi_escape_parser.js');
const sauce = require('./sauce.js'); const sauce = require('./sauce.js');
// deps // deps
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
const assert = require('assert'); const assert = require('assert');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const _ = require('lodash'); const _ = require('lodash');
const xxhash = require('xxhash'); const xxhash = require('xxhash');
exports.getArt = getArt; exports.getArt = getArt;
exports.getArtFromPath = getArtFromPath; exports.getArtFromPath = getArtFromPath;
exports.display = display; exports.display = display;
exports.defaultEncodingFromExtension = defaultEncodingFromExtension; exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
// :TODO: Return MCI code information // :TODO: Return MCI code information
// :TODO: process SAUCE comments // :TODO: process SAUCE comments
// :TODO: return font + font mapped information from SAUCE // :TODO: return font + font mapped information from SAUCE
const SUPPORTED_ART_TYPES = { const SUPPORTED_ART_TYPES = {
// :TODO: the defualt encoding are really useless if they are all the same ... // :TODO: the defualt encoding are really useless if they are all the same ...
// perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf // perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf
'.ans' : { name : 'ANSI', defaultEncoding : 'cp437', eof : 0x1a }, '.ans' : { name : 'ANSI', defaultEncoding : 'cp437', eof : 0x1a },
'.asc' : { name : 'ASCII', defaultEncoding : 'cp437', eof : 0x1a }, '.asc' : { name : 'ASCII', defaultEncoding : 'cp437', eof : 0x1a },
'.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a }, '.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a },
'.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a }, '.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a },
'.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a }, '.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a },
'.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a }, '.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a },
// :TODO: extentions for wwiv, renegade, celerity, syncronet, ... // :TODO: extentions for wwiv, renegade, celerity, syncronet, ...
// :TODO: extension for atari // :TODO: extension for atari
// :TODO: extension for topaz ansi/ascii. // :TODO: extension for topaz ansi/ascii.
}; };
function getFontNameFromSAUCE(sauce) { function getFontNameFromSAUCE(sauce) {
@ -47,8 +47,8 @@ function getFontNameFromSAUCE(sauce) {
} }
function sliceAtEOF(data, eofMarker) { function sliceAtEOF(data, eofMarker) {
let eof = data.length; let eof = data.length;
const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE)
for(let i = eof - 1; i > stopPos; i--) { for(let i = eof - 1; i > stopPos; i--) {
if(eofMarker === data[i]) { if(eofMarker === data[i]) {
@ -66,12 +66,12 @@ function getArtFromPath(path, options, cb) {
} }
// //
// Convert from encodedAs -> j // Convert from encodedAs -> j
// //
const ext = paths.extname(path).toLowerCase(); const ext = paths.extname(path).toLowerCase();
const encoding = options.encodedAs || defaultEncodingFromExtension(ext); const encoding = options.encodedAs || defaultEncodingFromExtension(ext);
// :TODO: how are BOM's currently handled if present? Are they removed? Do we need to? // :TODO: how are BOM's currently handled if present? Are they removed? Do we need to?
function sliceOfData() { function sliceOfData() {
if(options.fullFile === true) { if(options.fullFile === true) {
@ -84,8 +84,8 @@ function getArtFromPath(path, options, cb) {
function getResult(sauce) { function getResult(sauce) {
const result = { const result = {
data : sliceOfData(), data : sliceOfData(),
fromPath : path, fromPath : path,
}; };
if(sauce) { if(sauce) {
@ -102,18 +102,18 @@ function getArtFromPath(path, options, cb) {
} }
// //
// If a encoding was not provided & we have a mapping from // If a encoding was not provided & we have a mapping from
// the information provided by SAUCE, use that. // the information provided by SAUCE, use that.
// //
if(!options.encodedAs) { if(!options.encodedAs) {
/* /*
if(sauce.Character && sauce.Character.fontName) { if(sauce.Character && sauce.Character.fontName) {
var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName]; var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName];
if(enc) { if(enc) {
encoding = enc; encoding = enc;
} }
} }
*/ */
} }
return cb(null, getResult(sauce)); return cb(null, getResult(sauce));
}); });
@ -126,10 +126,10 @@ function getArtFromPath(path, options, cb) {
function getArt(name, options, cb) { function getArt(name, options, cb) {
const ext = paths.extname(name); const ext = paths.extname(name);
options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art); options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art);
options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true); options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true);
// :TODO: make use of asAnsi option and convert from supported -> ansi // :TODO: make use of asAnsi option and convert from supported -> ansi
if('' !== ext) { if('' !== ext) {
options.types = [ ext.toLowerCase() ]; options.types = [ ext.toLowerCase() ];
@ -141,7 +141,7 @@ function getArt(name, options, cb) {
} }
} }
// If an extension is provided, just read the file now // If an extension is provided, just read the file now
if('' !== ext) { if('' !== ext) {
const directPath = paths.join(options.basePath, name); const directPath = paths.join(options.basePath, name);
return getArtFromPath(directPath, options, cb); return getArtFromPath(directPath, options, cb);
@ -225,10 +225,10 @@ function defaultEofFromExtension(ext) {
} }
} }
// :TODO: Implement the following // :TODO: Implement the following
// * Pause (disabled | termHeight | keyPress ) // * Pause (disabled | termHeight | keyPress )
// * Cancel (disabled | <keys> ) // * Cancel (disabled | <keys> )
// * Resume from pause -> continous (disabled | <keys>) // * Resume from pause -> continous (disabled | <keys>)
function display(client, art, options, cb) { function display(client, art, options, cb) {
if(_.isFunction(options) && !cb) { if(_.isFunction(options) && !cb) {
cb = options; cb = options;
@ -239,25 +239,25 @@ function display(client, art, options, cb) {
return cb(new Error('Empty art')); return cb(new Error('Empty art'));
} }
options.mciReplaceChar = options.mciReplaceChar || ' '; options.mciReplaceChar = options.mciReplaceChar || ' ';
options.disableMciCache = options.disableMciCache || false; options.disableMciCache = options.disableMciCache || false;
// :TODO: this is going to be broken into two approaches controlled via options: // :TODO: this is going to be broken into two approaches controlled via options:
// 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc. // 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc.
// 2) CPR driven // 2) CPR driven
if(!_.isBoolean(options.iceColors)) { if(!_.isBoolean(options.iceColors)) {
// try to detect from SAUCE // try to detect from SAUCE
if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) { if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) {
options.iceColors = true; options.iceColors = true;
} }
} }
const ansiParser = new aep.ANSIEscapeParser({ const ansiParser = new aep.ANSIEscapeParser({
mciReplaceChar : options.mciReplaceChar, mciReplaceChar : options.mciReplaceChar,
termHeight : client.term.termHeight, termHeight : client.term.termHeight,
termWidth : client.term.termWidth, termWidth : client.term.termWidth,
trailingLF : options.trailingLF, trailingLF : options.trailingLF,
}); });
let parseComplete = false; let parseComplete = false;
@ -273,12 +273,12 @@ function display(client, art, options, cb) {
} }
if(!options.disableMciCache && !mciMapFromCache) { if(!options.disableMciCache && !mciMapFromCache) {
// cache our MCI findings... // cache our MCI findings...
client.mciCache[artHash] = mciMap; client.mciCache[artHash] = mciMap;
client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Added MCI map to cache'); client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Added MCI map to cache');
} }
ansiParser.removeAllListeners(); // :TODO: Necessary??? ansiParser.removeAllListeners(); // :TODO: Necessary???
const extraInfo = { const extraInfo = {
height : ansiParser.row - 1, height : ansiParser.row - 1,
@ -288,11 +288,11 @@ function display(client, art, options, cb) {
} }
if(!options.disableMciCache) { if(!options.disableMciCache) {
artHash = xxhash.hash(Buffer.from(art), 0xCAFEBABE); artHash = xxhash.hash(Buffer.from(art), 0xCAFEBABE);
// see if we have a mciMap cached for this art // see if we have a mciMap cached for this art
if(client.mciCache) { if(client.mciCache) {
mciMap = client.mciCache[artHash]; mciMap = client.mciCache[artHash];
} }
} }
@ -300,7 +300,7 @@ function display(client, art, options, cb) {
mciMapFromCache = true; mciMapFromCache = true;
client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Loaded MCI map from cache'); client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Loaded MCI map from cache');
} else { } else {
// no cached MCI info // no cached MCI info
mciMap = {}; mciMap = {};
cprListener = function(pos) { cprListener = function(pos) {
@ -318,20 +318,20 @@ function display(client, art, options, cb) {
let generatedId = 100; let generatedId = 100;
ansiParser.on('mci', mciInfo => { ansiParser.on('mci', mciInfo => {
// :TODO: ensure generatedId's do not conflict with any existing |id| // :TODO: ensure generatedId's do not conflict with any existing |id|
const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId; const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId;
const mapKey = `${mciInfo.mci}${id}`; const mapKey = `${mciInfo.mci}${id}`;
const mapEntry = mciMap[mapKey]; const mapEntry = mciMap[mapKey];
if(mapEntry) { if(mapEntry) {
mapEntry.focusSGR = mciInfo.SGR; mapEntry.focusSGR = mciInfo.SGR;
mapEntry.focusArgs = mciInfo.args; mapEntry.focusArgs = mciInfo.args;
} else { } else {
mciMap[mapKey] = { mciMap[mapKey] = {
args : mciInfo.args, args : mciInfo.args,
SGR : mciInfo.SGR, SGR : mciInfo.SGR,
code : mciInfo.mci, code : mciInfo.mci,
id : id, id : id,
}; };
if(!mciInfo.id) { if(!mciInfo.id) {
@ -366,10 +366,10 @@ function display(client, art, options, cb) {
} }
// //
// Set SyncTERM font if we're switching only. Most terminals // Set SyncTERM font if we're switching only. Most terminals
// that support this ESC sequence can only show *one* font // that support this ESC sequence can only show *one* font
// at a time. This applies to detection only (e.g. SAUCE). // at a time. This applies to detection only (e.g. SAUCE).
// If explicit, we'll set it no matter what (above) // If explicit, we'll set it no matter what (above)
// //
if(fontName && client.term.currentSyncFont != fontName) { if(fontName && client.term.currentSyncFont != fontName) {
client.term.currentSyncFont = fontName; client.term.currentSyncFont = fontName;

View File

@ -1,21 +1,21 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
exports.parseAsset = parseAsset; exports.parseAsset = parseAsset;
exports.getAssetWithShorthand = getAssetWithShorthand; exports.getAssetWithShorthand = getAssetWithShorthand;
exports.getArtAsset = getArtAsset; exports.getArtAsset = getArtAsset;
exports.getModuleAsset = getModuleAsset; exports.getModuleAsset = getModuleAsset;
exports.resolveConfigAsset = resolveConfigAsset; exports.resolveConfigAsset = resolveConfigAsset;
exports.resolveSystemStatAsset = resolveSystemStatAsset; exports.resolveSystemStatAsset = resolveSystemStatAsset;
exports.getViewPropertyAsset = getViewPropertyAsset; exports.getViewPropertyAsset = getViewPropertyAsset;
const ALL_ASSETS = [ const ALL_ASSETS = [
'art', 'art',
@ -39,9 +39,9 @@ function parseAsset(s) {
if(m[3]) { if(m[3]) {
result.location = m[2]; result.location = m[2];
result.asset = m[3]; result.asset = m[3];
} else { } else {
result.asset = m[2]; result.asset = m[2];
} }
return result; return result;
@ -61,8 +61,8 @@ function getAssetWithShorthand(spec, defaultType) {
} }
return { return {
type : defaultType, type : defaultType,
asset : spec, asset : spec,
}; };
} }
@ -94,8 +94,8 @@ function resolveConfigAsset(spec) {
if(asset) { if(asset) {
assert('config' === asset.type); assert('config' === asset.type);
const path = asset.asset.split('.'); const path = asset.asset.split('.');
let conf = Config(); let conf = Config();
for(let i = 0; i < path.length; ++i) { for(let i = 0; i < path.length; ++i) {
if(_.isUndefined(conf[path[i]])) { if(_.isUndefined(conf[path[i]])) {
return spec; return spec;

View File

@ -1,54 +1,54 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const resetScreen = require('./ansi_term.js').resetScreen; const resetScreen = require('./ansi_term.js').resetScreen;
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const http = require('http'); const http = require('http');
const net = require('net'); const net = require('net');
const crypto = require('crypto'); const crypto = require('crypto');
const packageJson = require('../package.json'); const packageJson = require('../package.json');
/* /*
Expected configuration block: Expected configuration block:
{ {
module: bbs_link module: bbs_link
... ...
config: { config: {
sysCode: XXXXX sysCode: XXXXX
authCode: XXXXX authCode: XXXXX
schemeCode: XXXX schemeCode: XXXX
door: lord door: lord
// default hoss: games.bbslink.net // default hoss: games.bbslink.net
host: games.bbslink.net host: games.bbslink.net
// defualt port: 23 // defualt port: 23
port: 23 port: 23
} }
} }
*/ */
// :TODO: BUG: When a client disconnects, it's not handled very well -- the log is spammed with tons of errors // :TODO: BUG: When a client disconnects, it's not handled very well -- the log is spammed with tons of errors
// :TODO: ENH: Support nodeMax and tooManyArt // :TODO: ENH: Support nodeMax and tooManyArt
exports.moduleInfo = { exports.moduleInfo = {
name : 'BBSLink', name : 'BBSLink',
desc : 'BBSLink Access Module', desc : 'BBSLink Access Module',
author : 'NuSkooler', author : 'NuSkooler',
}; };
exports.getModule = class BBSLinkModule extends MenuModule { exports.getModule = class BBSLinkModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
this.config.host = this.config.host || 'games.bbslink.net'; this.config.host = this.config.host || 'games.bbslink.net';
this.config.port = this.config.port || 23; this.config.port = this.config.port || 23;
} }
initSequence() { initSequence() {
@ -61,9 +61,9 @@ exports.getModule = class BBSLinkModule extends MenuModule {
[ [
function validateConfig(callback) { function validateConfig(callback) {
if(_.isString(self.config.sysCode) && if(_.isString(self.config.sysCode) &&
_.isString(self.config.authCode) && _.isString(self.config.authCode) &&
_.isString(self.config.schemeCode) && _.isString(self.config.schemeCode) &&
_.isString(self.config.door)) _.isString(self.config.door))
{ {
callback(null); callback(null);
} else { } else {
@ -72,7 +72,7 @@ exports.getModule = class BBSLinkModule extends MenuModule {
}, },
function acquireToken(callback) { function acquireToken(callback) {
// //
// Acquire an authentication token // Acquire an authentication token
// //
crypto.randomBytes(16, function rand(ex, buf) { crypto.randomBytes(16, function rand(ex, buf) {
if(ex) { if(ex) {
@ -93,19 +93,19 @@ exports.getModule = class BBSLinkModule extends MenuModule {
}, },
function authenticateToken(callback) { function authenticateToken(callback) {
// //
// Authenticate the token we acquired previously // Authenticate the token we acquired previously
// //
var headers = { var headers = {
'X-User' : self.client.user.userId.toString(), 'X-User' : self.client.user.userId.toString(),
'X-System' : self.config.sysCode, 'X-System' : self.config.sysCode,
'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'), 'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'),
'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'), 'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'),
'X-Rows' : self.client.term.termHeight.toString(), 'X-Rows' : self.client.term.termHeight.toString(),
'X-Key' : randomKey, 'X-Key' : randomKey,
'X-Door' : self.config.door, 'X-Door' : self.config.door,
'X-Token' : token, 'X-Token' : token,
'X-Type' : 'enigma-bbs', 'X-Type' : 'enigma-bbs',
'X-Version' : packageJson.version, 'X-Version' : packageJson.version,
}; };
self.simpleHttpRequest('/auth.php?key=' + randomKey, headers, function resp(err, body) { self.simpleHttpRequest('/auth.php?key=' + randomKey, headers, function resp(err, body) {
@ -120,12 +120,12 @@ exports.getModule = class BBSLinkModule extends MenuModule {
}, },
function createTelnetBridge(callback) { function createTelnetBridge(callback) {
// //
// Authentication with BBSLink successful. Now, we need to create a telnet // Authentication with BBSLink successful. Now, we need to create a telnet
// bridge from us to them // bridge from us to them
// //
var connectOpts = { var connectOpts = {
port : self.config.port, port : self.config.port,
host : self.config.host, host : self.config.host,
}; };
var clientTerminated; var clientTerminated;
@ -151,8 +151,8 @@ exports.getModule = class BBSLinkModule extends MenuModule {
}; };
bridgeConnection.on('data', function incomingData(data) { bridgeConnection.on('data', function incomingData(data) {
// pass along // pass along
// :TODO: just pipe this as well // :TODO: just pipe this as well
self.client.term.rawWrite(data); self.client.term.rawWrite(data);
}); });
@ -182,9 +182,9 @@ exports.getModule = class BBSLinkModule extends MenuModule {
simpleHttpRequest(path, headers, cb) { simpleHttpRequest(path, headers, cb) {
const getOpts = { const getOpts = {
host : this.config.host, host : this.config.host,
path : path, path : path,
headers : headers, headers : headers,
}; };
const req = http.get(getOpts, function response(resp) { const req = http.get(getOpts, function response(resp) {

View File

@ -1,73 +1,73 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const { const {
getModDatabasePath, getModDatabasePath,
getTransactionDatabase getTransactionDatabase
} = require('./database.js'); } = require('./database.js');
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const theme = require('./theme.js'); const theme = require('./theme.js');
const User = require('./user.js'); const User = require('./user.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
// deps // deps
const async = require('async'); const async = require('async');
const sqlite3 = require('sqlite3'); const sqlite3 = require('sqlite3');
const _ = require('lodash'); const _ = require('lodash');
// :TODO: add notes field // :TODO: add notes field
const moduleInfo = exports.moduleInfo = { const moduleInfo = exports.moduleInfo = {
name : 'BBS List', name : 'BBS List',
desc : 'List of other BBSes', desc : 'List of other BBSes',
author : 'Andrew Pamment', author : 'Andrew Pamment',
packageName : 'com.magickabbs.enigma.bbslist' packageName : 'com.magickabbs.enigma.bbslist'
}; };
const MciViewIds = { const MciViewIds = {
view : { view : {
BBSList : 1, BBSList : 1,
SelectedBBSName : 2, SelectedBBSName : 2,
SelectedBBSSysOp : 3, SelectedBBSSysOp : 3,
SelectedBBSTelnet : 4, SelectedBBSTelnet : 4,
SelectedBBSWww : 5, SelectedBBSWww : 5,
SelectedBBSLoc : 6, SelectedBBSLoc : 6,
SelectedBBSSoftware : 7, SelectedBBSSoftware : 7,
SelectedBBSNotes : 8, SelectedBBSNotes : 8,
SelectedBBSSubmitter : 9, SelectedBBSSubmitter : 9,
}, },
add : { add : {
BBSName : 1, BBSName : 1,
Sysop : 2, Sysop : 2,
Telnet : 3, Telnet : 3,
Www : 4, Www : 4,
Location : 5, Location : 5,
Software : 6, Software : 6,
Notes : 7, Notes : 7,
Error : 8, Error : 8,
} }
}; };
const FormIds = { const FormIds = {
View : 0, View : 0,
Add : 1, Add : 1,
}; };
const SELECTED_MCI_NAME_TO_ENTRY = { const SELECTED_MCI_NAME_TO_ENTRY = {
SelectedBBSName : 'bbsName', SelectedBBSName : 'bbsName',
SelectedBBSSysOp : 'sysOp', SelectedBBSSysOp : 'sysOp',
SelectedBBSTelnet : 'telnet', SelectedBBSTelnet : 'telnet',
SelectedBBSWww : 'www', SelectedBBSWww : 'www',
SelectedBBSLoc : 'location', SelectedBBSLoc : 'location',
SelectedBBSSoftware : 'software', SelectedBBSSoftware : 'software',
SelectedBBSSubmitter : 'submitter', SelectedBBSSubmitter : 'submitter',
SelectedBBSSubmitterId : 'submitterUserId', SelectedBBSSubmitterId : 'submitterUserId',
SelectedBBSNotes : 'notes', SelectedBBSNotes : 'notes',
}; };
exports.getModule = class BBSListModule extends MenuModule { exports.getModule = class BBSListModule extends MenuModule {
@ -77,7 +77,7 @@ exports.getModule = class BBSListModule extends MenuModule {
const self = this; const self = this;
this.menuMethods = { this.menuMethods = {
// //
// Validators // Validators
// //
viewValidationListener : function(err, cb) { viewValidationListener : function(err, cb) {
const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error); const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error);
@ -93,7 +93,7 @@ exports.getModule = class BBSListModule extends MenuModule {
}, },
// //
// Key & submit handlers // Key & submit handlers
// //
addBBS : function(formData, extraArgs, cb) { addBBS : function(formData, extraArgs, cb) {
self.displayAddScreen(cb); self.displayAddScreen(cb);
@ -106,7 +106,7 @@ exports.getModule = class BBSListModule extends MenuModule {
const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList);
if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) { if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) {
// must be owner or +op // must be owner or +op
return cb(null); return cb(null);
} }
@ -117,7 +117,7 @@ exports.getModule = class BBSListModule extends MenuModule {
self.database.run( self.database.run(
`DELETE FROM bbs_list `DELETE FROM bbs_list
WHERE id=?;`, WHERE id=?;`,
[ entry.id ], [ entry.id ],
err => { err => {
if (err) { if (err) {
@ -147,13 +147,13 @@ exports.getModule = class BBSListModule extends MenuModule {
} }
}); });
if(!ok) { if(!ok) {
// validators should prevent this! // validators should prevent this!
return cb(null); return cb(null);
} }
self.database.run( self.database.run(
`INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes) `INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes)
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
[ [
formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www,
formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes
@ -188,7 +188,7 @@ exports.getModule = class BBSListModule extends MenuModule {
], ],
err => { err => {
if(err) { if(err) {
// :TODO: Handle me -- initSequence() should really take a completion callback // :TODO: Handle me -- initSequence() should really take a completion callback
} }
self.finishedLoading(); self.finishedLoading();
} }
@ -218,9 +218,9 @@ exports.getModule = class BBSListModule extends MenuModule {
} }
setEntries(entriesView) { setEntries(entriesView) {
const config = this.menuConfig.config; const config = this.menuConfig.config;
const listFormat = config.listFormat || '{bbsName}'; const listFormat = config.listFormat || '{bbsName}';
const focusListFormat = config.focusListFormat || '{bbsName}'; const focusListFormat = config.focusListFormat || '{bbsName}';
entriesView.setItems(this.entries.map( e => stringFormat(listFormat, e) ) ); entriesView.setItems(this.entries.map( e => stringFormat(listFormat, e) ) );
entriesView.setFocusItems(this.entries.map( e => stringFormat(focusListFormat, e) ) ); entriesView.setFocusItems(this.entries.map( e => stringFormat(focusListFormat, e) ) );
@ -255,9 +255,9 @@ exports.getModule = class BBSListModule extends MenuModule {
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds.View, formId : FormIds.View,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -273,19 +273,19 @@ exports.getModule = class BBSListModule extends MenuModule {
self.database.each( self.database.each(
`SELECT id, bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes `SELECT id, bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes
FROM bbs_list;`, FROM bbs_list;`,
(err, row) => { (err, row) => {
if (!err) { if (!err) {
self.entries.push({ self.entries.push({
id : row.id, id : row.id,
bbsName : row.bbs_name, bbsName : row.bbs_name,
sysOp : row.sysop, sysOp : row.sysop,
telnet : row.telnet, telnet : row.telnet,
www : row.www, www : row.www,
location : row.location, location : row.location,
software : row.software, software : row.software,
submitterUserId : row.submitter_user_id, submitterUserId : row.submitter_user_id,
notes : row.notes, notes : row.notes,
}); });
} }
}, },
@ -371,9 +371,9 @@ exports.getModule = class BBSListModule extends MenuModule {
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds.Add, formId : FormIds.Add,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -414,16 +414,16 @@ exports.getModule = class BBSListModule extends MenuModule {
self.database.serialize( () => { self.database.serialize( () => {
self.database.run( self.database.run(
`CREATE TABLE IF NOT EXISTS bbs_list ( `CREATE TABLE IF NOT EXISTS bbs_list (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
bbs_name VARCHAR NOT NULL, bbs_name VARCHAR NOT NULL,
sysop VARCHAR NOT NULL, sysop VARCHAR NOT NULL,
telnet VARCHAR NOT NULL, telnet VARCHAR NOT NULL,
www VARCHAR, www VARCHAR,
location VARCHAR, location VARCHAR,
software VARCHAR, software VARCHAR,
submitter_user_id INTEGER NOT NULL, submitter_user_id INTEGER NOT NULL,
notes VARCHAR notes VARCHAR
);` );`
); );
}); });
callback(null); callback(null);

View File

@ -1,17 +1,17 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const TextView = require('./text_view.js').TextView; const TextView = require('./text_view.js').TextView;
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
const util = require('util'); const util = require('util');
exports.ButtonView = ButtonView; exports.ButtonView = ButtonView;
function ButtonView(options) { function ButtonView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.justify = miscUtil.valueWithDefault(options.justify, 'center'); options.justify = miscUtil.valueWithDefault(options.justify, 'center');
options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide'); options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide');
TextView.call(this, options); TextView.call(this, options);
} }
@ -29,12 +29,12 @@ ButtonView.prototype.onKeyPress = function(ch, key) {
}; };
/* /*
ButtonView.prototype.onKeyPress = function(ch, key) { ButtonView.prototype.onKeyPress = function(ch, key) {
// allow space = submit // allow space = submit
if(' ' === ch) { if(' ' === ch) {
this.emit('action', 'accept'); this.emit('action', 'accept');
} }
ButtonView.super_.prototype.onKeyPress.call(this, ch, key); ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
}; };
*/ */

View File

@ -2,69 +2,69 @@
'use strict'; 'use strict';
/* /*
Portions of this code for key handling heavily inspired from the following: Portions of this code for key handling heavily inspired from the following:
https://github.com/chjj/blessed/blob/master/lib/keys.js https://github.com/chjj/blessed/blob/master/lib/keys.js
chji's blessed is MIT licensed: chji's blessed is MIT licensed:
----/snip/---------------------- ----/snip/----------------------
The MIT License (MIT) The MIT License (MIT)
Copyright (c) <year> <copyright holders> Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software. all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
----/snip/---------------------- ----/snip/----------------------
*/ */
// ENiGMA½ // ENiGMA½
const term = require('./client_term.js'); const term = require('./client_term.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const User = require('./user.js'); const User = require('./user.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const MenuStack = require('./menu_stack.js'); const MenuStack = require('./menu_stack.js');
const ACS = require('./acs.js'); const ACS = require('./acs.js');
const Events = require('./events.js'); const Events = require('./events.js');
// deps // deps
const stream = require('stream'); const stream = require('stream');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
exports.Client = Client; exports.Client = Client;
// :TODO: Move all of the key stuff to it's own module // :TODO: Move all of the key stuff to it's own module
// //
// Resources & Standards: // Resources & Standards:
// * http://www.ansi-bbs.org/ansi-bbs-core-server.html // * http://www.ansi-bbs.org/ansi-bbs-core-server.html
// //
const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/; const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/;
const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/; const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/;
const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/; const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/;
const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$'); const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$');
const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [ const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [
'(\\d+)(?:;(\\d+))?([~^$])', '(\\d+)(?:;(\\d+))?([~^$])',
'(?:M([@ #!a`])(.)(.))', // mouse stuff '(?:M([@ #!a`])(.)(.))', // mouse stuff
'(?:1;)?(\\d+)?([a-zA-Z@])' '(?:1;)?(\\d+)?([a-zA-Z@])'
].join('|') + ')'); ].join('|') + ')');
const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source); const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source);
const RE_ESC_CODE_ANYWHERE = new RegExp( [ const RE_ESC_CODE_ANYWHERE = new RegExp( [
RE_FUNCTION_KEYCODE_ANYWHERE.source, RE_FUNCTION_KEYCODE_ANYWHERE.source,
RE_META_KEYCODE_ANYWHERE.source, RE_META_KEYCODE_ANYWHERE.source,
RE_DSR_RESPONSE_ANYWHERE.source, RE_DSR_RESPONSE_ANYWHERE.source,
@ -76,14 +76,14 @@ const RE_ESC_CODE_ANYWHERE = new RegExp( [
function Client(/*input, output*/) { function Client(/*input, output*/) {
stream.call(this); stream.call(this);
const self = this; const self = this;
this.user = new User(); this.user = new User();
this.currentTheme = { info : { name : 'N/A', description : 'None' } }; this.currentTheme = { info : { name : 'N/A', description : 'None' } };
this.lastKeyPressMs = Date.now(); this.lastKeyPressMs = Date.now();
this.menuStack = new MenuStack(this); this.menuStack = new MenuStack(this);
this.acs = new ACS(this); this.acs = new ACS(this);
this.mciCache = {}; this.mciCache = {};
this.clearMciCache = function() { this.clearMciCache = function() {
this.mciCache = {}; this.mciCache = {};
@ -119,34 +119,34 @@ function Client(/*input, output*/) {
// //
// Peek at incoming |data| and emit events for any special // Peek at incoming |data| and emit events for any special
// handling that may include: // handling that may include:
// * Keyboard input // * Keyboard input
// * ANSI CSR's and the like // * ANSI CSR's and the like
// //
// References: // References:
// * http://www.ansi-bbs.org/ansi-bbs-core-server.html // * http://www.ansi-bbs.org/ansi-bbs-core-server.html
// * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/ // * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/
// //
this.getTermClient = function(deviceAttr) { this.getTermClient = function(deviceAttr) {
let termClient = { let termClient = {
// //
// See http://www.fbl.cz/arctel/download/techman.pdf // See http://www.fbl.cz/arctel/download/techman.pdf
// //
// Known clients: // Known clients:
// * Irssi ConnectBot (Android) // * Irssi ConnectBot (Android)
// //
'63;1;2' : 'arctel', '63;1;2' : 'arctel',
'50;86;84;88' : 'vtx', '50;86;84;88' : 'vtx',
}[deviceAttr]; }[deviceAttr];
if(!termClient) { if(!termClient) {
if(_.startsWith(deviceAttr, '67;84;101;114;109')) { if(_.startsWith(deviceAttr, '67;84;101;114;109')) {
// //
// See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
// //
// Known clients: // Known clients:
// * SyncTERM // * SyncTERM
// //
termClient = 'cterm'; termClient = 'cterm';
} }
@ -156,18 +156,18 @@ function Client(/*input, output*/) {
}; };
this.isMouseInput = function(data) { this.isMouseInput = function(data) {
return /\x1b\[M/.test(data) || // eslint-disable-line no-control-regex return /\x1b\[M/.test(data) || // eslint-disable-line no-control-regex
/\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || // eslint-disable-line no-control-regex /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || // eslint-disable-line no-control-regex
/\u001b\[(\d+;\d+;\d+)M/.test(data) || /\u001b\[(\d+;\d+;\d+)M/.test(data) ||
/\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) || /\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) ||
/\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) ||
/\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) || /\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) ||
/\u001b\[(O|I)/.test(data); /\u001b\[(O|I)/.test(data);
}; };
this.getKeyComponentsFromCode = function(code) { this.getKeyComponentsFromCode = function(code) {
return { return {
// xterm/gnome // xterm/gnome
'OP' : { name : 'f1' }, 'OP' : { name : 'f1' },
'OQ' : { name : 'f2' }, 'OQ' : { name : 'f2' },
'OR' : { name : 'f3' }, 'OR' : { name : 'f3' },
@ -181,93 +181,93 @@ function Client(/*input, output*/) {
'OF' : { name : 'end' }, 'OF' : { name : 'end' },
'OH' : { name : 'home' }, 'OH' : { name : 'home' },
// xterm/rxvt // xterm/rxvt
'[11~' : { name : 'f1' }, '[11~' : { name : 'f1' },
'[12~' : { name : 'f2' }, '[12~' : { name : 'f2' },
'[13~' : { name : 'f3' }, '[13~' : { name : 'f3' },
'[14~' : { name : 'f4' }, '[14~' : { name : 'f4' },
'[1~' : { name : 'home' }, '[1~' : { name : 'home' },
'[2~' : { name : 'insert' }, '[2~' : { name : 'insert' },
'[3~' : { name : 'delete' }, '[3~' : { name : 'delete' },
'[4~' : { name : 'end' }, '[4~' : { name : 'end' },
'[5~' : { name : 'page up' }, '[5~' : { name : 'page up' },
'[6~' : { name : 'page down' }, '[6~' : { name : 'page down' },
// Cygwin & libuv // Cygwin & libuv
'[[A' : { name : 'f1' }, '[[A' : { name : 'f1' },
'[[B' : { name : 'f2' }, '[[B' : { name : 'f2' },
'[[C' : { name : 'f3' }, '[[C' : { name : 'f3' },
'[[D' : { name : 'f4' }, '[[D' : { name : 'f4' },
'[[E' : { name : 'f5' }, '[[E' : { name : 'f5' },
// Common impls // Common impls
'[15~' : { name : 'f5' }, '[15~' : { name : 'f5' },
'[17~' : { name : 'f6' }, '[17~' : { name : 'f6' },
'[18~' : { name : 'f7' }, '[18~' : { name : 'f7' },
'[19~' : { name : 'f8' }, '[19~' : { name : 'f8' },
'[20~' : { name : 'f9' }, '[20~' : { name : 'f9' },
'[21~' : { name : 'f10' }, '[21~' : { name : 'f10' },
'[23~' : { name : 'f11' }, '[23~' : { name : 'f11' },
'[24~' : { name : 'f12' }, '[24~' : { name : 'f12' },
// xterm // xterm
'[A' : { name : 'up arrow' }, '[A' : { name : 'up arrow' },
'[B' : { name : 'down arrow' }, '[B' : { name : 'down arrow' },
'[C' : { name : 'right arrow' }, '[C' : { name : 'right arrow' },
'[D' : { name : 'left arrow' }, '[D' : { name : 'left arrow' },
'[E' : { name : 'clear' }, '[E' : { name : 'clear' },
'[F' : { name : 'end' }, '[F' : { name : 'end' },
'[H' : { name : 'home' }, '[H' : { name : 'home' },
// PuTTY // PuTTY
'[[5~' : { name : 'page up' }, '[[5~' : { name : 'page up' },
'[[6~' : { name : 'page down' }, '[[6~' : { name : 'page down' },
// rvxt // rvxt
'[7~' : { name : 'home' }, '[7~' : { name : 'home' },
'[8~' : { name : 'end' }, '[8~' : { name : 'end' },
// rxvt with modifiers // rxvt with modifiers
'[a' : { name : 'up arrow', shift : true }, '[a' : { name : 'up arrow', shift : true },
'[b' : { name : 'down arrow', shift : true }, '[b' : { name : 'down arrow', shift : true },
'[c' : { name : 'right arrow', shift : true }, '[c' : { name : 'right arrow', shift : true },
'[d' : { name : 'left arrow', shift : true }, '[d' : { name : 'left arrow', shift : true },
'[e' : { name : 'clear', shift : true }, '[e' : { name : 'clear', shift : true },
'[2$' : { name : 'insert', shift : true }, '[2$' : { name : 'insert', shift : true },
'[3$' : { name : 'delete', shift : true }, '[3$' : { name : 'delete', shift : true },
'[5$' : { name : 'page up', shift : true }, '[5$' : { name : 'page up', shift : true },
'[6$' : { name : 'page down', shift : true }, '[6$' : { name : 'page down', shift : true },
'[7$' : { name : 'home', shift : true }, '[7$' : { name : 'home', shift : true },
'[8$' : { name : 'end', shift : true }, '[8$' : { name : 'end', shift : true },
'Oa' : { name : 'up arrow', ctrl : true }, 'Oa' : { name : 'up arrow', ctrl : true },
'Ob' : { name : 'down arrow', ctrl : true }, 'Ob' : { name : 'down arrow', ctrl : true },
'Oc' : { name : 'right arrow', ctrl : true }, 'Oc' : { name : 'right arrow', ctrl : true },
'Od' : { name : 'left arrow', ctrl : true }, 'Od' : { name : 'left arrow', ctrl : true },
'Oe' : { name : 'clear', ctrl : true }, 'Oe' : { name : 'clear', ctrl : true },
'[2^' : { name : 'insert', ctrl : true }, '[2^' : { name : 'insert', ctrl : true },
'[3^' : { name : 'delete', ctrl : true }, '[3^' : { name : 'delete', ctrl : true },
'[5^' : { name : 'page up', ctrl : true }, '[5^' : { name : 'page up', ctrl : true },
'[6^' : { name : 'page down', ctrl : true }, '[6^' : { name : 'page down', ctrl : true },
'[7^' : { name : 'home', ctrl : true }, '[7^' : { name : 'home', ctrl : true },
'[8^' : { name : 'end', ctrl : true }, '[8^' : { name : 'end', ctrl : true },
// SyncTERM / EtherTerm // SyncTERM / EtherTerm
'[K' : { name : 'end' }, '[K' : { name : 'end' },
'[@' : { name : 'insert' }, '[@' : { name : 'insert' },
'[V' : { name : 'page up' }, '[V' : { name : 'page up' },
'[U' : { name : 'page down' }, '[U' : { name : 'page down' },
// other // other
'[Z' : { name : 'tab', shift : true }, '[Z' : { name : 'tab', shift : true },
}[code]; }[code];
}; };
this.on('data', function clientData(data) { this.on('data', function clientData(data) {
// create a uniform format that can be parsed below // create a uniform format that can be parsed below
if(data[0] > 127 && undefined === data[1]) { if(data[0] > 127 && undefined === data[1]) {
data[0] -= 128; data[0] -= 128;
data = '\u001b' + data.toString('utf-8'); data = '\u001b' + data.toString('utf-8');
@ -287,15 +287,15 @@ function Client(/*input, output*/) {
data = data.slice(m.index + m[0].length); data = data.slice(m.index + m[0].length);
} }
buf = buf.concat(data.split('')); // remainder buf = buf.concat(data.split('')); // remainder
buf.forEach(function bufPart(s) { buf.forEach(function bufPart(s) {
var key = { var key = {
seq : s, seq : s,
name : undefined, name : undefined,
ctrl : false, ctrl : false,
meta : false, meta : false,
shift : false, shift : false,
}; };
var parts; var parts;
@ -325,55 +325,55 @@ function Client(/*input, output*/) {
key.name = 'tab'; key.name = 'tab';
} else if('\x7f' === s) { } else if('\x7f' === s) {
// //
// Backspace vs delete is a crazy thing, especially in *nix. // Backspace vs delete is a crazy thing, especially in *nix.
// - ANSI-BBS uses 0x7f for DEL // - ANSI-BBS uses 0x7f for DEL
// - xterm et. al clients send 0x7f for backspace... ugg. // - xterm et. al clients send 0x7f for backspace... ugg.
// //
// See http://www.hypexr.org/linux_ruboff.php // See http://www.hypexr.org/linux_ruboff.php
// And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html // And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html
// //
if(self.term.isNixTerm()) { if(self.term.isNixTerm()) {
key.name = 'backspace'; key.name = 'backspace';
} else { } else {
key.name = 'delete'; key.name = 'delete';
} }
} else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) { } else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) {
// backspace, CTRL-H // backspace, CTRL-H
key.name = 'backspace'; key.name = 'backspace';
key.meta = ('\x1b' === s.charAt(0)); key.meta = ('\x1b' === s.charAt(0));
} else if('\x1b' === s || '\x1b\x1b' === s) { } else if('\x1b' === s || '\x1b\x1b' === s) {
key.name = 'escape'; key.name = 'escape';
key.meta = (2 === s.length); key.meta = (2 === s.length);
} else if (' ' === s || '\x1b ' === s) { } else if (' ' === s || '\x1b ' === s) {
// rather annoying that space can come in other than just " " // rather annoying that space can come in other than just " "
key.name = 'space'; key.name = 'space';
key.meta = (2 === s.length); key.meta = (2 === s.length);
} else if(1 === s.length && s <= '\x1a') { } else if(1 === s.length && s <= '\x1a') {
// CTRL-<letter> // CTRL-<letter>
key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
key.ctrl = true; key.ctrl = true;
} else if(1 === s.length && s >= 'a' && s <= 'z') { } else if(1 === s.length && s >= 'a' && s <= 'z') {
// normal, lowercased letter // normal, lowercased letter
key.name = s; key.name = s;
} else if(1 === s.length && s >= 'A' && s <= 'Z') { } else if(1 === s.length && s >= 'A' && s <= 'Z') {
key.name = s.toLowerCase(); key.name = s.toLowerCase();
key.shift = true; key.shift = true;
} else if ((parts = RE_META_KEYCODE.exec(s))) { } else if ((parts = RE_META_KEYCODE.exec(s))) {
// meta with character key // meta with character key
key.name = parts[1].toLowerCase(); key.name = parts[1].toLowerCase();
key.meta = true; key.meta = true;
key.shift = /^[A-Z]$/.test(parts[1]); key.shift = /^[A-Z]$/.test(parts[1]);
} else if((parts = RE_FUNCTION_KEYCODE.exec(s))) { } else if((parts = RE_FUNCTION_KEYCODE.exec(s))) {
var code = var code =
(parts[1] || '') + (parts[2] || '') + (parts[1] || '') + (parts[2] || '') +
(parts[4] || '') + (parts[9] || ''); (parts[4] || '') + (parts[9] || '');
var modifier = (parts[3] || parts[8] || 1) - 1; var modifier = (parts[3] || parts[8] || 1) - 1;
key.ctrl = !!(modifier & 4); key.ctrl = !!(modifier & 4);
key.meta = !!(modifier & 10); key.meta = !!(modifier & 10);
key.shift = !!(modifier & 1); key.shift = !!(modifier & 1);
key.code = code; key.code = code;
_.assign(key, self.getKeyComponentsFromCode(code)); _.assign(key, self.getKeyComponentsFromCode(code));
} }
@ -382,7 +382,7 @@ function Client(/*input, output*/) {
if(1 === s.length) { if(1 === s.length) {
ch = s; ch = s;
} else if('space' === key.name) { } else if('space' === key.name) {
// stupid hack to always get space as a regular char // stupid hack to always get space as a regular char
ch = ' '; ch = ' ';
} }
@ -390,18 +390,18 @@ function Client(/*input, output*/) {
key = undefined; key = undefined;
} else { } else {
// //
// Adjust name for CTRL/Shift/Meta modifiers // Adjust name for CTRL/Shift/Meta modifiers
// //
key.name = key.name =
(key.ctrl ? 'ctrl + ' : '') + (key.ctrl ? 'ctrl + ' : '') +
(key.meta ? 'meta + ' : '') + (key.meta ? 'meta + ' : '') +
(key.shift ? 'shift + ' : '') + (key.shift ? 'shift + ' : '') +
key.name; key.name;
} }
if(key || ch) { if(key || ch) {
if(Config().logging.traceUserKeyboardInput) { if(Config().logging.traceUserKeyboardInput) {
self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line
} }
self.lastKeyPressMs = Date.now(); self.lastKeyPressMs = Date.now();
@ -417,15 +417,15 @@ function Client(/*input, output*/) {
require('util').inherits(Client, stream); require('util').inherits(Client, stream);
Client.prototype.setInputOutput = function(input, output) { Client.prototype.setInputOutput = function(input, output) {
this.input = input; this.input = input;
this.output = output; this.output = output;
this.term = new term.ClientTerminal(this.output); this.term = new term.ClientTerminal(this.output);
}; };
Client.prototype.setTermType = function(termType) { Client.prototype.setTermType = function(termType) {
this.term.env.TERM = termType; this.term.env.TERM = termType;
this.term.termType = termType; this.term.termType = termType;
this.log.debug( { termType : termType }, 'Set terminal type'); this.log.debug( { termType : termType }, 'Set terminal type');
}; };
@ -434,10 +434,10 @@ Client.prototype.startIdleMonitor = function() {
this.lastKeyPressMs = Date.now(); this.lastKeyPressMs = Date.now();
// //
// Every 1m, check for idle. // Every 1m, check for idle.
// //
this.idleCheck = setInterval( () => { this.idleCheck = setInterval( () => {
const nowMs = Date.now(); const nowMs = Date.now();
const idleLogoutSeconds = this.user.isAuthenticated() ? const idleLogoutSeconds = this.user.isAuthenticated() ?
Config().misc.idleLogoutSeconds : Config().misc.idleLogoutSeconds :
@ -468,12 +468,12 @@ Client.prototype.end = function () {
try { try {
// //
// We can end up calling 'end' before TTY/etc. is established, e.g. with SSH // We can end up calling 'end' before TTY/etc. is established, e.g. with SSH
// //
// :TODO: is this OK? // :TODO: is this OK?
return this.output.end.apply(this.output, arguments); return this.output.end.apply(this.output, arguments);
} catch(e) { } catch(e) {
// TypeError // TypeError
} }
}; };
@ -492,15 +492,15 @@ Client.prototype.waitForKeyPress = function(cb) {
}; };
Client.prototype.isLocal = function() { Client.prototype.isLocal = function() {
// :TODO: Handle ipv6 better // :TODO: Handle ipv6 better
return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress); return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress);
}; };
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Default error handlers // Default error handlers
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something // :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something
Client.prototype.defaultHandlerMissingMod = function() { Client.prototype.defaultHandlerMissingMod = function() {
var self = this; var self = this;
@ -516,7 +516,7 @@ Client.prototype.defaultHandlerMissingMod = function() {
//self.term.write(err); //self.term.write(err);
//if(miscUtil.isDevelopment() && err.stack) { //if(miscUtil.isDevelopment() && err.stack) {
// self.term.write('\n' + err.stack + '\n'); // self.term.write('\n' + err.stack + '\n');
//} //}
self.end(); self.end();
@ -530,7 +530,7 @@ Client.prototype.terminalSupports = function(query) {
switch(query) { switch(query) {
case 'vtx_audio' : case 'vtx_audio' :
// https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
return 'vtx' === termClient; return 'vtx' === termClient;
case 'vtx_hyperlink' : case 'vtx_hyperlink' :

View File

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

View File

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

View File

@ -1,39 +1,39 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var ansi = require('./ansi_term.js'); var ansi = require('./ansi_term.js');
var getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue; var getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue;
var assert = require('assert'); var assert = require('assert');
var _ = require('lodash'); var _ = require('lodash');
exports.enigmaToAnsi = enigmaToAnsi; exports.enigmaToAnsi = enigmaToAnsi;
exports.stripPipeCodes = exports.stripEnigmaCodes = stripEnigmaCodes; exports.stripPipeCodes = exports.stripEnigmaCodes = stripEnigmaCodes;
exports.pipeStrLen = exports.enigmaStrLen = enigmaStrLen; exports.pipeStrLen = exports.enigmaStrLen = enigmaStrLen;
exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi; exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi;
exports.controlCodesToAnsi = controlCodesToAnsi; exports.controlCodesToAnsi = controlCodesToAnsi;
// :TODO: Not really happy with the module name of "color_codes". Would like something better // :TODO: Not really happy with the module name of "color_codes". Would like something better
// Also add: // Also add:
// * fromCelerity(): |<case sensitive letter> // * fromCelerity(): |<case sensitive letter>
// * fromPCBoard(): (@X<bg><fg>) // * fromPCBoard(): (@X<bg><fg>)
// * fromWildcat(): (@<bg><fg>@ (same as PCBoard without 'X' prefix and '@' suffix) // * fromWildcat(): (@<bg><fg>@ (same as PCBoard without 'X' prefix and '@' suffix)
// * fromWWIV(): <ctrl-c><0-7> // * fromWWIV(): <ctrl-c><0-7>
// * fromSyncronet(): <ctrl-a><colorCode> // * fromSyncronet(): <ctrl-a><colorCode>
// See http://wiki.synchro.net/custom:colors // See http://wiki.synchro.net/custom:colors
// :TODO: rid of enigmaToAnsi() -- never really use. Instead, create bbsToAnsi() that supports renegade, PCB, WWIV, etc... // :TODO: rid of enigmaToAnsi() -- never really use. Instead, create bbsToAnsi() that supports renegade, PCB, WWIV, etc...
function enigmaToAnsi(s, client) { function enigmaToAnsi(s, client) {
if(-1 == s.indexOf('|')) { if(-1 == s.indexOf('|')) {
return s; // no pipe codes present return s; // no pipe codes present
} }
var result = ''; var result = '';
var re = /\|([A-Z\d]{2}|\|)/g; var re = /\|([A-Z\d]{2}|\|)/g;
var m; var m;
var lastIndex = 0; var lastIndex = 0;
while((m = re.exec(s))) { while((m = re.exec(s))) {
@ -44,14 +44,14 @@ function enigmaToAnsi(s, client) {
continue; continue;
} }
// convert to number // convert to number
val = parseInt(val, 10); val = parseInt(val, 10);
if(isNaN(val)) { if(isNaN(val)) {
// //
// ENiGMA MCI code? Only available if |client| // ENiGMA MCI code? Only available if |client|
// is supplied. // is supplied.
// //
val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal
} }
if(_.isString(val)) { if(_.isString(val)) {
@ -89,51 +89,51 @@ function enigmaStrLen(s) {
function ansiSgrFromRenegadeColorCode(cc) { function ansiSgrFromRenegadeColorCode(cc) {
return ansi.sgr({ return ansi.sgr({
0 : [ 'reset', 'black' ], 0 : [ 'reset', 'black' ],
1 : [ 'reset', 'blue' ], 1 : [ 'reset', 'blue' ],
2 : [ 'reset', 'green' ], 2 : [ 'reset', 'green' ],
3 : [ 'reset', 'cyan' ], 3 : [ 'reset', 'cyan' ],
4 : [ 'reset', 'red' ], 4 : [ 'reset', 'red' ],
5 : [ 'reset', 'magenta' ], 5 : [ 'reset', 'magenta' ],
6 : [ 'reset', 'yellow' ], 6 : [ 'reset', 'yellow' ],
7 : [ 'reset', 'white' ], 7 : [ 'reset', 'white' ],
8 : [ 'bold', 'black' ], 8 : [ 'bold', 'black' ],
9 : [ 'bold', 'blue' ], 9 : [ 'bold', 'blue' ],
10 : [ 'bold', 'green' ], 10 : [ 'bold', 'green' ],
11 : [ 'bold', 'cyan' ], 11 : [ 'bold', 'cyan' ],
12 : [ 'bold', 'red' ], 12 : [ 'bold', 'red' ],
13 : [ 'bold', 'magenta' ], 13 : [ 'bold', 'magenta' ],
14 : [ 'bold', 'yellow' ], 14 : [ 'bold', 'yellow' ],
15 : [ 'bold', 'white' ], 15 : [ 'bold', 'white' ],
16 : [ 'blackBG' ], 16 : [ 'blackBG' ],
17 : [ 'blueBG' ], 17 : [ 'blueBG' ],
18 : [ 'greenBG' ], 18 : [ 'greenBG' ],
19 : [ 'cyanBG' ], 19 : [ 'cyanBG' ],
20 : [ 'redBG' ], 20 : [ 'redBG' ],
21 : [ 'magentaBG' ], 21 : [ 'magentaBG' ],
22 : [ 'yellowBG' ], 22 : [ 'yellowBG' ],
23 : [ 'whiteBG' ], 23 : [ 'whiteBG' ],
24 : [ 'blink', 'blackBG' ], 24 : [ 'blink', 'blackBG' ],
25 : [ 'blink', 'blueBG' ], 25 : [ 'blink', 'blueBG' ],
26 : [ 'blink', 'greenBG' ], 26 : [ 'blink', 'greenBG' ],
27 : [ 'blink', 'cyanBG' ], 27 : [ 'blink', 'cyanBG' ],
28 : [ 'blink', 'redBG' ], 28 : [ 'blink', 'redBG' ],
29 : [ 'blink', 'magentaBG' ], 29 : [ 'blink', 'magentaBG' ],
30 : [ 'blink', 'yellowBG' ], 30 : [ 'blink', 'yellowBG' ],
31 : [ 'blink', 'whiteBG' ], 31 : [ 'blink', 'whiteBG' ],
}[cc] || 'normal'); }[cc] || 'normal');
} }
function renegadeToAnsi(s, client) { function renegadeToAnsi(s, client) {
if(-1 == s.indexOf('|')) { if(-1 == s.indexOf('|')) {
return s; // no pipe codes present return s; // no pipe codes present
} }
var result = ''; var result = '';
var re = /\|([A-Z\d]{2}|\|)/g; var re = /\|([A-Z\d]{2}|\|)/g;
var m; var m;
var lastIndex = 0; var lastIndex = 0;
while((m = re.exec(s))) { while((m = re.exec(s))) {
@ -144,10 +144,10 @@ function renegadeToAnsi(s, client) {
continue; continue;
} }
// convert to number // convert to number
val = parseInt(val, 10); val = parseInt(val, 10);
if(isNaN(val)) { if(isNaN(val)) {
val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal
} }
if(_.isString(val)) { if(_.isString(val)) {
@ -164,27 +164,27 @@ function renegadeToAnsi(s, client) {
} }
// //
// Converts various control codes popular in BBS packages // Converts various control codes popular in BBS packages
// to ANSI escape sequences. Additionaly supports ENiGMA style // to ANSI escape sequences. Additionaly supports ENiGMA style
// MCI codes. // MCI codes.
// //
// Supported control code formats: // Supported control code formats:
// * Renegade : |## // * Renegade : |##
// * PCBoard : @X## where the first number/char is FG color, and second is BG // * PCBoard : @X## where the first number/char is FG color, and second is BG
// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix // * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix
// * WWIV : ^# // * WWIV : ^#
// //
// TODO: Add Synchronet and Celerity format support // TODO: Add Synchronet and Celerity format support
// //
// Resources: // Resources:
// * http://wiki.synchro.net/custom:colors // * http://wiki.synchro.net/custom:colors
// //
function controlCodesToAnsi(s, client) { function controlCodesToAnsi(s, client) {
const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex
let m; let m;
let result = ''; let result = '';
let lastIndex = 0; let lastIndex = 0;
let v; let v;
let fg; let fg;
let bg; let bg;
@ -192,11 +192,11 @@ function controlCodesToAnsi(s, client) {
while((m = RE.exec(s))) { while((m = RE.exec(s))) {
switch(m[0].charAt(0)) { switch(m[0].charAt(0)) {
case '|' : case '|' :
// Renegade or ENiGMA MCI // Renegade or ENiGMA MCI
v = parseInt(m[2], 10); v = parseInt(m[2], 10);
if(isNaN(v)) { if(isNaN(v)) {
v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal
} }
if(_.isString(v)) { if(_.isString(v)) {
@ -208,52 +208,52 @@ function controlCodesToAnsi(s, client) {
break; break;
case '@' : case '@' :
// PCBoard @X## or Wildcat! @##@ // PCBoard @X## or Wildcat! @##@
if('@' === m[0].substr(-1)) { if('@' === m[0].substr(-1)) {
// Wildcat! // Wildcat!
v = m[6]; v = m[6];
} else { } else {
v = m[4]; v = m[4];
} }
fg = { fg = {
0 : [ 'reset', 'black' ], 0 : [ 'reset', 'black' ],
1 : [ 'reset', 'blue' ], 1 : [ 'reset', 'blue' ],
2 : [ 'reset', 'green' ], 2 : [ 'reset', 'green' ],
3 : [ 'reset', 'cyan' ], 3 : [ 'reset', 'cyan' ],
4 : [ 'reset', 'red' ], 4 : [ 'reset', 'red' ],
5 : [ 'reset', 'magenta' ], 5 : [ 'reset', 'magenta' ],
6 : [ 'reset', 'yellow' ], 6 : [ 'reset', 'yellow' ],
7 : [ 'reset', 'white' ], 7 : [ 'reset', 'white' ],
8 : [ 'blink', 'black' ], 8 : [ 'blink', 'black' ],
9 : [ 'blink', 'blue' ], 9 : [ 'blink', 'blue' ],
A : [ 'blink', 'green' ], A : [ 'blink', 'green' ],
B : [ 'blink', 'cyan' ], B : [ 'blink', 'cyan' ],
C : [ 'blink', 'red' ], C : [ 'blink', 'red' ],
D : [ 'blink', 'magenta' ], D : [ 'blink', 'magenta' ],
E : [ 'blink', 'yellow' ], E : [ 'blink', 'yellow' ],
F : [ 'blink', 'white' ], F : [ 'blink', 'white' ],
}[v.charAt(0)] || ['normal']; }[v.charAt(0)] || ['normal'];
bg = { bg = {
0 : [ 'blackBG' ], 0 : [ 'blackBG' ],
1 : [ 'blueBG' ], 1 : [ 'blueBG' ],
2 : [ 'greenBG' ], 2 : [ 'greenBG' ],
3 : [ 'cyanBG' ], 3 : [ 'cyanBG' ],
4 : [ 'redBG' ], 4 : [ 'redBG' ],
5 : [ 'magentaBG' ], 5 : [ 'magentaBG' ],
6 : [ 'yellowBG' ], 6 : [ 'yellowBG' ],
7 : [ 'whiteBG' ], 7 : [ 'whiteBG' ],
8 : [ 'bold', 'blackBG' ], 8 : [ 'bold', 'blackBG' ],
9 : [ 'bold', 'blueBG' ], 9 : [ 'bold', 'blueBG' ],
A : [ 'bold', 'greenBG' ], A : [ 'bold', 'greenBG' ],
B : [ 'bold', 'cyanBG' ], B : [ 'bold', 'cyanBG' ],
C : [ 'bold', 'redBG' ], C : [ 'bold', 'redBG' ],
D : [ 'bold', 'magentaBG' ], D : [ 'bold', 'magentaBG' ],
E : [ 'bold', 'yellowBG' ], E : [ 'bold', 'yellowBG' ],
F : [ 'bold', 'whiteBG' ], F : [ 'bold', 'whiteBG' ],
}[v.charAt(1)] || [ 'normal' ]; }[v.charAt(1)] || [ 'normal' ];
v = ansi.sgr(fg.concat(bg)); v = ansi.sgr(fg.concat(bg));
@ -267,16 +267,16 @@ function controlCodesToAnsi(s, client) {
v += m[0]; v += m[0];
} else { } else {
v = ansi.sgr({ v = ansi.sgr({
0 : [ 'reset', 'black' ], 0 : [ 'reset', 'black' ],
1 : [ 'bold', 'cyan' ], 1 : [ 'bold', 'cyan' ],
2 : [ 'bold', 'yellow' ], 2 : [ 'bold', 'yellow' ],
3 : [ 'reset', 'magenta' ], 3 : [ 'reset', 'magenta' ],
4 : [ 'bold', 'white', 'blueBG' ], 4 : [ 'bold', 'white', 'blueBG' ],
5 : [ 'reset', 'green' ], 5 : [ 'reset', 'green' ],
6 : [ 'bold', 'blink', 'red' ], 6 : [ 'bold', 'blink', 'red' ],
7 : [ 'bold', 'blue' ], 7 : [ 'bold', 'blue' ],
8 : [ 'reset', 'blue' ], 8 : [ 'reset', 'blue' ],
9 : [ 'reset', 'cyan' ], 9 : [ 'reset', 'cyan' ],
}[v] || 'normal'); }[v] || 'normal');
} }

View File

@ -1,29 +1,29 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const MenuModule = require('../core/menu_module.js').MenuModule; const MenuModule = require('../core/menu_module.js').MenuModule;
const resetScreen = require('../core/ansi_term.js').resetScreen; const resetScreen = require('../core/ansi_term.js').resetScreen;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const RLogin = require('rlogin'); const RLogin = require('rlogin');
exports.moduleInfo = { exports.moduleInfo = {
name : 'CombatNet', name : 'CombatNet',
desc : 'CombatNet Access Module', desc : 'CombatNet Access Module',
author : 'Dave Stephens', author : 'Dave Stephens',
}; };
exports.getModule = class CombatNetModule extends MenuModule { exports.getModule = class CombatNetModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
// establish defaults // establish defaults
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
this.config.host = this.config.host || 'bbs.combatnet.us'; this.config.host = this.config.host || 'bbs.combatnet.us';
this.config.rloginPort = this.config.rloginPort || 4513; this.config.rloginPort = this.config.rloginPort || 4513;
} }
initSequence() { initSequence() {
@ -51,7 +51,7 @@ exports.getModule = class CombatNetModule extends MenuModule {
}; };
const rlogin = new RLogin( const rlogin = new RLogin(
{ 'clientUsername' : self.config.password, { 'clientUsername' : self.config.password,
'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`, 'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`,
'host' : self.config.host, 'host' : self.config.host,
'port' : self.config.rloginPort, 'port' : self.config.rloginPort,
@ -79,7 +79,7 @@ exports.getModule = class CombatNetModule extends MenuModule {
} }
rlogin.on('connect', rlogin.on('connect',
/* The 'connect' event handler will be supplied with one argument, /* The 'connect' event handler will be supplied with one argument,
a boolean indicating whether or not the connection was established. */ a boolean indicating whether or not the connection was established. */
function(state) { function(state) {
@ -101,7 +101,7 @@ exports.getModule = class CombatNetModule extends MenuModule {
// connect... // connect...
rlogin.connect(); rlogin.connect();
// note: no explicit callback() until we're finished! // note: no explicit callback() until we're finished!
} }
], ],
err => { err => {
@ -109,7 +109,7 @@ exports.getModule = class CombatNetModule extends MenuModule {
self.client.log.warn( { error : err.message }, 'CombatNet error'); self.client.log.warn( { error : err.message }, 'CombatNet error');
} }
// if the client is still here, go to previous // if the client is still here, go to previous
self.prevMenu(); self.prevMenu();
} }
); );

View File

@ -1,15 +1,15 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
exports.sortAreasOrConfs = sortAreasOrConfs; exports.sortAreasOrConfs = sortAreasOrConfs;
// //
// Method for sorting message, file, etc. areas and confs // Method for sorting message, file, etc. areas and confs
// If the sort key is present and is a number, sort in numerical order; // If the sort key is present and is a number, sort in numerical order;
// Otherwise, use a locale comparison on the sort key or name as a fallback // Otherwise, use a locale comparison on the sort key or name as a fallback
// //
function sortAreasOrConfs(areasOrConfs, type) { function sortAreasOrConfs(areasOrConfs, type) {
let entryA; let entryA;
@ -24,7 +24,7 @@ function sortAreasOrConfs(areasOrConfs, type) {
} else { } else {
const keyA = entryA.sort ? entryA.sort.toString() : entryA.name; const keyA = entryA.sort ? entryA.sort.toString() : entryA.name;
const keyB = entryB.sort ? entryB.sort.toString() : entryB.name; const keyB = entryB.sort ? entryB.sort.toString() : entryB.name;
return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare
} }
}); });
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,16 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// deps // deps
const paths = require('path'); const paths = require('path');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const hjson = require('hjson'); const hjson = require('hjson');
const sane = require('sane'); const sane = require('sane');
module.exports = new class ConfigCache module.exports = new class ConfigCache
{ {
constructor() { constructor() {
this.cache = new Map(); // path->parsed config this.cache = new Map(); // path->parsed config
} }
getConfigWithOptions(options, cb) { getConfigWithOptions(options, cb) {

View File

@ -1,19 +1,19 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const Config = require('./config.js').get; const Config = require('./config.js').get;
const ConfigCache = require('./config_cache.js'); const ConfigCache = require('./config_cache.js');
const Events = require('./events.js'); const Events = require('./events.js');
// deps // deps
const paths = require('path'); const paths = require('path');
const async = require('async'); const async = require('async');
exports.init = init; exports.init = init;
exports.getFullConfig = getFullConfig; exports.getFullConfig = getFullConfig;
function getConfigPath(filePath) { function getConfigPath(filePath) {
// |filePath| is assumed to be in the config path if it's only a file name // |filePath| is assumed to be in the config path if it's only a file name
if('.' === paths.dirname(filePath)) { if('.' === paths.dirname(filePath)) {
filePath = paths.join(Config().paths.config, filePath); filePath = paths.join(Config().paths.config, filePath);
} }
@ -21,7 +21,7 @@ function getConfigPath(filePath) {
} }
function init(cb) { function init(cb) {
// pre-cache menu.hjson and prompt.hjson + establish events // pre-cache menu.hjson and prompt.hjson + establish events
const changed = ( { fileName, fileRoot } ) => { const changed = ( { fileName, fileRoot } ) => {
const reCachedPath = paths.join(fileRoot, fileName); const reCachedPath = paths.join(fileRoot, fileName);
if(reCachedPath === getConfigPath(Config().general.menuFile)) { if(reCachedPath === getConfigPath(Config().general.menuFile)) {

View File

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

View File

@ -52,8 +52,8 @@ exports.CRC32 = class CRC32 {
} }
update_4(input) { update_4(input) {
const len = input.length - 3; const len = input.length - 3;
let i = 0; let i = 0;
for(i = 0; i < len;) { for(i = 0; i < len;) {
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
@ -67,8 +67,8 @@ exports.CRC32 = class CRC32 {
} }
update_8(input) { update_8(input) {
const len = input.length - 7; const len = input.length - 7;
let i = 0; let i = 0;
for(i = 0; i < len;) { for(i = 0; i < len;) {
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];

View File

@ -1,28 +1,28 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const conf = require('./config.js'); const conf = require('./config.js');
// deps // deps
const sqlite3 = require('sqlite3'); const sqlite3 = require('sqlite3');
const sqlite3Trans = require('sqlite3-trans'); const sqlite3Trans = require('sqlite3-trans');
const paths = require('path'); const paths = require('path');
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
const moment = require('moment'); const moment = require('moment');
// database handles // database handles
const dbs = {}; const dbs = {};
exports.getTransactionDatabase = getTransactionDatabase; exports.getTransactionDatabase = getTransactionDatabase;
exports.getModDatabasePath = getModDatabasePath; exports.getModDatabasePath = getModDatabasePath;
exports.getISOTimestampString = getISOTimestampString; exports.getISOTimestampString = getISOTimestampString;
exports.sanatizeString = sanatizeString; exports.sanatizeString = sanatizeString;
exports.initializeDatabases = initializeDatabases; exports.initializeDatabases = initializeDatabases;
exports.dbs = dbs; exports.dbs = dbs;
function getTransactionDatabase(db) { function getTransactionDatabase(db) {
return sqlite3Trans.wrap(db); return sqlite3Trans.wrap(db);
@ -34,9 +34,9 @@ function getDatabasePath(name) {
function getModDatabasePath(moduleInfo, suffix) { function getModDatabasePath(moduleInfo, suffix) {
// //
// Mods that use a database are stored in Config.paths.modsDb (e.g. enigma-bbs/db/mods) // Mods that use a database are stored in Config.paths.modsDb (e.g. enigma-bbs/db/mods)
// We expect that moduleInfo defines packageName which will be the base of the modules // We expect that moduleInfo defines packageName which will be the base of the modules
// filename. An optional suffix may be supplied as well. // filename. An optional suffix may be supplied as well.
// //
const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
@ -61,14 +61,14 @@ function getISOTimestampString(ts) {
} }
function sanatizeString(s) { function sanatizeString(s) {
return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex
switch (c) { switch (c) {
case '\0' : return '\\0'; case '\0' : return '\\0';
case '\x08' : return '\\b'; case '\x08' : return '\\b';
case '\x09' : return '\\t'; case '\x09' : return '\\t';
case '\x1a' : return '\\z'; case '\x1a' : return '\\z';
case '\n' : return '\\n'; case '\n' : return '\\n';
case '\r' : return '\\r'; case '\r' : return '\\r';
case '"' : case '"' :
case '\'' : case '\'' :
@ -107,35 +107,35 @@ const DB_INIT_TABLE = {
system : (cb) => { system : (cb) => {
enableForeignKeys(dbs.system); enableForeignKeys(dbs.system);
// Various stat/event logging - see stat_log.js // Various stat/event logging - see stat_log.js
dbs.system.run( dbs.system.run(
`CREATE TABLE IF NOT EXISTS system_stat ( `CREATE TABLE IF NOT EXISTS system_stat (
stat_name VARCHAR PRIMARY KEY NOT NULL, stat_name VARCHAR PRIMARY KEY NOT NULL,
stat_value VARCHAR NOT NULL stat_value VARCHAR NOT NULL
);` );`
); );
dbs.system.run( dbs.system.run(
`CREATE TABLE IF NOT EXISTS system_event_log ( `CREATE TABLE IF NOT EXISTS system_event_log (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
timestamp DATETIME NOT NULL, timestamp DATETIME NOT NULL,
log_name VARCHAR NOT NULL, log_name VARCHAR NOT NULL,
log_value VARCHAR NOT NULL, log_value VARCHAR NOT NULL,
UNIQUE(timestamp, log_name) UNIQUE(timestamp, log_name)
);` );`
); );
dbs.system.run( dbs.system.run(
`CREATE TABLE IF NOT EXISTS user_event_log ( `CREATE TABLE IF NOT EXISTS user_event_log (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
timestamp DATETIME NOT NULL, timestamp DATETIME NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
log_name VARCHAR NOT NULL, log_name VARCHAR NOT NULL,
log_value VARCHAR NOT NULL, log_value VARCHAR NOT NULL,
UNIQUE(timestamp, user_id, log_name) UNIQUE(timestamp, user_id, log_name)
);` );`
); );
return cb(null); return cb(null);
@ -146,38 +146,38 @@ const DB_INIT_TABLE = {
dbs.user.run( dbs.user.run(
`CREATE TABLE IF NOT EXISTS user ( `CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
user_name VARCHAR NOT NULL, user_name VARCHAR NOT NULL,
UNIQUE(user_name) UNIQUE(user_name)
);` );`
); );
// :TODO: create FK on delete/etc. // :TODO: create FK on delete/etc.
dbs.user.run( dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_property ( `CREATE TABLE IF NOT EXISTS user_property (
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
prop_name VARCHAR NOT NULL, prop_name VARCHAR NOT NULL,
prop_value VARCHAR, prop_value VARCHAR,
UNIQUE(user_id, prop_name), UNIQUE(user_id, prop_name),
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
);` );`
); );
dbs.user.run( dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_group_member ( `CREATE TABLE IF NOT EXISTS user_group_member (
group_name VARCHAR NOT NULL, group_name VARCHAR NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
UNIQUE(group_name, user_id) UNIQUE(group_name, user_id)
);` );`
); );
dbs.user.run( dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_login_history ( `CREATE TABLE IF NOT EXISTS user_login_history (
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
user_name VARCHAR NOT NULL, user_name VARCHAR NOT NULL,
timestamp DATETIME NOT NULL timestamp DATETIME NOT NULL
);` );`
); );
return cb(null); return cb(null);
@ -188,104 +188,104 @@ const DB_INIT_TABLE = {
dbs.message.run( dbs.message.run(
`CREATE TABLE IF NOT EXISTS message ( `CREATE TABLE IF NOT EXISTS message (
message_id INTEGER PRIMARY KEY, message_id INTEGER PRIMARY KEY,
area_tag VARCHAR NOT NULL, area_tag VARCHAR NOT NULL,
message_uuid VARCHAR(36) NOT NULL, message_uuid VARCHAR(36) NOT NULL,
reply_to_message_id INTEGER, reply_to_message_id INTEGER,
to_user_name VARCHAR NOT NULL, to_user_name VARCHAR NOT NULL,
from_user_name VARCHAR NOT NULL, from_user_name VARCHAR NOT NULL,
subject, /* FTS @ message_fts */ subject, /* FTS @ message_fts */
message, /* FTS @ message_fts */ message, /* FTS @ message_fts */
modified_timestamp DATETIME NOT NULL, modified_timestamp DATETIME NOT NULL,
view_count INTEGER NOT NULL DEFAULT 0, view_count INTEGER NOT NULL DEFAULT 0,
UNIQUE(message_uuid) UNIQUE(message_uuid)
);` );`
); );
dbs.message.run( dbs.message.run(
`CREATE INDEX IF NOT EXISTS message_by_area_tag_index `CREATE INDEX IF NOT EXISTS message_by_area_tag_index
ON message (area_tag);` ON message (area_tag);`
); );
dbs.message.run( dbs.message.run(
`CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 ( `CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 (
content="message", content="message",
subject, subject,
message message
);` );`
); );
dbs.message.run( dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN `CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN
DELETE FROM message_fts WHERE docid=old.rowid; DELETE FROM message_fts WHERE docid=old.rowid;
END;` END;`
); );
dbs.message.run( dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN `CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN
DELETE FROM message_fts WHERE docid=old.rowid; DELETE FROM message_fts WHERE docid=old.rowid;
END;` END;`
); );
dbs.message.run( dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN `CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN
INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);
END;` END;`
); );
dbs.message.run( dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN `CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN
INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);
END;` END;`
); );
dbs.message.run( dbs.message.run(
`CREATE TABLE IF NOT EXISTS message_meta ( `CREATE TABLE IF NOT EXISTS message_meta (
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
meta_category INTEGER NOT NULL, meta_category INTEGER NOT NULL,
meta_name VARCHAR NOT NULL, meta_name VARCHAR NOT NULL,
meta_value VARCHAR NOT NULL, meta_value VARCHAR NOT NULL,
UNIQUE(message_id, meta_category, meta_name, meta_value), UNIQUE(message_id, meta_category, meta_name, meta_value),
FOREIGN KEY(message_id) REFERENCES message(message_id) ON DELETE CASCADE FOREIGN KEY(message_id) REFERENCES message(message_id) ON DELETE CASCADE
);` );`
); );
// :TODO: need SQL to ensure cleaned up if delete from message? // :TODO: need SQL to ensure cleaned up if delete from message?
/* /*
dbs.message.run( dbs.message.run(
`CREATE TABLE IF NOT EXISTS hash_tag ( `CREATE TABLE IF NOT EXISTS hash_tag (
hash_tag_id INTEGER PRIMARY KEY, hash_tag_id INTEGER PRIMARY KEY,
hash_tag_name VARCHAR NOT NULL, hash_tag_name VARCHAR NOT NULL,
UNIQUE(hash_tag_name) UNIQUE(hash_tag_name)
);` );`
); );
// :TODO: need SQL to ensure cleaned up if delete from message? // :TODO: need SQL to ensure cleaned up if delete from message?
dbs.message.run( dbs.message.run(
`CREATE TABLE IF NOT EXISTS message_hash_tag ( `CREATE TABLE IF NOT EXISTS message_hash_tag (
hash_tag_id INTEGER NOT NULL, hash_tag_id INTEGER NOT NULL,
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
);` );`
); );
*/ */
dbs.message.run( dbs.message.run(
`CREATE TABLE IF NOT EXISTS user_message_area_last_read ( `CREATE TABLE IF NOT EXISTS user_message_area_last_read (
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
area_tag VARCHAR NOT NULL, area_tag VARCHAR NOT NULL,
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
UNIQUE(user_id, area_tag) UNIQUE(user_id, area_tag)
);` );`
); );
dbs.message.run( dbs.message.run(
`CREATE TABLE IF NOT EXISTS message_area_last_scan ( `CREATE TABLE IF NOT EXISTS message_area_last_scan (
scan_toss VARCHAR NOT NULL, scan_toss VARCHAR NOT NULL,
area_tag VARCHAR NOT NULL, area_tag VARCHAR NOT NULL,
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
UNIQUE(scan_toss, area_tag) UNIQUE(scan_toss, area_tag)
);` );`
); );
return cb(null); return cb(null);
@ -295,114 +295,114 @@ const DB_INIT_TABLE = {
enableForeignKeys(dbs.file); enableForeignKeys(dbs.file);
dbs.file.run( dbs.file.run(
// :TODO: should any of this be unique -- file_sha256 unless dupes are allowed on the system // :TODO: should any of this be unique -- file_sha256 unless dupes are allowed on the system
`CREATE TABLE IF NOT EXISTS file ( `CREATE TABLE IF NOT EXISTS file (
file_id INTEGER PRIMARY KEY, file_id INTEGER PRIMARY KEY,
area_tag VARCHAR NOT NULL, area_tag VARCHAR NOT NULL,
file_sha256 VARCHAR NOT NULL, file_sha256 VARCHAR NOT NULL,
file_name, /* FTS @ file_fts */ file_name, /* FTS @ file_fts */
storage_tag VARCHAR NOT NULL, storage_tag VARCHAR NOT NULL,
desc, /* FTS @ file_fts */ desc, /* FTS @ file_fts */
desc_long, /* FTS @ file_fts */ desc_long, /* FTS @ file_fts */
upload_timestamp DATETIME NOT NULL upload_timestamp DATETIME NOT NULL
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE INDEX IF NOT EXISTS file_by_area_tag_index `CREATE INDEX IF NOT EXISTS file_by_area_tag_index
ON file (area_tag);` ON file (area_tag);`
); );
dbs.file.run( dbs.file.run(
`CREATE INDEX IF NOT EXISTS file_by_sha256_index `CREATE INDEX IF NOT EXISTS file_by_sha256_index
ON file (file_sha256);` ON file (file_sha256);`
); );
dbs.file.run( dbs.file.run(
`CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts4 ( `CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts4 (
content="file", content="file",
file_name, file_name,
desc, desc,
desc_long desc_long
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN `CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN
DELETE FROM file_fts WHERE docid=old.rowid; DELETE FROM file_fts WHERE docid=old.rowid;
END;` END;`
); );
dbs.file.run( dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN `CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN
DELETE FROM file_fts WHERE docid=old.rowid; DELETE FROM file_fts WHERE docid=old.rowid;
END;` END;`
); );
dbs.file.run( dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN `CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN
INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long); INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long);
END;` END;`
); );
dbs.file.run( dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN `CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN
INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long); INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long);
END;` END;`
); );
dbs.file.run( dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_meta ( `CREATE TABLE IF NOT EXISTS file_meta (
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,
meta_name VARCHAR NOT NULL, meta_name VARCHAR NOT NULL,
meta_value VARCHAR NOT NULL, meta_value VARCHAR NOT NULL,
UNIQUE(file_id, meta_name, meta_value), UNIQUE(file_id, meta_name, meta_value),
FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE TABLE IF NOT EXISTS hash_tag ( `CREATE TABLE IF NOT EXISTS hash_tag (
hash_tag_id INTEGER PRIMARY KEY, hash_tag_id INTEGER PRIMARY KEY,
hash_tag VARCHAR NOT NULL, hash_tag VARCHAR NOT NULL,
UNIQUE(hash_tag) UNIQUE(hash_tag)
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_hash_tag ( `CREATE TABLE IF NOT EXISTS file_hash_tag (
hash_tag_id INTEGER NOT NULL, hash_tag_id INTEGER NOT NULL,
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,
UNIQUE(hash_tag_id, file_id) UNIQUE(hash_tag_id, file_id)
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_user_rating ( `CREATE TABLE IF NOT EXISTS file_user_rating (
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
rating INTEGER NOT NULL, rating INTEGER NOT NULL,
UNIQUE(file_id, user_id) UNIQUE(file_id, user_id)
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_web_serve ( `CREATE TABLE IF NOT EXISTS file_web_serve (
hash_id VARCHAR NOT NULL PRIMARY KEY, hash_id VARCHAR NOT NULL PRIMARY KEY,
expire_timestamp DATETIME NOT NULL expire_timestamp DATETIME NOT NULL
);` );`
); );
dbs.file.run( dbs.file.run(
`CREATE TABLE IF NOT EXISTS file_web_serve_batch ( `CREATE TABLE IF NOT EXISTS file_web_serve_batch (
hash_id VARCHAR NOT NULL, hash_id VARCHAR NOT NULL,
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,
UNIQUE(hash_id, file_id) UNIQUE(hash_id, file_id)
);` );`
); );
return cb(null); return cb(null);

View File

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

View File

@ -2,36 +2,36 @@
'use strict'; 'use strict';
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const events = require('events'); const events = require('events');
const _ = require('lodash'); const _ = require('lodash');
const pty = require('node-pty'); const pty = require('node-pty');
const decode = require('iconv-lite').decode; const decode = require('iconv-lite').decode;
const createServer = require('net').createServer; const createServer = require('net').createServer;
exports.Door = Door; exports.Door = Door;
function Door(client, exeInfo) { function Door(client, exeInfo) {
events.EventEmitter.call(this); events.EventEmitter.call(this);
const self = this; const self = this;
this.client = client; this.client = client;
this.exeInfo = exeInfo; this.exeInfo = exeInfo;
this.exeInfo.encoding = (this.exeInfo.encoding || 'cp437').toLowerCase(); this.exeInfo.encoding = (this.exeInfo.encoding || 'cp437').toLowerCase();
let restored = false; let restored = false;
// //
// Members of exeInfo: // Members of exeInfo:
// cmd // cmd
// args[] // args[]
// env{} // env{}
// cwd // cwd
// io // io
// encoding // encoding
// dropFile // dropFile
// node // node
// inhSocket // inhSocket
// //
this.doorDataHandler = function(data) { this.doorDataHandler = function(data) {
@ -52,7 +52,7 @@ function Door(client, exeInfo) {
sockServer.getConnections( (err, count) => { sockServer.getConnections( (err, count) => {
// We expect only one connection from our DOOR/emulator/etc. // We expect only one connection from our DOOR/emulator/etc.
if(!err && count <= 1) { if(!err && count <= 1) {
self.client.term.output.pipe(conn); self.client.term.output.pipe(conn);
@ -94,25 +94,25 @@ Door.prototype.run = function() {
return self.doorExited(); return self.doorExited();
} }
// Expand arg strings, e.g. {dropFile} -> DOOR32.SYS // Expand arg strings, e.g. {dropFile} -> DOOR32.SYS
// :TODO: Use .map() here // :TODO: Use .map() here
let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified
for(let i = 0; i < args.length; ++i) { for(let i = 0; i < args.length; ++i) {
args[i] = stringFormat(self.exeInfo.args[i], { args[i] = stringFormat(self.exeInfo.args[i], {
dropFile : self.exeInfo.dropFile, dropFile : self.exeInfo.dropFile,
node : self.exeInfo.node.toString(), node : self.exeInfo.node.toString(),
srvPort : sockServer ? sockServer.address().port.toString() : '-1', srvPort : sockServer ? sockServer.address().port.toString() : '-1',
userId : self.client.user.userId.toString(), userId : self.client.user.userId.toString(),
}); });
} }
const door = pty.spawn(self.exeInfo.cmd, args, { const door = pty.spawn(self.exeInfo.cmd, args, {
cols : self.client.term.termWidth, cols : self.client.term.termWidth,
rows : self.client.term.termHeight, rows : self.client.term.termHeight,
// :TODO: cwd // :TODO: cwd
env : self.exeInfo.env, env : self.exeInfo.env,
encoding : null, // we want to handle all encoding ourself encoding : null, // we want to handle all encoding ourself
}); });
if('stdio' === self.exeInfo.io) { if('stdio' === self.exeInfo.io) {
@ -136,7 +136,7 @@ Door.prototype.run = function() {
sockServer.close(); sockServer.close();
} }
// we may not get a close // we may not get a close
if('stdio' === self.exeInfo.io) { if('stdio' === self.exeInfo.io) {
self.restoreIo(door); self.restoreIo(door);
} }

View File

@ -1,30 +1,30 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const MenuModule = require('../core/menu_module.js').MenuModule; const MenuModule = require('../core/menu_module.js').MenuModule;
const resetScreen = require('../core/ansi_term.js').resetScreen; const resetScreen = require('../core/ansi_term.js').resetScreen;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const SSHClient = require('ssh2').Client; const SSHClient = require('ssh2').Client;
exports.moduleInfo = { exports.moduleInfo = {
name : 'DoorParty', name : 'DoorParty',
desc : 'DoorParty Access Module', desc : 'DoorParty Access Module',
author : 'NuSkooler', author : 'NuSkooler',
}; };
exports.getModule = class DoorPartyModule extends MenuModule { exports.getModule = class DoorPartyModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
// establish defaults // establish defaults
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
this.config.host = this.config.host || 'dp.throwbackbbs.com'; this.config.host = this.config.host || 'dp.throwbackbbs.com';
this.config.sshPort = this.config.sshPort || 2022; this.config.sshPort = this.config.sshPort || 2022;
this.config.rloginPort = this.config.rloginPort || 513; this.config.rloginPort = this.config.rloginPort || 513;
} }
initSequence() { initSequence() {
@ -61,32 +61,32 @@ exports.getModule = class DoorPartyModule extends MenuModule {
}; };
sshClient.on('ready', () => { sshClient.on('ready', () => {
// track client termination so we can clean up early // track client termination so we can clean up early
self.client.once('end', () => { self.client.once('end', () => {
self.client.log.info('Connection ended. Terminating DoorParty connection'); self.client.log.info('Connection ended. Terminating DoorParty connection');
clientTerminated = true; clientTerminated = true;
sshClient.end(); sshClient.end();
}); });
// establish tunnel for rlogin // establish tunnel for rlogin
sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => { sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => {
if(err) { if(err) {
return callback(new Error('Failed to establish tunnel')); return callback(new Error('Failed to establish tunnel'));
} }
// //
// Send rlogin // Send rlogin
// DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g. // DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g.
// [XA]nuskooler // [XA]nuskooler
// //
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
stream.write(rlogin); stream.write(rlogin);
pipedStream = stream; // :TODO: this is hacky... pipedStream = stream; // :TODO: this is hacky...
self.client.term.output.pipe(stream); self.client.term.output.pipe(stream);
stream.on('data', d => { stream.on('data', d => {
// :TODO: we should just pipe this... // :TODO: we should just pipe this...
self.client.term.rawWrite(d); self.client.term.rawWrite(d);
}); });
@ -107,13 +107,13 @@ exports.getModule = class DoorPartyModule extends MenuModule {
}); });
sshClient.connect( { sshClient.connect( {
host : self.config.host, host : self.config.host,
port : self.config.sshPort, port : self.config.sshPort,
username : self.config.username, username : self.config.username,
password : self.config.password, password : self.config.password,
}); });
// note: no explicit callback() until we're finished! // note: no explicit callback() until we're finished!
} }
], ],
err => { err => {
@ -121,7 +121,7 @@ exports.getModule = class DoorPartyModule extends MenuModule {
self.client.log.warn( { error : err.message }, 'DoorParty error'); self.client.log.warn( { error : err.message }, 'DoorParty error');
} }
// if the client is stil here, go to previous // if the client is stil here, go to previous
if(!clientTerminated) { if(!clientTerminated) {
self.prevMenu(); self.prevMenu();
} }

View File

@ -1,14 +1,14 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
// deps // deps
const { partition } = require('lodash'); const { partition } = require('lodash');
module.exports = class DownloadQueue { module.exports = class DownloadQueue {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
if(!Array.isArray(this.client.user.downloadQueue)) { if(!Array.isArray(this.client.user.downloadQueue)) {
if(this.client.user.properties.dl_queue) { if(this.client.user.properties.dl_queue) {
@ -37,12 +37,12 @@ module.exports = class DownloadQueue {
add(fileEntry, systemFile=false) { add(fileEntry, systemFile=false) {
this.client.user.downloadQueue.push({ this.client.user.downloadQueue.push({
fileId : fileEntry.fileId, fileId : fileEntry.fileId,
areaTag : fileEntry.areaTag, areaTag : fileEntry.areaTag,
fileName : fileEntry.fileName, fileName : fileEntry.fileName,
path : fileEntry.filePath, path : fileEntry.filePath,
byteSize : fileEntry.meta.byte_size || 0, byteSize : fileEntry.meta.byte_size || 0,
systemFile : systemFile, systemFile : systemFile,
}); });
} }

View File

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

View File

@ -1,21 +1,21 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const TextView = require('./text_view.js').TextView; const TextView = require('./text_view.js').TextView;
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
const strUtil = require('./string_util.js'); const strUtil = require('./string_util.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
exports.EditTextView = EditTextView; exports.EditTextView = EditTextView;
function EditTextView(options) { function EditTextView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
options.resizable = false; options.resizable = false;
TextView.call(this, options); TextView.call(this, options);
@ -47,9 +47,9 @@ EditTextView.prototype.onKeyPress = function(ch, key) {
return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); return EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
} else if(this.isKeyMapped('clearLine', key.name)) { } else if(this.isKeyMapped('clearLine', key.name)) {
this.text = ''; this.text = '';
this.cursorPos.col = 0; this.cursorPos.col = 0;
this.setFocus(true); // resetting focus will redraw & adjust cursor this.setFocus(true); // resetting focus will redraw & adjust cursor
return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); return EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
} }
@ -62,7 +62,7 @@ EditTextView.prototype.onKeyPress = function(ch, key) {
this.text += ch; this.text += ch;
if(this.text.length > this.dimens.width) { if(this.text.length > this.dimens.width) {
// no shortcuts - redraw the view // no shortcuts - redraw the view
this.redraw(); this.redraw();
} else { } else {
this.cursorPos.col += 1; this.cursorPos.col += 1;
@ -82,9 +82,9 @@ EditTextView.prototype.onKeyPress = function(ch, key) {
}; };
EditTextView.prototype.setText = function(text) { EditTextView.prototype.setText = function(text) {
// draw & set |text| // draw & set |text|
EditTextView.super_.prototype.setText.call(this, text); EditTextView.super_.prototype.setText.call(this, text);
// adjust local cursor tracking // adjust local cursor tracking
this.cursorPos = { row : 0, col : text.length }; this.cursorPos = { row : 0, col : text.length };
}; };

View File

@ -1,16 +1,16 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const nodeMailer = require('nodemailer'); const nodeMailer = require('nodemailer');
exports.sendMail = sendMail; exports.sendMail = sendMail;
function sendMail(message, cb) { function sendMail(message, cb) {
const config = Config(); const config = Config();
@ -21,7 +21,7 @@ function sendMail(message, cb) {
message.from = message.from || config.email.defaultFrom; message.from = message.from || config.email.defaultFrom;
const transportOptions = Object.assign( {}, config.email.transport, { const transportOptions = Object.assign( {}, config.email.transport, {
logger : Log, logger : Log,
}); });
const transport = nodeMailer.createTransport(transportOptions); const transport = nodeMailer.createTransport(transportOptions);

View File

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

View File

@ -1,12 +1,12 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
// deps // deps
const assert = require('assert'); const assert = require('assert');
module.exports = function(condition, message) { module.exports = function(condition, message) {
if(Config().debug.assertsEnabled) { if(Config().debug.assertsEnabled) {

View File

@ -1,55 +1,55 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const net = require('net'); const net = require('net');
/* /*
Expected configuration block example: Expected configuration block example:
config: { config: {
host: 192.168.1.171 host: 192.168.1.171
port: 5001 port: 5001
bbsTag: SOME_TAG bbsTag: SOME_TAG
} }
*/ */
exports.getModule = ErcClientModule; exports.getModule = ErcClientModule;
exports.moduleInfo = { exports.moduleInfo = {
name : 'ENiGMA Relay Chat Client', name : 'ENiGMA Relay Chat Client',
desc : 'Chat with other ENiGMA BBSes', desc : 'Chat with other ENiGMA BBSes',
author : 'Andrew Pamment', author : 'Andrew Pamment',
}; };
var MciViewIds = { var MciViewIds = {
ChatDisplay : 1, ChatDisplay : 1,
InputArea : 3, InputArea : 3,
}; };
// :TODO: needs converted to ES6 MenuModule subclass // :TODO: needs converted to ES6 MenuModule subclass
function ErcClientModule(options) { function ErcClientModule(options) {
MenuModule.prototype.ctorShim.call(this, options); MenuModule.prototype.ctorShim.call(this, options);
const self = this; const self = this;
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}'; this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}';
this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}'; this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}';
this.finishedLoading = function() { this.finishedLoading = function() {
async.waterfall( async.waterfall(
[ [
function validateConfig(callback) { function validateConfig(callback) {
if(_.isString(self.config.host) && if(_.isString(self.config.host) &&
_.isNumber(self.config.port) && _.isNumber(self.config.port) &&
_.isString(self.config.bbsTag)) _.isString(self.config.bbsTag))
{ {
return callback(null); return callback(null);
} else { } else {
@ -58,8 +58,8 @@ function ErcClientModule(options) {
}, },
function connectToServer(callback) { function connectToServer(callback) {
const connectOpts = { const connectOpts = {
port : self.config.port, port : self.config.port,
host : self.config.host, host : self.config.host,
}; };
const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay);
@ -69,7 +69,7 @@ function ErcClientModule(options) {
self.viewControllers.menu.switchFocus(MciViewIds.InputArea); self.viewControllers.menu.switchFocus(MciViewIds.InputArea);
// :TODO: Track actual client->enig connection for optional prevMenu @ final CB // :TODO: Track actual client->enig connection for optional prevMenu @ final CB
self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host); self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host);
self.chatConnection.on('data', data => { self.chatConnection.on('data', data => {
@ -87,10 +87,10 @@ function ErcClientModule(options) {
let text; let text;
try { try {
if(data.userName) { if(data.userName) {
// user message // user message
text = stringFormat(self.chatEntryFormat, data); text = stringFormat(self.chatEntryFormat, data);
} else { } else {
// system message // system message
text = stringFormat(self.systemEntryFormat, data); text = stringFormat(self.systemEntryFormat, data);
} }
} catch(e) { } catch(e) {
@ -99,7 +99,7 @@ function ErcClientModule(options) {
chatMessageView.addText(text); chatMessageView.addText(text);
if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height? if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height?
chatMessageView.deleteLine(0); chatMessageView.deleteLine(0);
chatMessageView.scrollDown(); chatMessageView.scrollDown();
} }
@ -130,8 +130,8 @@ function ErcClientModule(options) {
}; };
this.scrollHandler = function(keyName) { this.scrollHandler = function(keyName) {
const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea);
const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay);
if('up arrow' === keyName) { if('up arrow' === keyName) {
chatDisplayView.scrollUp(); chatDisplayView.scrollUp();
@ -147,7 +147,7 @@ function ErcClientModule(options) {
this.menuMethods = { this.menuMethods = {
inputAreaSubmit : function(formData, extraArgs, cb) { inputAreaSubmit : function(formData, extraArgs, cb) {
const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea);
const inputData = inputAreaView.getData(); const inputData = inputAreaView.getData();
if('/quit' === inputData.toLowerCase()) { if('/quit' === inputData.toLowerCase()) {
self.chatConnection.end(); self.chatConnection.end();

View File

@ -1,37 +1,37 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const PluginModule = require('./plugin_module.js').PluginModule; const PluginModule = require('./plugin_module.js').PluginModule;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const _ = require('lodash'); const _ = require('lodash');
const later = require('later'); const later = require('later');
const path = require('path'); const path = require('path');
const pty = require('node-pty'); const pty = require('node-pty');
const sane = require('sane'); const sane = require('sane');
const moment = require('moment'); const moment = require('moment');
const paths = require('path'); const paths = require('path');
const fse = require('fs-extra'); const fse = require('fs-extra');
exports.getModule = EventSchedulerModule; exports.getModule = EventSchedulerModule;
exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart
exports.moduleInfo = { exports.moduleInfo = {
name : 'Event Scheduler', name : 'Event Scheduler',
desc : 'Support for scheduling arbritary events', desc : 'Support for scheduling arbritary events',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/; const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/;
const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/; const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/;
class ScheduledEvent { class ScheduledEvent {
constructor(events, name) { constructor(events, name) {
this.name = name; this.name = name;
this.schedule = this.parseScheduleString(events[name].schedule); this.schedule = this.parseScheduleString(events[name].schedule);
this.action = this.parseActionSpec(events[name].action); this.action = this.parseActionSpec(events[name].action);
if(this.action) { if(this.action) {
this.action.args = events[name].args || []; this.action.args = events[name].args || [];
} }
@ -72,7 +72,7 @@ class ScheduledEvent {
} }
} }
// return undefined if we couldn't parse out anything useful // return undefined if we couldn't parse out anything useful
if(!_.isEmpty(schedule)) { if(!_.isEmpty(schedule)) {
return schedule; return schedule;
} }
@ -86,21 +86,21 @@ class ScheduledEvent {
if(m[2].indexOf(':') > -1) { if(m[2].indexOf(':') > -1) {
const parts = m[2].split(':'); const parts = m[2].split(':');
return { return {
type : m[1], type : m[1],
location : parts[0], location : parts[0],
what : parts[1], what : parts[1],
}; };
} else { } else {
return { return {
type : m[1], type : m[1],
what : m[2], what : m[2],
}; };
} }
} }
} else { } else {
return { return {
type : 'execute', type : 'execute',
what : actionSpec, what : actionSpec,
}; };
} }
} }
@ -110,7 +110,7 @@ class ScheduledEvent {
Log.info( { eventName : this.name, action : this.action, reason : reason }, 'Executing scheduled event action...'); Log.info( { eventName : this.name, action : this.action, reason : reason }, 'Executing scheduled event action...');
if('method' === this.action.type) { if('method' === this.action.type) {
const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js') const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js')
try { try {
const methodModule = require(modulePath); const methodModule = require(modulePath);
methodModule[this.action.what](this.action.args, err => { methodModule[this.action.what](this.action.args, err => {
@ -131,11 +131,11 @@ class ScheduledEvent {
} }
} else if('execute' === this.action.type) { } else if('execute' === this.action.type) {
const opts = { const opts = {
// :TODO: cwd // :TODO: cwd
name : this.name, name : this.name,
cols : 80, cols : 80,
rows : 24, rows : 24,
env : process.env, env : process.env,
}; };
const proc = pty.spawn(this.action.what, this.action.args, opts); const proc = pty.spawn(this.action.what, this.action.args, opts);
@ -165,7 +165,7 @@ function EventSchedulerModule(options) {
this.performAction = function(schedEvent, reason) { this.performAction = function(schedEvent, reason) {
if(self.runningActions.has(schedEvent.name)) { if(self.runningActions.has(schedEvent.name)) {
return; // already running return; // already running
} }
self.runningActions.add(schedEvent.name); self.runningActions.add(schedEvent.name);
@ -176,13 +176,13 @@ function EventSchedulerModule(options) {
}; };
} }
// convienence static method for direct load + start // convienence static method for direct load + start
EventSchedulerModule.loadAndStart = function(cb) { EventSchedulerModule.loadAndStart = function(cb) {
const loadModuleEx = require('./module_util.js').loadModuleEx; const loadModuleEx = require('./module_util.js').loadModuleEx;
const loadOpts = { const loadOpts = {
name : path.basename(__filename, '.js'), name : path.basename(__filename, '.js'),
path : __dirname, path : __dirname,
}; };
loadModuleEx(loadOpts, (err, mod) => { loadModuleEx(loadOpts, (err, mod) => {
@ -199,7 +199,7 @@ EventSchedulerModule.loadAndStart = function(cb) {
EventSchedulerModule.prototype.startup = function(cb) { EventSchedulerModule.prototype.startup = function(cb) {
this.eventTimers = []; this.eventTimers = [];
const self = this; const self = this;
if(this.moduleConfig && _.has(this.moduleConfig, 'events')) { if(this.moduleConfig && _.has(this.moduleConfig, 'events')) {
@ -215,10 +215,10 @@ EventSchedulerModule.prototype.startup = function(cb) {
Log.debug( Log.debug(
{ {
eventName : schedEvent.name, eventName : schedEvent.name,
schedule : this.moduleConfig.events[schedEvent.name].schedule, schedule : this.moduleConfig.events[schedEvent.name].schedule,
action : schedEvent.action, action : schedEvent.action,
next : schedEvent.schedule.sched ? moment(later.schedule(schedEvent.schedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', next : schedEvent.schedule.sched ? moment(later.schedule(schedEvent.schedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A',
}, },
'Scheduled event loaded' 'Scheduled event loaded'
); );
@ -237,7 +237,7 @@ EventSchedulerModule.prototype.startup = function(cb) {
} }
); );
// :TODO: should track watched files & stop watching @ shutdown? // :TODO: should track watched files & stop watching @ shutdown?
[ 'change', 'add', 'delete' ].forEach(event => { [ 'change', 'add', 'delete' ].forEach(event => {
watcher.on(event, (fileName, fileRoot) => { watcher.on(event, (fileName, fileRoot) => {

View File

@ -1,20 +1,20 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const paths = require('path'); const paths = require('path');
const events = require('events'); const events = require('events');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const SystemEvents = require('./system_events.js'); const SystemEvents = require('./system_events.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const async = require('async'); const async = require('async');
const glob = require('glob'); const glob = require('glob');
module.exports = new class Events extends events.EventEmitter { module.exports = new class Events extends events.EventEmitter {
constructor() { constructor() {
super(); super();
this.setMaxListeners(32); // :TODO: play with this... this.setMaxListeners(32); // :TODO: play with this...
} }
getSystemEvents() { getSystemEvents() {
@ -60,7 +60,7 @@ module.exports = new class Events extends events.EventEmitter {
const mod = require(fullModulePath); const mod = require(fullModulePath);
if(_.isFunction(mod.registerEvents)) { if(_.isFunction(mod.registerEvents)) {
// :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ? // :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ?
mod.registerEvents(this); mod.registerEvents(this);
} }
} catch(e) { } catch(e) {

View File

@ -1,83 +1,83 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const resetScreen = require('./ansi_term.js').resetScreen; const resetScreen = require('./ansi_term.js').resetScreen;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const getEnigmaUserAgent = require('./misc_util.js').getEnigmaUserAgent; const getEnigmaUserAgent = require('./misc_util.js').getEnigmaUserAgent;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const joinPath = require('path').join; const joinPath = require('path').join;
const crypto = require('crypto'); const crypto = require('crypto');
const moment = require('moment'); const moment = require('moment');
const https = require('https'); const https = require('https');
const querystring = require('querystring'); const querystring = require('querystring');
const fs = require('fs'); const fs = require('fs');
const SSHClient = require('ssh2').Client; const SSHClient = require('ssh2').Client;
/* /*
Configuration block: Configuration block:
someDoor: { someDoor: {
module: exodus module: exodus
config: { config: {
// defaults // defaults
ticketHost: oddnetwork.org ticketHost: oddnetwork.org
ticketPort: 1984 ticketPort: 1984
ticketPath: /exodus ticketPath: /exodus
rejectUnauthorized: false // set to true to allow untrusted CA's (dangerous!) rejectUnauthorized: false // set to true to allow untrusted CA's (dangerous!)
sshHost: oddnetwork.org sshHost: oddnetwork.org
sshPort: 22 sshPort: 22
sshUser: exodus sshUser: exodus
sshKeyPem: /path/to/enigma-bbs/misc/exodus.id_rsa sshKeyPem: /path/to/enigma-bbs/misc/exodus.id_rsa
// optional // optional
caPem: /path/to/cacerts.pem // see https://curl.haxx.se/docs/caextract.html caPem: /path/to/cacerts.pem // see https://curl.haxx.se/docs/caextract.html
// required // required
board: XXXX board: XXXX
key: XXXX key: XXXX
door: some_door door: some_door
} }
} }
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'Exodus', name : 'Exodus',
desc : 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/', desc : 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/',
author : 'NuSkooler', author : 'NuSkooler',
}; };
exports.getModule = class ExodusModule extends MenuModule { exports.getModule = class ExodusModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = options.menuConfig.config || {}; this.config = options.menuConfig.config || {};
this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org'; this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org';
this.config.ticketPort = this.config.ticketPort || 1984, this.config.ticketPort = this.config.ticketPort || 1984,
this.config.ticketPath = this.config.ticketPath || '/exodus'; this.config.ticketPath = this.config.ticketPath || '/exodus';
this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true); this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true);
this.config.sshHost = this.config.sshHost || this.config.ticketHost; this.config.sshHost = this.config.sshHost || this.config.ticketHost;
this.config.sshPort = this.config.sshPort || 22; this.config.sshPort = this.config.sshPort || 22;
this.config.sshUser = this.config.sshUser || 'exodus_server'; this.config.sshUser = this.config.sshUser || 'exodus_server';
this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa'); this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa');
} }
initSequence() { initSequence() {
const self = this; const self = this;
let clientTerminated = false; let clientTerminated = false;
async.waterfall( async.waterfall(
[ [
function validateConfig(callback) { function validateConfig(callback) {
// very basic validation on optionals // very basic validation on optionals
async.each( [ 'board', 'key', 'door' ], (key, next) => { async.each( [ 'board', 'key', 'door' ], (key, next) => {
return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`)); return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`));
}, callback); }, callback);
@ -92,27 +92,27 @@ exports.getModule = class ExodusModule extends MenuModule {
}); });
}, },
function getTicket(certAuthorities, callback) { function getTicket(certAuthorities, callback) {
const now = moment.utc().unix(); const now = moment.utc().unix();
const sha256 = crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex'); const sha256 = crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex');
const token = `${sha256}|${now}`; const token = `${sha256}|${now}`;
const postData = querystring.stringify({ const postData = querystring.stringify({
token : token, token : token,
board : self.config.board, board : self.config.board,
user : self.client.user.username, user : self.client.user.username,
door : self.config.door, door : self.config.door,
}); });
const reqOptions = { const reqOptions = {
hostname : self.config.ticketHost, hostname : self.config.ticketHost,
port : self.config.ticketPort, port : self.config.ticketPort,
path : self.config.ticketPath, path : self.config.ticketPath,
rejectUnauthorized : self.config.rejectUnauthorized, rejectUnauthorized : self.config.rejectUnauthorized,
method : 'POST', method : 'POST',
headers : { headers : {
'Content-Type' : 'application/x-www-form-urlencoded', 'Content-Type' : 'application/x-www-form-urlencoded',
'Content-Length' : postData.length, 'Content-Length' : postData.length,
'User-Agent' : getEnigmaUserAgent(), 'User-Agent' : getEnigmaUserAgent(),
} }
}; };
@ -165,11 +165,11 @@ exports.getModule = class ExodusModule extends MenuModule {
const sshClient = new SSHClient(); const sshClient = new SSHClient();
const window = { const window = {
rows : self.client.term.termHeight, rows : self.client.term.termHeight,
cols : self.client.term.termWidth, cols : self.client.term.termWidth,
width : 0, width : 0,
height : 0, height : 0,
term : 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :( term : 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :(
}; };
const options = { const options = {
@ -186,7 +186,7 @@ exports.getModule = class ExodusModule extends MenuModule {
}); });
sshClient.shell(window, options, (err, stream) => { sshClient.shell(window, options, (err, stream) => {
pipedStream = stream; // :TODO: ewwwwwwwww hack pipedStream = stream; // :TODO: ewwwwwwwww hack
self.client.term.output.pipe(stream); self.client.term.output.pipe(stream);
stream.on('data', d => { stream.on('data', d => {
@ -210,10 +210,10 @@ exports.getModule = class ExodusModule extends MenuModule {
}); });
sshClient.connect({ sshClient.connect({
host : self.config.sshHost, host : self.config.sshHost,
port : self.config.sshPort, port : self.config.sshPort,
username : self.config.sshUser, username : self.config.sshUser,
privateKey : privateKey, privateKey : privateKey,
}); });
} }
], ],

View File

@ -1,36 +1,36 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
const FileBaseFilters = require('./file_base_filter.js'); const FileBaseFilters = require('./file_base_filter.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
// deps // deps
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Area Filter Editor', name : 'File Area Filter Editor',
desc : 'Module for adding, deleting, and modifying file base filters', desc : 'Module for adding, deleting, and modifying file base filters',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
editor : { editor : {
searchTerms : 1, searchTerms : 1,
tags : 2, tags : 2,
area : 3, area : 3,
sort : 4, sort : 4,
order : 5, order : 5,
filterName : 6, filterName : 6,
navMenu : 7, navMenu : 7,
// :TODO: use the customs new standard thing - filter obj can have active/selected, etc. // :TODO: use the customs new standard thing - filter obj can have active/selected, etc.
selectedFilterInfo : 10, // { ...filter object ... } selectedFilterInfo : 10, // { ...filter object ... }
activeFilterInfo : 11, // { ...filter object ... } activeFilterInfo : 11, // { ...filter object ... }
error : 12, // validation errors error : 12, // validation errors
} }
}; };
@ -38,11 +38,11 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them
this.currentFilterIndex = 0; // into |filtersArray| this.currentFilterIndex = 0; // into |filtersArray|
// //
// Lexical sort + keep currently active filter (if any) as the first item in |filtersArray| // Lexical sort + keep currently active filter (if any) as the first item in |filtersArray|
// //
const activeFilter = FileBaseFilters.getActiveFilter(this.client); const activeFilter = FileBaseFilters.getActiveFilter(this.client);
this.filtersArray.sort( (filterA, filterB) => { this.filtersArray.sort( (filterA, filterB) => {
@ -87,41 +87,41 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
return cb(null); return cb(null);
}, },
newFilter : (formData, extraArgs, cb) => { newFilter : (formData, extraArgs, cb) => {
this.currentFilterIndex = this.filtersArray.length; // next avail slot this.currentFilterIndex = this.filtersArray.length; // next avail slot
this.clearForm(MciViewIds.editor.searchTerms); this.clearForm(MciViewIds.editor.searchTerms);
return cb(null); return cb(null);
}, },
deleteFilter : (formData, extraArgs, cb) => { deleteFilter : (formData, extraArgs, cb) => {
const selectedFilter = this.filtersArray[this.currentFilterIndex]; const selectedFilter = this.filtersArray[this.currentFilterIndex];
const filterUuid = selectedFilter.uuid; const filterUuid = selectedFilter.uuid;
// cannot delete built-in/system filters // cannot delete built-in/system filters
if(true === selectedFilter.system) { if(true === selectedFilter.system) {
this.showError('Cannot delete built in filters!'); this.showError('Cannot delete built in filters!');
return cb(null); return cb(null);
} }
this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry
// remove from stored properties // remove from stored properties
const filters = new FileBaseFilters(this.client); const filters = new FileBaseFilters(this.client);
filters.remove(filterUuid); filters.remove(filterUuid);
filters.persist( () => { filters.persist( () => {
// //
// If the item was also the active filter, we need to make a new one active // If the item was also the active filter, we need to make a new one active
// //
if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) { if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) {
const newActive = this.filtersArray[this.currentFilterIndex]; const newActive = this.filtersArray[this.currentFilterIndex];
if(newActive) { if(newActive) {
filters.setActive(newActive.uuid); filters.setActive(newActive.uuid);
} else { } else {
// nothing to set active to // nothing to set active to
this.client.user.removeProperty('file_base_filter_active_uuid'); this.client.user.removeProperty('file_base_filter_active_uuid');
} }
} }
// update UI // update UI
this.updateActiveLabel(); this.updateActiveLabel();
if(this.filtersArray.length > 0) { if(this.filtersArray.length > 0) {
@ -140,7 +140,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
if(errorView) { if(errorView) {
if(err) { if(err) {
errorView.setText(err.message); errorView.setText(err.message);
err.view.clearText(); // clear out the invalid data err.view.clearText(); // clear out the invalid data
} else { } else {
errorView.clearText(); errorView.clearText();
} }
@ -168,8 +168,8 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) ); const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) );
async.series( async.series(
[ [
@ -241,7 +241,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
getSelectedAreaTag(index) { getSelectedAreaTag(index) {
if(0 === index) { if(0 === index) {
return ''; // -ALL- return ''; // -ALL-
} }
const area = this.availAreas[index]; const area = this.availAreas[index];
if(!area) { if(!area) {
@ -258,7 +258,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
let index; let index;
const filter = this.getCurrentFilter(); const filter = this.getCurrentFilter();
if(filter) { if(filter) {
// special treatment: areaTag saved as blank ("") if -ALL- // special treatment: areaTag saved as blank ("") if -ALL-
index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0; index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0;
} else { } else {
index = 0; index = 0;
@ -293,31 +293,31 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
} }
setFilterValuesFromFormData(filter, formData) { setFilterValuesFromFormData(filter, formData) {
filter.name = formData.value.name; filter.name = formData.value.name;
filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex); filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex);
filter.terms = formData.value.searchTerms; filter.terms = formData.value.searchTerms;
filter.tags = formData.value.tags; filter.tags = formData.value.tags;
filter.order = this.getOrderBy(formData.value.orderByIndex); filter.order = this.getOrderBy(formData.value.orderByIndex);
filter.sort = this.getSortBy(formData.value.sortByIndex); filter.sort = this.getSortBy(formData.value.sortByIndex);
} }
saveCurrentFilter(formData, cb) { saveCurrentFilter(formData, cb) {
const filters = new FileBaseFilters(this.client); const filters = new FileBaseFilters(this.client);
const selectedFilter = this.filtersArray[this.currentFilterIndex]; const selectedFilter = this.filtersArray[this.currentFilterIndex];
if(selectedFilter) { if(selectedFilter) {
// *update* currently selected filter // *update* currently selected filter
this.setFilterValuesFromFormData(selectedFilter, formData); this.setFilterValuesFromFormData(selectedFilter, formData);
filters.replace(selectedFilter.uuid, selectedFilter); filters.replace(selectedFilter.uuid, selectedFilter);
} else { } else {
// add a new entry; note that UUID will be generated // add a new entry; note that UUID will be generated
const newFilter = {}; const newFilter = {};
this.setFilterValuesFromFormData(newFilter, formData); this.setFilterValuesFromFormData(newFilter, formData);
// set current to what we just saved // set current to what we just saved
newFilter.uuid = filters.add(newFilter); newFilter.uuid = filters.add(newFilter);
// add to our array (at current index position) // add to our array (at current index position)
this.filtersArray[this.currentFilterIndex] = newFilter; this.filtersArray[this.currentFilterIndex] = newFilter;
} }
@ -327,9 +327,9 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
loadDataForFilter(filterIndex) { loadDataForFilter(filterIndex) {
const filter = this.filtersArray[filterIndex]; const filter = this.filtersArray[filterIndex];
if(filter) { if(filter) {
this.setText(MciViewIds.editor.searchTerms, filter.terms); this.setText(MciViewIds.editor.searchTerms, filter.terms);
this.setText(MciViewIds.editor.tags, filter.tags); this.setText(MciViewIds.editor.tags, filter.tags);
this.setText(MciViewIds.editor.filterName, filter.name); this.setText(MciViewIds.editor.filterName, filter.name);
this.setAreaIndexFromCurrentFilter(); this.setAreaIndexFromCurrentFilter();
this.setSortByFromCurrentFilter(); this.setSortByFromCurrentFilter();

View File

@ -1,71 +1,71 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const theme = require('./theme.js'); const theme = require('./theme.js');
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const FileArea = require('./file_base_area.js'); const FileArea = require('./file_base_area.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
const ArchiveUtil = require('./archive_util.js'); const ArchiveUtil = require('./archive_util.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const DownloadQueue = require('./download_queue.js'); const DownloadQueue = require('./download_queue.js');
const FileAreaWeb = require('./file_area_web.js'); const FileAreaWeb = require('./file_area_web.js');
const FileBaseFilters = require('./file_base_filter.js'); const FileBaseFilters = require('./file_base_filter.js');
const resolveMimeType = require('./mime_util.js').resolveMimeType; const resolveMimeType = require('./mime_util.js').resolveMimeType;
const isAnsi = require('./string_util.js').isAnsi; const isAnsi = require('./string_util.js').isAnsi;
const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi; const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
const paths = require('path'); const paths = require('path');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Area List', name : 'File Area List',
desc : 'Lists contents of file an file area', desc : 'Lists contents of file an file area',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const FormIds = { const FormIds = {
browse : 0, browse : 0,
details : 1, details : 1,
detailsGeneral : 2, detailsGeneral : 2,
detailsNfo : 3, detailsNfo : 3,
detailsFileList : 4, detailsFileList : 4,
}; };
const MciViewIds = { const MciViewIds = {
browse : { browse : {
desc : 1, desc : 1,
navMenu : 2, navMenu : 2,
customRangeStart : 10, // 10+ = customs customRangeStart : 10, // 10+ = customs
}, },
details : { details : {
navMenu : 1, navMenu : 1,
infoXyTop : 2, // %XY starting position for info area infoXyTop : 2, // %XY starting position for info area
infoXyBottom : 3, infoXyBottom : 3,
customRangeStart : 10, // 10+ = customs customRangeStart : 10, // 10+ = customs
}, },
detailsGeneral : { detailsGeneral : {
customRangeStart : 10, // 10+ = customs customRangeStart : 10, // 10+ = customs
}, },
detailsNfo : { detailsNfo : {
nfo : 1, nfo : 1,
customRangeStart : 10, // 10+ = customs customRangeStart : 10, // 10+ = customs
}, },
detailsFileList : { detailsFileList : {
fileList : 1, fileList : 1,
customRangeStart : 10, // 10+ = customs customRangeStart : 10, // 10+ = customs
}, },
}; };
@ -74,12 +74,12 @@ exports.getModule = class FileAreaList extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.filterCriteria = _.get(options, 'extraArgs.filterCriteria'); this.filterCriteria = _.get(options, 'extraArgs.filterCriteria');
this.fileList = _.get(options, 'extraArgs.fileList'); this.fileList = _.get(options, 'extraArgs.fileList');
this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true); this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true);
if(this.fileList) { if(this.fileList) {
// we'll need to adjust position as well! // we'll need to adjust position as well!
this.fileListPosition = 0; this.fileListPosition = 0;
} }
@ -102,7 +102,7 @@ exports.getModule = class FileAreaList extends MenuModule {
if(this.fileListPosition + 1 < this.fileList.length) { if(this.fileListPosition + 1 < this.fileList.length) {
this.fileListPosition += 1; this.fileListPosition += 1;
return this.displayBrowsePage(true, cb); // true=clerarScreen return this.displayBrowsePage(true, cb); // true=clerarScreen
} }
if(this.lastFileNextExit) { if(this.lastFileNextExit) {
@ -115,7 +115,7 @@ exports.getModule = class FileAreaList extends MenuModule {
if(this.fileListPosition > 0) { if(this.fileListPosition > 0) {
--this.fileListPosition; --this.fileListPosition;
return this.displayBrowsePage(true, cb); // true=clearScreen return this.displayBrowsePage(true, cb); // true=clearScreen
} }
return cb(null); return cb(null);
@ -132,7 +132,7 @@ exports.getModule = class FileAreaList extends MenuModule {
} }
}); });
return this.displayBrowsePage(true, cb); // true=clearScreen return this.displayBrowsePage(true, cb); // true=clearScreen
}, },
toggleQueue : (formData, extraArgs, cb) => { toggleQueue : (formData, extraArgs, cb) => {
this.dlQueue.toggle(this.currentFileEntry); this.dlQueue.toggle(this.currentFileEntry);
@ -158,15 +158,15 @@ exports.getModule = class FileAreaList extends MenuModule {
getSaveState() { getSaveState() {
return { return {
fileList : this.fileList, fileList : this.fileList,
fileListPosition : this.fileListPosition, fileListPosition : this.fileListPosition,
}; };
} }
restoreSavedState(savedState) { restoreSavedState(savedState) {
if(savedState) { if(savedState) {
this.fileList = savedState.fileList; this.fileList = savedState.fileList;
this.fileListPosition = savedState.fileListPosition; this.fileListPosition = savedState.fileListPosition;
} }
} }
@ -215,35 +215,35 @@ exports.getModule = class FileAreaList extends MenuModule {
} }
populateCurrentEntryInfo(cb) { populateCurrentEntryInfo(cb) {
const config = this.menuConfig.config; const config = this.menuConfig.config;
const currEntry = this.currentFileEntry; const currEntry = this.currentFileEntry;
const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD'; const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD';
const area = FileArea.getFileAreaByTag(currEntry.areaTag); const area = FileArea.getFileAreaByTag(currEntry.areaTag);
const hashTagsSep = config.hashTagsSep || ', '; const hashTagsSep = config.hashTagsSep || ', ';
const isQueuedIndicator = config.isQueuedIndicator || 'Y'; const isQueuedIndicator = config.isQueuedIndicator || 'Y';
const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N'; const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N';
const entryInfo = currEntry.entryInfo = { const entryInfo = currEntry.entryInfo = {
fileId : currEntry.fileId, fileId : currEntry.fileId,
areaTag : currEntry.areaTag, areaTag : currEntry.areaTag,
areaName : _.get(area, 'name') || 'N/A', areaName : _.get(area, 'name') || 'N/A',
areaDesc : _.get(area, 'desc') || 'N/A', areaDesc : _.get(area, 'desc') || 'N/A',
fileSha256 : currEntry.fileSha256, fileSha256 : currEntry.fileSha256,
fileName : currEntry.fileName, fileName : currEntry.fileName,
desc : currEntry.desc || '', desc : currEntry.desc || '',
descLong : currEntry.descLong || '', descLong : currEntry.descLong || '',
userRating : currEntry.userRating, userRating : currEntry.userRating,
uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat), uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat),
hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), hashTags : Array.from(currEntry.hashTags).join(hashTagsSep),
isQueued : this.dlQueue.isQueued(currEntry) ? isQueuedIndicator : isNotQueuedIndicator, isQueued : this.dlQueue.isQueued(currEntry) ? isQueuedIndicator : isNotQueuedIndicator,
webDlLink : '', // :TODO: fetch web any existing web d/l link webDlLink : '', // :TODO: fetch web any existing web d/l link
webDlExpire : '', // :TODO: fetch web d/l link expire time webDlExpire : '', // :TODO: fetch web d/l link expire time
}; };
// //
// We need the entry object to contain meta keys even if they are empty as // We need the entry object to contain meta keys even if they are empty as
// consumers may very likely attempt to use them // consumers may very likely attempt to use them
// //
const metaValues = FileEntry.WellKnownMetaValues; const metaValues = FileEntry.WellKnownMetaValues;
metaValues.forEach(name => { metaValues.forEach(name => {
@ -258,7 +258,7 @@ exports.getModule = class FileAreaList extends MenuModule {
let fileType = _.get(Config(), [ 'fileTypes', mimeType ] ); let fileType = _.get(Config(), [ 'fileTypes', mimeType ] );
if(Array.isArray(fileType)) { if(Array.isArray(fileType)) {
// further refine by extention // further refine by extention
fileType = fileType.find(ft => paths.extname(currEntry.fileName) === ft.ext); fileType = fileType.find(ft => paths.extname(currEntry.fileName) === ft.ext);
} }
desc = fileType && fileType.desc; desc = fileType && fileType.desc;
@ -268,31 +268,31 @@ exports.getModule = class FileAreaList extends MenuModule {
entryInfo.archiveTypeDesc = 'N/A'; entryInfo.archiveTypeDesc = 'N/A';
} }
entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported
entryInfo.hashTags = entryInfo.hashTags || '(none)'; entryInfo.hashTags = entryInfo.hashTags || '(none)';
// create a rating string, e.g. "**---" // create a rating string, e.g. "**---"
const userRatingTicked = config.userRatingTicked || '*'; const userRatingTicked = config.userRatingTicked || '*';
const userRatingUnticked = config.userRatingUnticked || ''; const userRatingUnticked = config.userRatingUnticked || '';
entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe! entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe!
entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating); entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating);
if(entryInfo.userRating < 5) { if(entryInfo.userRating < 5) {
entryInfo.userRatingString += userRatingUnticked.repeat( (5 - entryInfo.userRating) ); entryInfo.userRatingString += userRatingUnticked.repeat( (5 - entryInfo.userRating) );
} }
FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => { FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => {
if(err) { if(err) {
entryInfo.webDlExpire = ''; entryInfo.webDlExpire = '';
if(ErrNotEnabled === err.reasonCode) { if(ErrNotEnabled === err.reasonCode) {
entryInfo.webDlExpire = config.webDlLinkNoWebserver || 'Web server is not enabled'; entryInfo.webDlExpire = config.webDlLinkNoWebserver || 'Web server is not enabled';
} else { } else {
entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated'; entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated';
} }
} else { } else {
const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
} }
return cb(null); return cb(null);
@ -304,8 +304,8 @@ exports.getModule = class FileAreaList extends MenuModule {
} }
displayArtAndPrepViewController(name, options, cb) { displayArtAndPrepViewController(name, options, cb) {
const self = this; const self = this;
const config = this.menuConfig.config; const config = this.menuConfig.config;
async.waterfall( async.waterfall(
[ [
@ -326,8 +326,8 @@ exports.getModule = class FileAreaList extends MenuModule {
function prepeareViewController(artData, callback) { function prepeareViewController(artData, callback) {
if(_.isUndefined(self.viewControllers[name])) { if(_.isUndefined(self.viewControllers[name])) {
const vcOpts = { const vcOpts = {
client : self.client, client : self.client,
formId : FormIds[name], formId : FormIds[name],
}; };
if(!_.isUndefined(options.noInput)) { if(!_.isUndefined(options.noInput)) {
@ -339,8 +339,8 @@ exports.getModule = class FileAreaList extends MenuModule {
if('details' === name) { if('details' === name) {
try { try {
self.detailsInfoArea = { self.detailsInfoArea = {
top : artData.mciMap.XY2.position, top : artData.mciMap.XY2.position,
bottom : artData.mciMap.XY3.position, bottom : artData.mciMap.XY3.position,
}; };
} catch(e) { } catch(e) {
return callback(Errors.DoesNotExist('Missing XY2 and XY3 position indicators!')); return callback(Errors.DoesNotExist('Missing XY2 and XY3 position indicators!'));
@ -348,9 +348,9 @@ exports.getModule = class FileAreaList extends MenuModule {
} }
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds[name], formId : FormIds[name],
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -368,7 +368,7 @@ exports.getModule = class FileAreaList extends MenuModule {
} }
displayBrowsePage(clearScreen, cb) { displayBrowsePage(clearScreen, cb) {
const self = this; const self = this;
async.series( async.series(
[ [
@ -376,7 +376,7 @@ exports.getModule = class FileAreaList extends MenuModule {
if(self.fileList) { if(self.fileList) {
return callback(null); return callback(null);
} }
return self.loadFileIds(false, callback); // false=do not force return self.loadFileIds(false, callback); // false=do not force
}, },
function checkEmptyResults(callback) { function checkEmptyResults(callback) {
if(0 === self.fileList.length) { if(0 === self.fileList.length) {
@ -403,21 +403,21 @@ exports.getModule = class FileAreaList extends MenuModule {
const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc);
if(descView) { if(descView) {
// //
// For descriptions we want to support as many color code systems // For descriptions we want to support as many color code systems
// as we can for coverage of what is found in the while (e.g. Renegade // as we can for coverage of what is found in the while (e.g. Renegade
// pipes, PCB @X##, etc.) // pipes, PCB @X##, etc.)
// //
// MLTEV doesn't support all of this, so convert. If we produced ANSI // MLTEV doesn't support all of this, so convert. If we produced ANSI
// esc sequences, we'll proceed with specialization, else just treat // esc sequences, we'll proceed with specialization, else just treat
// it as text. // it as text.
// //
const desc = controlCodesToAnsi(self.currentFileEntry.desc); const desc = controlCodesToAnsi(self.currentFileEntry.desc);
if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) { if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) {
descView.setAnsi( descView.setAnsi(
desc, desc,
{ {
prepped : false, prepped : false,
forceLineTerm : true forceLineTerm : true
}, },
() => { () => {
return callback(null); return callback(null);
@ -447,7 +447,7 @@ exports.getModule = class FileAreaList extends MenuModule {
} }
displayDetailsPage(cb) { displayDetailsPage(cb) {
const self = this; const self = this;
async.series( async.series(
[ [
@ -467,9 +467,9 @@ exports.getModule = class FileAreaList extends MenuModule {
navMenu.on('index update', index => { navMenu.on('index update', index => {
const sectionName = { const sectionName = {
0 : 'general', 0 : 'general',
1 : 'nfo', 1 : 'nfo',
2 : 'fileList', 2 : 'fileList',
}[index]; }[index];
if(sectionName) { if(sectionName) {
@ -524,8 +524,8 @@ exports.getModule = class FileAreaList extends MenuModule {
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
self.currentFileEntry.entryInfo.webDlLink = ansi.vtxHyperlink(self.client, url) + url; self.currentFileEntry.entryInfo.webDlLink = ansi.vtxHyperlink(self.client, url) + url;
self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat); self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat);
return callback(null); return callback(null);
} }
@ -547,8 +547,8 @@ exports.getModule = class FileAreaList extends MenuModule {
} }
updateQueueIndicator() { updateQueueIndicator() {
const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y'; const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y';
const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N'; const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N';
this.currentFileEntry.entryInfo.isQueued = stringFormat( this.currentFileEntry.entryInfo.isQueued = stringFormat(
this.dlQueue.isQueued(this.currentFileEntry) ? this.dlQueue.isQueued(this.currentFileEntry) ?
@ -565,7 +565,7 @@ exports.getModule = class FileAreaList extends MenuModule {
} }
cacheArchiveEntries(cb) { cacheArchiveEntries(cb) {
// check cache // check cache
if(this.currentFileEntry.archiveEntries) { if(this.currentFileEntry.archiveEntries) {
return cb(null, 'cache'); return cb(null, 'cache');
} }
@ -575,8 +575,8 @@ exports.getModule = class FileAreaList extends MenuModule {
return cb(Errors.Invalid('Invalid area tag')); return cb(Errors.Invalid('Invalid area tag'));
} }
const filePath = this.currentFileEntry.filePath; const filePath = this.currentFileEntry.filePath;
const archiveUtil = ArchiveUtil.getInstance(); const archiveUtil = ArchiveUtil.getInstance();
archiveUtil.listEntries(filePath, this.currentFileEntry.entryInfo.archiveType, (err, entries) => { archiveUtil.listEntries(filePath, this.currentFileEntry.entryInfo.archiveType, (err, entries) => {
if(err) { if(err) {
@ -594,14 +594,14 @@ exports.getModule = class FileAreaList extends MenuModule {
if(this.currentFileEntry.entryInfo.archiveType) { if(this.currentFileEntry.entryInfo.archiveType) {
this.cacheArchiveEntries( (err, cacheStatus) => { this.cacheArchiveEntries( (err, cacheStatus) => {
if(err) { if(err) {
// :TODO: Handle me!!! // :TODO: Handle me!!!
fileListView.setItems( [ 'Failed getting file listing' ] ); // :TODO: make this not suck fileListView.setItems( [ 'Failed getting file listing' ] ); // :TODO: make this not suck
return; return;
} }
if('re-cached' === cacheStatus) { if('re-cached' === cacheStatus) {
const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here? const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here?
const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat; const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat;
fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) ); fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) );
fileListView.setFocusItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(focusFileListEntryFormat, entry) ) ); fileListView.setFocusItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(focusFileListEntryFormat, entry) ) );
@ -615,8 +615,8 @@ exports.getModule = class FileAreaList extends MenuModule {
} }
displayDetailsSection(sectionName, clearArea, cb) { displayDetailsSection(sectionName, clearArea, cb) {
const self = this; const self = this;
const name = `details${_.upperFirst(sectionName)}`; const name = `details${_.upperFirst(sectionName)}`;
async.series( async.series(
[ [
@ -637,8 +637,8 @@ exports.getModule = class FileAreaList extends MenuModule {
if(clearArea) { if(clearArea) {
self.client.term.rawWrite(ansi.reset()); self.client.term.rawWrite(ansi.reset());
let pos = self.detailsInfoArea.top[0]; let pos = self.detailsInfoArea.top[0];
const bottom = self.detailsInfoArea.bottom[0]; const bottom = self.detailsInfoArea.bottom[0];
while(pos++ <= bottom) { while(pos++ <= bottom) {
self.client.term.rawWrite(ansi.eraseLine() + ansi.down()); self.client.term.rawWrite(ansi.eraseLine() + ansi.down());
@ -664,8 +664,8 @@ exports.getModule = class FileAreaList extends MenuModule {
nfoView.setAnsi( nfoView.setAnsi(
self.currentFileEntry.entryInfo.descLong, self.currentFileEntry.entryInfo.descLong,
{ {
prepped : false, prepped : false,
forceLineTerm : true, forceLineTerm : true,
}, },
() => { () => {
return callback(null); return callback(null);
@ -701,7 +701,7 @@ exports.getModule = class FileAreaList extends MenuModule {
loadFileIds(force, cb) { loadFileIds(force, cb) {
if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) { if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) {
this.fileListPosition = 0; this.fileListPosition = 0;
const filterCriteria = Object.assign({}, this.filterCriteria); const filterCriteria = Object.assign({}, this.filterCriteria);
if(!filterCriteria.areaTag) { if(!filterCriteria.areaTag) {

View File

@ -1,29 +1,29 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const FileDb = require('./database.js').dbs.file; const FileDb = require('./database.js').dbs.file;
const getISOTimestampString = require('./database.js').getISOTimestampString; const getISOTimestampString = require('./database.js').getISOTimestampString;
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const getServer = require('./listening_server.js').getServer; const getServer = require('./listening_server.js').getServer;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const User = require('./user.js'); const User = require('./user.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId; const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId;
const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName;
const Events = require('./events.js'); const Events = require('./events.js');
// deps // deps
const hashids = require('hashids'); const hashids = require('hashids');
const moment = require('moment'); const moment = require('moment');
const paths = require('path'); const paths = require('path');
const async = require('async'); const async = require('async');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const mimeTypes = require('mime-types'); const mimeTypes = require('mime-types');
const yazl = require('yazl'); const yazl = require('yazl');
function notEnabledError() { function notEnabledError() {
return Errors.General('Web server is not enabled', ErrNotEnabled); return Errors.General('Web server is not enabled', ErrNotEnabled);
@ -31,8 +31,8 @@ function notEnabledError() {
class FileAreaWebAccess { class FileAreaWebAccess {
constructor() { constructor() {
this.hashids = new hashids(Config().general.boardName); this.hashids = new hashids(Config().general.boardName);
this.expireTimers = {}; // hashId->timer this.expireTimers = {}; // hashId->timer
} }
startup(cb) { startup(cb) {
@ -51,13 +51,13 @@ class FileAreaWebAccess {
if(self.isEnabled()) { if(self.isEnabled()) {
const routeAdded = self.webServer.instance.addRoute({ const routeAdded = self.webServer.instance.addRoute({
method : 'GET', method : 'GET',
path : Config().fileBase.web.routePath, path : Config().fileBase.web.routePath,
handler : self.routeWebRequest.bind(self), handler : self.routeWebRequest.bind(self),
}); });
return callback(routeAdded ? null : Errors.General('Failed adding route')); return callback(routeAdded ? null : Errors.General('Failed adding route'));
} else { } else {
return callback(null); // not enabled, but no error return callback(null); // not enabled, but no error
} }
} }
], ],
@ -77,18 +77,18 @@ class FileAreaWebAccess {
static getHashIdTypes() { static getHashIdTypes() {
return { return {
SingleFile : 0, SingleFile : 0,
BatchArchive : 1, BatchArchive : 1,
}; };
} }
load(cb) { load(cb) {
// //
// Load entries, register expiration timers // Load entries, register expiration timers
// //
FileDb.each( FileDb.each(
`SELECT hash_id, expire_timestamp `SELECT hash_id, expire_timestamp
FROM file_web_serve;`, FROM file_web_serve;`,
(err, row) => { (err, row) => {
if(row) { if(row) {
this.scheduleExpire(row.hash_id, moment(row.expire_timestamp)); this.scheduleExpire(row.hash_id, moment(row.expire_timestamp));
@ -102,11 +102,11 @@ class FileAreaWebAccess {
removeEntry(hashId) { removeEntry(hashId) {
// //
// Delete record from DB, and our timer // Delete record from DB, and our timer
// //
FileDb.run( FileDb.run(
`DELETE FROM file_web_serve `DELETE FROM file_web_serve
WHERE hash_id = ?;`, WHERE hash_id = ?;`,
[ hashId ] [ hashId ]
); );
@ -115,7 +115,7 @@ class FileAreaWebAccess {
scheduleExpire(hashId, expireTime) { scheduleExpire(hashId, expireTime) {
// remove any previous entry for this hashId // remove any previous entry for this hashId
const previous = this.expireTimers[hashId]; const previous = this.expireTimers[hashId];
if(previous) { if(previous) {
clearTimeout(previous); clearTimeout(previous);
@ -138,8 +138,8 @@ class FileAreaWebAccess {
loadServedHashId(hashId, cb) { loadServedHashId(hashId, cb) {
FileDb.get( FileDb.get(
`SELECT expire_timestamp FROM `SELECT expire_timestamp FROM
file_web_serve file_web_serve
WHERE hash_id = ?`, WHERE hash_id = ?`,
[ hashId ], [ hashId ],
(err, result) => { (err, result) => {
if(err || !result) { if(err || !result) {
@ -148,16 +148,16 @@ class FileAreaWebAccess {
const decoded = this.hashids.decode(hashId); const decoded = this.hashids.decode(hashId);
// decode() should provide an array of [ userId, hashIdType, id, ... ] // decode() should provide an array of [ userId, hashIdType, id, ... ]
if(!Array.isArray(decoded) || decoded.length < 3) { if(!Array.isArray(decoded) || decoded.length < 3) {
return cb(Errors.Invalid('Invalid or unknown hash ID')); return cb(Errors.Invalid('Invalid or unknown hash ID'));
} }
const servedItem = { const servedItem = {
hashId : hashId, hashId : hashId,
userId : decoded[0], userId : decoded[0],
hashIdType : decoded[1], hashIdType : decoded[1],
expireTimestamp : moment(result.expire_timestamp), expireTimestamp : moment(result.expire_timestamp),
}; };
if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) { if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) {
@ -209,10 +209,10 @@ class FileAreaWebAccess {
} }
_addOrUpdateHashIdRecord(dbOrTrans, hashId, expireTime, cb) { _addOrUpdateHashIdRecord(dbOrTrans, hashId, expireTime, cb) {
// add/update rec with hash id and (latest) timestamp // add/update rec with hash id and (latest) timestamp
dbOrTrans.run( dbOrTrans.run(
`REPLACE INTO file_web_serve (hash_id, expire_timestamp) `REPLACE INTO file_web_serve (hash_id, expire_timestamp)
VALUES (?, ?);`, VALUES (?, ?);`,
[ hashId, getISOTimestampString(expireTime) ], [ hashId, getISOTimestampString(expireTime) ],
err => { err => {
if(err) { if(err) {
@ -231,9 +231,9 @@ class FileAreaWebAccess {
return cb(notEnabledError()); return cb(notEnabledError());
} }
const hashId = this.getSingleFileHashId(client, fileEntry); const hashId = this.getSingleFileHashId(client, fileEntry);
const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId); const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId);
options.expireTime = options.expireTime || moment().add(2, 'days'); options.expireTime = options.expireTime || moment().add(2, 'days');
this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => { this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => {
return cb(err, url); return cb(err, url);
@ -245,10 +245,10 @@ class FileAreaWebAccess {
return cb(notEnabledError()); return cb(notEnabledError());
} }
const batchId = moment().utc().unix(); const batchId = moment().utc().unix();
const hashId = this.getBatchArchiveHashId(client, batchId); const hashId = this.getBatchArchiveHashId(client, batchId);
const url = this.buildBatchArchiveTempDownloadLink(client, hashId); const url = this.buildBatchArchiveTempDownloadLink(client, hashId);
options.expireTime = options.expireTime || moment().add(2, 'days'); options.expireTime = options.expireTime || moment().add(2, 'days');
FileDb.beginTransaction( (err, trans) => { FileDb.beginTransaction( (err, trans) => {
if(err) { if(err) {
@ -265,7 +265,7 @@ class FileAreaWebAccess {
async.eachSeries(fileEntries, (entry, nextEntry) => { async.eachSeries(fileEntries, (entry, nextEntry) => {
trans.run( trans.run(
`INSERT INTO file_web_serve_batch (hash_id, file_id) `INSERT INTO file_web_serve_batch (hash_id, file_id)
VALUES (?, ?);`, VALUES (?, ?);`,
[ hashId, entry.fileId ], [ hashId, entry.fileId ],
err => { err => {
return nextEntry(err); return nextEntry(err);
@ -332,19 +332,19 @@ class FileAreaWebAccess {
} }
resp.on('close', () => { resp.on('close', () => {
// connection closed *before* the response was fully sent // connection closed *before* the response was fully sent
// :TODO: Log and such // :TODO: Log and such
}); });
resp.on('finish', () => { resp.on('finish', () => {
// transfer completed fully // transfer completed fully
this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]); this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]);
}); });
const headers = { const headers = {
'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'), 'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'),
'Content-Length' : stats.size, 'Content-Length' : stats.size,
'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`, 'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`,
}; };
const readStream = fs.createReadStream(filePath); const readStream = fs.createReadStream(filePath);
@ -358,10 +358,10 @@ class FileAreaWebAccess {
Log.debug( { servedItem : servedItem }, 'Batch file web request'); Log.debug( { servedItem : servedItem }, 'Batch file web request');
// //
// We are going to build an on-the-fly zip file stream of 1:n // We are going to build an on-the-fly zip file stream of 1:n
// files in the batch. // files in the batch.
// //
// First, collect all file IDs // First, collect all file IDs
// //
const self = this; const self = this;
@ -370,8 +370,8 @@ class FileAreaWebAccess {
function fetchFileIds(callback) { function fetchFileIds(callback) {
FileDb.all( FileDb.all(
`SELECT file_id `SELECT file_id
FROM file_web_serve_batch FROM file_web_serve_batch
WHERE hash_id = ?;`, WHERE hash_id = ?;`,
[ servedItem.hashId ], [ servedItem.hashId ],
(err, fileIdRows) => { (err, fileIdRows) => {
if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) { if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) {
@ -408,10 +408,10 @@ class FileAreaWebAccess {
filePaths.forEach(fp => { filePaths.forEach(fp => {
zipFile.addFile( zipFile.addFile(
fp, // path to physical file fp, // path to physical file
paths.basename(fp), // filename/path *stored in archive* paths.basename(fp), // filename/path *stored in archive*
{ {
compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us. compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us.
} }
); );
}); });
@ -422,21 +422,21 @@ class FileAreaWebAccess {
} }
resp.on('close', () => { resp.on('close', () => {
// connection closed *before* the response was fully sent // connection closed *before* the response was fully sent
// :TODO: Log and such // :TODO: Log and such
}); });
resp.on('finish', () => { resp.on('finish', () => {
// transfer completed fully // transfer completed fully
self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries); self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries);
}); });
const batchFileName = `batch_${servedItem.hashId}.zip`; const batchFileName = `batch_${servedItem.hashId}.zip`;
const headers = { const headers = {
'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'), 'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'),
'Content-Length' : finalZipSize, 'Content-Length' : finalZipSize,
'Content-Disposition' : `attachment; filename="${batchFileName}"`, 'Content-Disposition' : `attachment; filename="${batchFileName}"`,
}; };
resp.writeHead(200, headers); resp.writeHead(200, headers);
@ -446,11 +446,11 @@ class FileAreaWebAccess {
], ],
err => { err => {
if(err) { if(err) {
// :TODO: Log me! // :TODO: Log me!
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
// ...otherwise, we would have called resp() already. // ...otherwise, we would have called resp() already.
} }
); );
} }
@ -464,7 +464,7 @@ class FileAreaWebAccess {
return callback(null, clientForUserId.user); return callback(null, clientForUserId.user);
} }
// not online now - look 'em up // not online now - look 'em up
User.getUser(userId, (err, assocUser) => { User.getUser(userId, (err, assocUser) => {
return callback(err, assocUser); return callback(err, assocUser);
}); });
@ -481,8 +481,8 @@ class FileAreaWebAccess {
Events.emit( Events.emit(
Events.getSystemEvents().UserDownload, Events.getSystemEvents().UserDownload,
{ {
user : user, user : user,
files : fileEntries, files : fileEntries,
} }
); );
return callback(null); return callback(null);

View File

@ -1,57 +1,57 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs;
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const FileDb = require('./database.js').dbs.file; const FileDb = require('./database.js').dbs.file;
const ArchiveUtil = require('./archive_util.js'); const ArchiveUtil = require('./archive_util.js');
const CRC32 = require('./crc.js').CRC32; const CRC32 = require('./crc.js').CRC32;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const resolveMimeType = require('./mime_util.js').resolveMimeType; const resolveMimeType = require('./mime_util.js').resolveMimeType;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const wordWrapText = require('./word_wrap.js').wordWrapText; const wordWrapText = require('./word_wrap.js').wordWrapText;
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const async = require('async'); const async = require('async');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const crypto = require('crypto'); const crypto = require('crypto');
const paths = require('path'); const paths = require('path');
const temptmp = require('temptmp').createTrackedSession('file_area'); const temptmp = require('temptmp').createTrackedSession('file_area');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const execFile = require('child_process').execFile; const execFile = require('child_process').execFile;
const moment = require('moment'); const moment = require('moment');
exports.startup = startup; exports.startup = startup;
exports.isInternalArea = isInternalArea; exports.isInternalArea = isInternalArea;
exports.getAvailableFileAreas = getAvailableFileAreas; exports.getAvailableFileAreas = getAvailableFileAreas;
exports.getAvailableFileAreaTags = getAvailableFileAreaTags; exports.getAvailableFileAreaTags = getAvailableFileAreaTags;
exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas;
exports.isValidStorageTag = isValidStorageTag; exports.isValidStorageTag = isValidStorageTag;
exports.getAreaStorageDirectoryByTag = getAreaStorageDirectoryByTag; exports.getAreaStorageDirectoryByTag = getAreaStorageDirectoryByTag;
exports.getAreaDefaultStorageDirectory = getAreaDefaultStorageDirectory; exports.getAreaDefaultStorageDirectory = getAreaDefaultStorageDirectory;
exports.getAreaStorageLocations = getAreaStorageLocations; exports.getAreaStorageLocations = getAreaStorageLocations;
exports.getDefaultFileAreaTag = getDefaultFileAreaTag; exports.getDefaultFileAreaTag = getDefaultFileAreaTag;
exports.getFileAreaByTag = getFileAreaByTag; exports.getFileAreaByTag = getFileAreaByTag;
exports.getFileEntryPath = getFileEntryPath; exports.getFileEntryPath = getFileEntryPath;
exports.changeFileAreaWithOptions = changeFileAreaWithOptions; exports.changeFileAreaWithOptions = changeFileAreaWithOptions;
exports.scanFile = scanFile; exports.scanFile = scanFile;
exports.scanFileAreaForChanges = scanFileAreaForChanges; exports.scanFileAreaForChanges = scanFileAreaForChanges;
exports.getDescFromFileName = getDescFromFileName; exports.getDescFromFileName = getDescFromFileName;
exports.getAreaStats = getAreaStats; exports.getAreaStats = getAreaStats;
exports.cleanUpTempSessionItems = cleanUpTempSessionItems; exports.cleanUpTempSessionItems = cleanUpTempSessionItems;
// for scheduler: // for scheduler:
exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent; exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent;
const WellKnownAreaTags = exports.WellKnownAreaTags = { const WellKnownAreaTags = exports.WellKnownAreaTags = {
Invalid : '', Invalid : '',
MessageAreaAttach : 'system_message_attachment', MessageAreaAttach : 'system_message_attachment',
TempDownloads : 'system_temporary_download', TempDownloads : 'system_temporary_download',
}; };
function startup(cb) { function startup(cb) {
@ -65,7 +65,7 @@ function isInternalArea(areaTag) {
function getAvailableFileAreas(client, options) { function getAvailableFileAreas(client, options) {
options = options || { }; options = options || { };
// perform ACS check per conf & omit internal if desired // perform ACS check per conf & omit internal if desired
const allAreas = _.map(Config().fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } )); const allAreas = _.map(Config().fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } ));
return _.omitBy(allAreas, areaInfo => { return _.omitBy(allAreas, areaInfo => {
@ -74,11 +74,11 @@ function getAvailableFileAreas(client, options) {
} }
if(options.skipAcsCheck) { if(options.skipAcsCheck) {
return false; // no ACS checks (below) return false; // no ACS checks (below)
} }
if(options.writeAcs && !client.acs.hasFileAreaWrite(areaInfo)) { if(options.writeAcs && !client.acs.hasFileAreaWrite(areaInfo)) {
return true; // omit return true; // omit
} }
return !client.acs.hasFileAreaRead(areaInfo); return !client.acs.hasFileAreaRead(areaInfo);
@ -116,8 +116,8 @@ function getDefaultFileAreaTag(client, disableAcsCheck) {
function getFileAreaByTag(areaTag) { function getFileAreaByTag(areaTag) {
const areaInfo = Config().fileBase.areas[areaTag]; const areaInfo = Config().fileBase.areas[areaTag];
if(areaInfo) { if(areaInfo) {
areaInfo.areaTag = areaTag; // convienence! areaInfo.areaTag = areaTag; // convienence!
areaInfo.storage = getAreaStorageLocations(areaInfo); areaInfo.storage = getAreaStorageLocations(areaInfo);
return areaInfo; return areaInfo;
} }
} }
@ -183,8 +183,8 @@ function getAreaStorageLocations(areaInfo) {
return _.compact(storageTags.map(storageTag => { return _.compact(storageTags.map(storageTag => {
if(avail[storageTag]) { if(avail[storageTag]) {
return { return {
storageTag : storageTag, storageTag : storageTag,
dir : getAreaStorageDirectoryByTag(storageTag), dir : getAreaStorageDirectoryByTag(storageTag),
}; };
} }
})); }));
@ -202,14 +202,14 @@ function getExistingFileEntriesBySha256(sha256, cb) {
FileDb.each( FileDb.each(
`SELECT file_id, area_tag `SELECT file_id, area_tag
FROM file FROM file
WHERE file_sha256=?;`, WHERE file_sha256=?;`,
[ sha256 ], [ sha256 ],
(err, fileRow) => { (err, fileRow) => {
if(fileRow) { if(fileRow) {
entries.push({ entries.push({
fileId : fileRow.file_id, fileId : fileRow.file_id,
areaTag : fileRow.area_tag, areaTag : fileRow.area_tag,
}); });
} }
}, },
@ -219,10 +219,10 @@ function getExistingFileEntriesBySha256(sha256, cb) {
); );
} }
// :TODO: This is bascially sliceAtEOF() from art.js .... DRY! // :TODO: This is bascially sliceAtEOF() from art.js .... DRY!
function sliceAtSauceMarker(data) { function sliceAtSauceMarker(data) {
let eof = data.length; let eof = data.length;
const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE)
for(let i = eof - 1; i > stopPos; i--) { for(let i = eof - 1; i > stopPos; i--) {
if(0x1a === data[i]) { if(0x1a === data[i]) {
@ -234,8 +234,8 @@ function sliceAtSauceMarker(data) {
} }
function attemptSetEstimatedReleaseDate(fileEntry) { function attemptSetEstimatedReleaseDate(fileEntry) {
// :TODO: yearEstPatterns RegExp's should be cached - we can do this @ Config (re)load time // :TODO: yearEstPatterns RegExp's should be cached - we can do this @ Config (re)load time
const patterns = Config().fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); const patterns = Config().fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi'));
function getMatch(input) { function getMatch(input) {
if(input) { if(input) {
@ -250,10 +250,10 @@ function attemptSetEstimatedReleaseDate(fileEntry) {
} }
// //
// We attempt detection in short -> long order // We attempt detection in short -> long order
// //
// Throw out anything that is current_year + 2 (we give some leway) // Throw out anything that is current_year + 2 (we give some leway)
// with the assumption that must be wrong. // with the assumption that must be wrong.
// //
const maxYear = moment().add(2, 'year').year(); const maxYear = moment().add(2, 'year').year();
const match = getMatch(fileEntry.desc) || getMatch(fileEntry.descLong); const match = getMatch(fileEntry.desc) || getMatch(fileEntry.descLong);
@ -279,7 +279,7 @@ function attemptSetEstimatedReleaseDate(fileEntry) {
} }
} }
// a simple log proxy for when we call from oputil.js // a simple log proxy for when we call from oputil.js
function logDebug(obj, msg) { function logDebug(obj, msg) {
if(Log) { if(Log) {
Log.debug(obj, msg); Log.debug(obj, msg);
@ -290,8 +290,8 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
async.waterfall( async.waterfall(
[ [
function extractDescFiles(callback) { function extractDescFiles(callback) {
// :TODO: would be nice if these RegExp's were cached // :TODO: would be nice if these RegExp's were cached
// :TODO: this is long winded... // :TODO: this is long winded...
const config = Config(); const config = Config();
const extractList = []; const extractList = [];
@ -327,8 +327,8 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
} }
const descFiles = { const descFiles = {
desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null, desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null,
descLong : longDescFile ? paths.join(tempDir, longDescFile.fileName) : null, descLong : longDescFile ? paths.join(tempDir, longDescFile.fileName) : null,
}; };
return callback(null, descFiles); return callback(null, descFiles);
@ -348,7 +348,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
return next(null); return next(null);
} }
// skip entries that are too large // skip entries that are too large
const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`; const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`;
if(config.fileBase[maxFileSizeKey] && stats.size > config.fileBase[maxFileSizeKey]) { if(config.fileBase[maxFileSizeKey] && stats.size > config.fileBase[maxFileSizeKey]) {
logDebug( { byteSize : stats.size, maxByteSize : config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` ); logDebug( { byteSize : stats.size, maxByteSize : config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` );
@ -361,18 +361,18 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
} }
// //
// Assume FILE_ID.DIZ, NFO files, etc. are CP437. // Assume FILE_ID.DIZ, NFO files, etc. are CP437.
// //
// :TODO: This isn't really always the case - how to handle this? We could do a quick detection... // :TODO: This isn't really always the case - how to handle this? We could do a quick detection...
fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437'); fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437');
fileEntry[`${descType}Src`] = 'descFile'; fileEntry[`${descType}Src`] = 'descFile';
return next(null); return next(null);
}); });
}); });
}, () => { }, () => {
// cleanup but don't wait // cleanup but don't wait
temptmp.cleanup( paths => { temptmp.cleanup( paths => {
// note: don't use client logger here - may not be avail // note: don't use client logger here - may not be avail
logDebug( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' ); logDebug( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' );
}); });
return callback(null); return callback(null);
@ -390,7 +390,7 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries
async.waterfall( async.waterfall(
[ [
function extractToTemp(callback) { function extractToTemp(callback) {
// :TODO: we may want to skip this if the compressed file is too large... // :TODO: we may want to skip this if the compressed file is too large...
temptmp.mkdir( { prefix : 'enigextract-' }, (err, tempDir) => { temptmp.mkdir( { prefix : 'enigextract-' }, (err, tempDir) => {
if(err) { if(err) {
return callback(err); return callback(err);
@ -398,7 +398,7 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries
const archiveUtil = ArchiveUtil.getInstance(); const archiveUtil = ArchiveUtil.getInstance();
// ensure we only extract one - there should only be one anyway -- we also just need the fileName // ensure we only extract one - there should only be one anyway -- we also just need the fileName
const extractList = archiveEntries.slice(0, 1).map(entry => entry.fileName); const extractList = archiveEntries.slice(0, 1).map(entry => entry.fileName);
archiveUtil.extractTo(filePath, tempDir, fileEntry.meta.archive_type, extractList, err => { archiveUtil.extractTo(filePath, tempDir, fileEntry.meta.archive_type, extractList, err => {
@ -413,8 +413,8 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries
function processSingleExtractedFile(extractedFile, callback) { function processSingleExtractedFile(extractedFile, callback) {
populateFileEntryInfoFromFile(fileEntry, extractedFile, err => { populateFileEntryInfoFromFile(fileEntry, extractedFile, err => {
if(!fileEntry.desc) { if(!fileEntry.desc) {
fileEntry.desc = getDescFromFileName(filePath); fileEntry.desc = getDescFromFileName(filePath);
fileEntry.descSrc = 'fileName'; fileEntry.descSrc = 'fileName';
} }
return callback(err); return callback(err);
}); });
@ -427,8 +427,8 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries
} }
function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, cb) { function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, cb) {
const archiveUtil = ArchiveUtil.getInstance(); const archiveUtil = ArchiveUtil.getInstance();
const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive() const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive()
async.waterfall( async.waterfall(
[ [
@ -449,7 +449,7 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c
} }
iterator(iterErr => { iterator(iterErr => {
return callback( iterErr, entries || [] ); // ignore original |err| here return callback( iterErr, entries || [] ); // ignore original |err| here
}); });
}); });
}); });
@ -462,12 +462,12 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c
}, },
function extractDescFromArchive(entries, callback) { function extractDescFromArchive(entries, callback) {
// //
// If we have a -single- entry in the archive, extract that file // If we have a -single- entry in the archive, extract that file
// and try retrieving info in the non-archive manor. This should // and try retrieving info in the non-archive manor. This should
// work for things like zipped up .pdf files. // work for things like zipped up .pdf files.
// //
// Otherwise, try to find particular desc files such as FILE_ID.DIZ // Otherwise, try to find particular desc files such as FILE_ID.DIZ
// and README.1ST // and README.1ST
// //
const archDescHandler = (1 === entries.length) ? extractAndProcessSingleArchiveEntry : extractAndProcessDescFiles; const archDescHandler = (1 === entries.length) ? extractAndProcessSingleArchiveEntry : extractAndProcessDescFiles;
archDescHandler(fileEntry, filePath, entries, err => { archDescHandler(fileEntry, filePath, entries, err => {
@ -494,7 +494,7 @@ function getInfoExtractUtilForDesc(mimeType, filePath, descType) {
let fileType = _.get(config, [ 'fileTypes', mimeType ] ); let fileType = _.get(config, [ 'fileTypes', mimeType ] );
if(Array.isArray(fileType)) { if(Array.isArray(fileType)) {
// further refine by extention // further refine by extention
fileType = fileType.find(ft => paths.extname(filePath) === ft.ext); fileType = fileType.find(ft => paths.extname(filePath) === ft.ext);
} }
@ -542,17 +542,17 @@ function populateFileEntryInfoFromFile(fileEntry, filePath, cb) {
const key = 'short' === descType ? 'desc' : 'descLong'; const key = 'short' === descType ? 'desc' : 'descLong';
if('desc' === key) { if('desc' === key) {
// //
// Word wrap short descriptions to FILE_ID.DIZ spec // Word wrap short descriptions to FILE_ID.DIZ spec
// //
// "...no more than 45 characters long" // "...no more than 45 characters long"
// //
// See http://www.textfiles.com/computers/fileid.txt // See http://www.textfiles.com/computers/fileid.txt
// //
stdout = (wordWrapText( stdout, { width : 45 } ).wrapped || []).join('\n'); stdout = (wordWrapText( stdout, { width : 45 } ).wrapped || []).join('\n');
} }
fileEntry[key] = stdout; fileEntry[key] = stdout;
fileEntry[`${key}Src`] = 'infoTool'; fileEntry[`${key}Src`] = 'infoTool';
} }
} }
@ -574,8 +574,8 @@ function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb
function getDescriptions(callback) { function getDescriptions(callback) {
populateFileEntryInfoFromFile(fileEntry, filePath, err => { populateFileEntryInfoFromFile(fileEntry, filePath, err => {
if(!fileEntry.desc) { if(!fileEntry.desc) {
fileEntry.desc = getDescFromFileName(filePath); fileEntry.desc = getDescFromFileName(filePath);
fileEntry.descSrc = 'fileName'; fileEntry.descSrc = 'fileName';
} }
return callback(err); return callback(err);
}); });
@ -592,7 +592,7 @@ function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb
} }
function addNewFileEntry(fileEntry, filePath, cb) { function addNewFileEntry(fileEntry, filePath, cb) {
// :TODO: Use detectTypeWithBuf() once avail - we *just* read some file data // :TODO: Use detectTypeWithBuf() once avail - we *just* read some file data
async.series( async.series(
[ [
@ -611,26 +611,26 @@ const HASH_NAMES = [ 'sha1', 'sha256', 'md5', 'crc32' ];
function scanFile(filePath, options, iterator, cb) { function scanFile(filePath, options, iterator, cb) {
if(3 === arguments.length && _.isFunction(iterator)) { if(3 === arguments.length && _.isFunction(iterator)) {
cb = iterator; cb = iterator;
iterator = null; iterator = null;
} else if(2 === arguments.length && _.isFunction(options)) { } else if(2 === arguments.length && _.isFunction(options)) {
cb = options; cb = options;
iterator = null; iterator = null;
options = {}; options = {};
} }
const fileEntry = new FileEntry({ const fileEntry = new FileEntry({
areaTag : options.areaTag, areaTag : options.areaTag,
meta : options.meta, meta : options.meta,
hashTags : options.hashTags, // Set() or Array hashTags : options.hashTags, // Set() or Array
fileName : paths.basename(filePath), fileName : paths.basename(filePath),
storageTag : options.storageTag, storageTag : options.storageTag,
fileSha256 : options.sha256, // caller may know this already fileSha256 : options.sha256, // caller may know this already
}); });
const stepInfo = { const stepInfo = {
filePath : filePath, filePath : filePath,
fileName : paths.basename(filePath), fileName : paths.basename(filePath),
}; };
const callIter = (next) => { const callIter = (next) => {
@ -638,8 +638,8 @@ function scanFile(filePath, options, iterator, cb) {
}; };
const readErrorCallIter = (origError, next) => { const readErrorCallIter = (origError, next) => {
stepInfo.step = 'read_error'; stepInfo.step = 'read_error';
stepInfo.error = origError.message; stepInfo.error = origError.message;
callIter( () => { callIter( () => {
return next(origError); return next(origError);
@ -648,7 +648,7 @@ function scanFile(filePath, options, iterator, cb) {
let lastCalcHashPercent; let lastCalcHashPercent;
// don't re-calc hashes for any we already have in |options| // don't re-calc hashes for any we already have in |options|
const hashesToCalc = HASH_NAMES.filter(hn => { const hashesToCalc = HASH_NAMES.filter(hn => {
if('sha256' === hn && fileEntry.fileSha256) { if('sha256' === hn && fileEntry.fileSha256) {
return false; return false;
@ -669,8 +669,8 @@ function scanFile(filePath, options, iterator, cb) {
return readErrorCallIter(err, callback); return readErrorCallIter(err, callback);
} }
stepInfo.step = 'start'; stepInfo.step = 'start';
stepInfo.byteSize = fileEntry.meta.byte_size = stats.size; stepInfo.byteSize = fileEntry.meta.byte_size = stats.size;
return callIter(callback); return callIter(callback);
}); });
@ -694,9 +694,9 @@ function scanFile(filePath, options, iterator, cb) {
}; };
// //
// Note that we are not using fs.createReadStream() here: // Note that we are not using fs.createReadStream() here:
// While convenient, it is quite a bit slower -- which adds // While convenient, it is quite a bit slower -- which adds
// up to many seconds in time for larger files. // up to many seconds in time for larger files.
// //
const chunkSize = 1024 * 64; const chunkSize = 1024 * 64;
const buffer = new Buffer(chunkSize); const buffer = new Buffer(chunkSize);
@ -714,7 +714,7 @@ function scanFile(filePath, options, iterator, cb) {
} }
if(0 === bytesRead) { if(0 === bytesRead) {
// done - finalize // done - finalize
fileEntry.meta.byte_size = stepInfo.bytesProcessed; fileEntry.meta.byte_size = stepInfo.bytesProcessed;
for(let i = 0; i < hashesToCalc.length; ++i) { for(let i = 0; i < hashesToCalc.length; ++i) {
@ -733,11 +733,11 @@ function scanFile(filePath, options, iterator, cb) {
return callIter(callback); return callIter(callback);
} }
stepInfo.bytesProcessed += bytesRead; stepInfo.bytesProcessed += bytesRead;
stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100));
// //
// Only send 'hash_update' step update if we have a noticable percentage change in progress // Only send 'hash_update' step update if we have a noticable percentage change in progress
// //
const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer; const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer;
if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) { if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) {
@ -745,7 +745,7 @@ function scanFile(filePath, options, iterator, cb) {
return nextChunk(); return nextChunk();
} else { } else {
lastCalcHashPercent = stepInfo.calcHashPercent; lastCalcHashPercent = stepInfo.calcHashPercent;
stepInfo.step = 'hash_update'; stepInfo.step = 'hash_update';
callIter(err => { callIter(err => {
if(err) { if(err) {
@ -767,7 +767,7 @@ function scanFile(filePath, options, iterator, cb) {
archiveUtil.detectType(filePath, (err, archiveType) => { archiveUtil.detectType(filePath, (err, archiveType) => {
if(archiveType) { if(archiveType) {
// save this off // save this off
fileEntry.meta.archive_type = archiveType; fileEntry.meta.archive_type = archiveType;
populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => { populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => {
@ -776,7 +776,7 @@ function scanFile(filePath, options, iterator, cb) {
if(err) { if(err) {
logDebug( { error : err.message }, 'Non-archive file entry population failed'); logDebug( { error : err.message }, 'Non-archive file entry population failed');
} }
return callback(null); // ignore err return callback(null); // ignore err
}); });
} else { } else {
return callback(null); return callback(null);
@ -787,7 +787,7 @@ function scanFile(filePath, options, iterator, cb) {
if(err) { if(err) {
logDebug( { error : err.message }, 'Non-archive file entry population failed'); logDebug( { error : err.message }, 'Non-archive file entry population failed');
} }
return callback(null); // ignore err return callback(null); // ignore err
}); });
} }
}); });
@ -816,12 +816,12 @@ function scanFile(filePath, options, iterator, cb) {
function scanFileAreaForChanges(areaInfo, options, iterator, cb) { function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
if(3 === arguments.length && _.isFunction(iterator)) { if(3 === arguments.length && _.isFunction(iterator)) {
cb = iterator; cb = iterator;
iterator = null; iterator = null;
} else if(2 === arguments.length && _.isFunction(options)) { } else if(2 === arguments.length && _.isFunction(options)) {
cb = options; cb = options;
iterator = null; iterator = null;
options = {}; options = {};
} }
const storageLocations = getAreaStorageLocations(areaInfo); const storageLocations = getAreaStorageLocations(areaInfo);
@ -842,8 +842,8 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
fs.stat(fullPath, (err, stats) => { fs.stat(fullPath, (err, stats) => {
if(err) { if(err) {
// :TODO: Log me! // :TODO: Log me!
return nextFile(null); // always try next file return nextFile(null); // always try next file
} }
if(!stats.isFile()) { if(!stats.isFile()) {
@ -853,18 +853,18 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
scanFile( scanFile(
fullPath, fullPath,
{ {
areaTag : areaInfo.areaTag, areaTag : areaInfo.areaTag,
storageTag : storageLoc.storageTag storageTag : storageLoc.storageTag
}, },
iterator, iterator,
(err, fileEntry, dupeEntries) => { (err, fileEntry, dupeEntries) => {
if(err) { if(err) {
// :TODO: Log me!!! // :TODO: Log me!!!
return nextFile(null); // try next anyway return nextFile(null); // try next anyway
} }
if(dupeEntries.length > 0) { if(dupeEntries.length > 0) {
// :TODO: Handle duplidates -- what to do here??? // :TODO: Handle duplidates -- what to do here???
} else { } else {
if(Array.isArray(options.tags)) { if(Array.isArray(options.tags)) {
options.tags.forEach(tag => { options.tags.forEach(tag => {
@ -872,7 +872,7 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
}); });
} }
addNewFileEntry(fileEntry, fullPath, err => { addNewFileEntry(fileEntry, fullPath, err => {
// pass along error; we failed to insert a record in our DB or something else bad // pass along error; we failed to insert a record in our DB or something else bad
return nextFile(err); return nextFile(err);
}); });
} }
@ -885,7 +885,7 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
}); });
}, },
function scanDbEntries(callback) { function scanDbEntries(callback) {
// :TODO: Look @ db entries for area that were *not* processed above // :TODO: Look @ db entries for area that were *not* processed above
return callback(null); return callback(null);
} }
], ],
@ -900,7 +900,7 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
} }
function getDescFromFileName(fileName) { function getDescFromFileName(fileName) {
// :TODO: this method could use some more logic to really be nice. // :TODO: this method could use some more logic to really be nice.
const ext = paths.extname(fileName); const ext = paths.extname(fileName);
const name = paths.basename(fileName, ext); const name = paths.basename(fileName, ext);
@ -908,26 +908,26 @@ function getDescFromFileName(fileName) {
} }
// //
// Return an object of stats about an area(s) // Return an object of stats about an area(s)
// //
// { // {
// //
// totalFiles : <totalFileCount>, // totalFiles : <totalFileCount>,
// totalBytes : <totalByteSize>, // totalBytes : <totalByteSize>,
// areas : { // areas : {
// <areaTag> : { // <areaTag> : {
// files : <fileCount>, // files : <fileCount>,
// bytes : <byteSize> // bytes : <byteSize>
// } // }
// } // }
// } // }
// //
function getAreaStats(cb) { function getAreaStats(cb) {
FileDb.all( FileDb.all(
`SELECT DISTINCT f.area_tag, COUNT(f.file_id) AS total_files, SUM(m.meta_value) AS total_byte_size `SELECT DISTINCT f.area_tag, COUNT(f.file_id) AS total_files, SUM(m.meta_value) AS total_byte_size
FROM file f, file_meta m FROM file f, file_meta m
WHERE f.file_id = m.file_id AND m.meta_name='byte_size' WHERE f.file_id = m.file_id AND m.meta_name='byte_size'
GROUP BY f.area_tag;`, GROUP BY f.area_tag;`,
(err, statRows) => { (err, statRows) => {
if(err) { if(err) {
return cb(err); return cb(err);
@ -946,8 +946,8 @@ function getAreaStats(cb) {
stats.areas = stats.areas || {}; stats.areas = stats.areas || {};
stats.areas[v.area_tag] = { stats.areas[v.area_tag] = {
files : v.total_files, files : v.total_files,
bytes : v.total_byte_size, bytes : v.total_byte_size,
}; };
return stats; return stats;
}, {}) }, {})
@ -956,7 +956,7 @@ function getAreaStats(cb) {
); );
} }
// method exposed for event scheduler // method exposed for event scheduler
function updateAreaStatsScheduledEvent(args, cb) { function updateAreaStatsScheduledEvent(args, cb) {
getAreaStats( (err, stats) => { getAreaStats( (err, stats) => {
if(!err) { if(!err) {
@ -968,13 +968,13 @@ function updateAreaStatsScheduledEvent(args, cb) {
} }
function cleanUpTempSessionItems(cb) { function cleanUpTempSessionItems(cb) {
// find (old) temporary session items and nuke 'em // find (old) temporary session items and nuke 'em
const filter = { const filter = {
areaTag : WellKnownAreaTags.TempDownloads, areaTag : WellKnownAreaTags.TempDownloads,
metaPairs : [ metaPairs : [
{ {
name : 'session_temp_dl', name : 'session_temp_dl',
value : 1 value : 1
} }
] ]
}; };

View File

@ -1,22 +1,22 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const { getSortedAvailableFileAreas } = require('./file_base_area.js'); const { getSortedAvailableFileAreas } = require('./file_base_area.js');
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
// deps // deps
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Area Selector', name : 'File Area Selector',
desc : 'Select from available file areas', desc : 'Select from available file areas',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
areaList : 1, areaList : 1,
}; };
exports.getModule = class FileAreaSelectModule extends MenuModule { exports.getModule = class FileAreaSelectModule extends MenuModule {
@ -26,14 +26,14 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
this.menuMethods = { this.menuMethods = {
selectArea : (formData, extraArgs, cb) => { selectArea : (formData, extraArgs, cb) => {
const filterCriteria = { const filterCriteria = {
areaTag : formData.value.areaTag, areaTag : formData.value.areaTag,
}; };
const menuOpts = { const menuOpts = {
extraArgs : { extraArgs : {
filterCriteria : filterCriteria, filterCriteria : filterCriteria,
}, },
menuFlags : [ 'popParent', 'mergeFlags' ], menuFlags : [ 'popParent', 'mergeFlags' ],
}; };
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
@ -54,12 +54,12 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
function mergeAreaStats(callback) { function mergeAreaStats(callback) {
const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} }; const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} };
// we could use 'sort' alone, but area/conf sorting has some special properties; user can still override // we could use 'sort' alone, but area/conf sorting has some special properties; user can still override
const availAreas = getSortedAvailableFileAreas(self.client); const availAreas = getSortedAvailableFileAreas(self.client);
availAreas.forEach(area => { availAreas.forEach(area => {
const stats = areaStats.areas[area.areaTag]; const stats = areaStats.areas[area.areaTag];
area.totalFiles = stats ? stats.files : 0; area.totalFiles = stats ? stats.files : 0;
area.totalBytes = stats ? stats.bytes : 0; area.totalBytes = stats ? stats.bytes : 0;
}); });
return callback(null, availAreas); return callback(null, availAreas);

View File

@ -1,37 +1,37 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const DownloadQueue = require('./download_queue.js'); const DownloadQueue = require('./download_queue.js');
const theme = require('./theme.js'); const theme = require('./theme.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const FileAreaWeb = require('./file_area_web.js'); const FileAreaWeb = require('./file_area_web.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Base Download Queue Manager', name : 'File Base Download Queue Manager',
desc : 'Module for interacting with download queue/batch', desc : 'Module for interacting with download queue/batch',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const FormIds = { const FormIds = {
queueManager : 0, queueManager : 0,
}; };
const MciViewIds = { const MciViewIds = {
queueManager : { queueManager : {
queue : 1, queue : 1,
navMenu : 2, navMenu : 2,
customRangeStart : 10, customRangeStart : 10,
}, },
}; };
@ -52,8 +52,8 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
downloadAll : (formData, extraArgs, cb) => { downloadAll : (formData, extraArgs, cb) => {
const modOpts = { const modOpts = {
extraArgs : { extraArgs : {
sendQueue : this.dlQueue.items, sendQueue : this.dlQueue.items,
direction : 'send', direction : 'send',
} }
}; };
@ -67,13 +67,13 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
this.dlQueue.removeItems(selectedItem.fileId); this.dlQueue.removeItems(selectedItem.fileId);
// :TODO: broken: does not redraw menu properly - needs fixed! // :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb);
}, },
clearQueue : (formData, extraArgs, cb) => { clearQueue : (formData, extraArgs, cb) => {
this.dlQueue.clear(); this.dlQueue.clear();
// :TODO: broken: does not redraw menu properly - needs fixed! // :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView('all', cb); return this.removeItemsFromDownloadQueueView('all', cb);
} }
}; };
@ -82,11 +82,11 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
initSequence() { initSequence() {
if(0 === this.dlQueue.items.length) { if(0 === this.dlQueue.items.length) {
if(this.sendFileIds) { if(this.sendFileIds) {
// we've finished everything up - just fall back // we've finished everything up - just fall back
return this.prevMenu(); return this.prevMenu();
} }
// Simply an empty D/L queue: Present a specialized "empty queue" page // Simply an empty D/L queue: Present a specialized "empty queue" page
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue');
} }
@ -129,11 +129,11 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
if(serveItem && serveItem.url) { if(serveItem && serveItem.url) {
const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
} else { } else {
fileEntry.webDlLink = ''; fileEntry.webDlLink = '';
fileEntry.webDlExpire = ''; fileEntry.webDlExpire = '';
} }
this.updateCustomViewTextsWithFilter( this.updateCustomViewTextsWithFilter(
@ -150,8 +150,8 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
return cb(Errors.DoesNotExist('Queue view does not exist')); return cb(Errors.DoesNotExist('Queue view does not exist'));
} }
const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}'; const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}';
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
@ -188,8 +188,8 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
} }
displayArtAndPrepViewController(name, options, cb) { displayArtAndPrepViewController(name, options, cb) {
const self = this; const self = this;
const config = this.menuConfig.config; const config = this.menuConfig.config;
async.waterfall( async.waterfall(
[ [
@ -210,8 +210,8 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
function prepeareViewController(artData, callback) { function prepeareViewController(artData, callback) {
if(_.isUndefined(self.viewControllers[name])) { if(_.isUndefined(self.viewControllers[name])) {
const vcOpts = { const vcOpts = {
client : self.client, client : self.client,
formId : FormIds[name], formId : FormIds[name],
}; };
if(!_.isUndefined(options.noInput)) { if(!_.isUndefined(options.noInput)) {
@ -221,9 +221,9 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
const vc = self.addViewController(name, new ViewController(vcOpts)); const vc = self.addViewController(name, new ViewController(vcOpts));
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds[name], formId : FormIds[name],
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);

View File

@ -1,13 +1,13 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const uuidV4 = require('uuid/v4'); const uuidV4 = require('uuid/v4');
module.exports = class FileBaseFilters { module.exports = class FileBaseFilters {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
this.load(); this.load();
} }
@ -74,7 +74,7 @@ module.exports = class FileBaseFilters {
try { try {
this.filters = JSON.parse(filtersProperty); this.filters = JSON.parse(filtersProperty);
} catch(e) { } catch(e) {
this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :( this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :(
defaulted = true; defaulted = true;
this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' ); this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' );
} }
@ -110,18 +110,18 @@ module.exports = class FileBaseFilters {
} }
static getBuiltInSystemFilters() { static getBuiltInSystemFilters() {
const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329'; const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329';
const filters = { const filters = {
[ U_LATEST ] : { [ U_LATEST ] : {
name : 'By Date Added', name : 'By Date Added',
areaTag : '', // all areaTag : '', // all
terms : '', // * terms : '', // *
tags : '', // * tags : '', // *
order : 'descending', order : 'descending',
sort : 'upload_timestamp', sort : 'upload_timestamp',
uuid : U_LATEST, uuid : U_LATEST,
system : true, system : true,
} }
}; };

View File

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

View File

@ -1,30 +1,30 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
const FileBaseFilters = require('./file_base_filter.js'); const FileBaseFilters = require('./file_base_filter.js');
// deps // deps
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Base Search', name : 'File Base Search',
desc : 'Module for quickly searching the file base', desc : 'Module for quickly searching the file base',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
search : { search : {
searchTerms : 1, searchTerms : 1,
search : 2, search : 2,
tags : 3, tags : 3,
area : 4, area : 4,
orderBy : 5, orderBy : 5,
sort : 6, sort : 6,
advSearch : 7, advSearch : 7,
} }
}; };
@ -46,8 +46,8 @@ exports.getModule = class FileBaseSearch extends MenuModule {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.addViewController( 'search', new ViewController( { client : this.client } ) ); const vc = self.addViewController( 'search', new ViewController( { client : this.client } ) );
async.series( async.series(
[ [
@ -74,7 +74,7 @@ exports.getModule = class FileBaseSearch extends MenuModule {
getSelectedAreaTag(index) { getSelectedAreaTag(index) {
if(0 === index) { if(0 === index) {
return ''; // -ALL- return ''; // -ALL-
} }
const area = this.availAreas[index]; const area = this.availAreas[index];
if(!area) { if(!area) {
@ -92,16 +92,16 @@ exports.getModule = class FileBaseSearch extends MenuModule {
} }
getFilterValuesFromFormData(formData, isAdvanced) { getFilterValuesFromFormData(formData, isAdvanced) {
const areaIndex = isAdvanced ? formData.value.areaIndex : 0; const areaIndex = isAdvanced ? formData.value.areaIndex : 0;
const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0; const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0;
const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0; const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0;
return { return {
areaTag : this.getSelectedAreaTag(areaIndex), areaTag : this.getSelectedAreaTag(areaIndex),
terms : formData.value.searchTerms, terms : formData.value.searchTerms,
tags : isAdvanced ? formData.value.tags : '', tags : isAdvanced ? formData.value.tags : '',
order : this.getOrderBy(orderByIndex), order : this.getOrderBy(orderByIndex),
sort : this.getSortBy(sortByIndex), sort : this.getSortBy(sortByIndex),
}; };
} }
@ -109,10 +109,10 @@ exports.getModule = class FileBaseSearch extends MenuModule {
const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced); const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced);
const menuOpts = { const menuOpts = {
extraArgs : { extraArgs : {
filterCriteria : filterCriteria, filterCriteria : filterCriteria,
}, },
menuFlags : [ 'popParent' ], menuFlags : [ 'popParent' ],
}; };
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);

View File

@ -1,66 +1,66 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const FileArea = require('./file_base_area.js'); const FileArea = require('./file_base_area.js');
const { renderSubstr } = require('./string_util.js'); const { renderSubstr } = require('./string_util.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const Events = require('./events.js'); const Events = require('./events.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const DownloadQueue = require('./download_queue.js'); const DownloadQueue = require('./download_queue.js');
const { exportFileList } = require('./file_base_list_export.js'); const { exportFileList } = require('./file_base_list_export.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const async = require('async'); const async = require('async');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const fse = require('fs-extra'); const fse = require('fs-extra');
const paths = require('path'); const paths = require('path');
const moment = require('moment'); const moment = require('moment');
const uuidv4 = require('uuid/v4'); const uuidv4 = require('uuid/v4');
const yazl = require('yazl'); const yazl = require('yazl');
/* /*
Module config block can contain the following: Module config block can contain the following:
templateEncoding - encoding of template files (utf8) templateEncoding - encoding of template files (utf8)
tsFormat - timestamp format (theme 'short') tsFormat - timestamp format (theme 'short')
descWidth - max desc width (45) descWidth - max desc width (45)
progBarChar - progress bar character () progBarChar - progress bar character ()
compressThreshold - threshold to kick in comrpession for lists (1.44 MiB) compressThreshold - threshold to kick in comrpession for lists (1.44 MiB)
templates - object containing: templates - object containing:
header - filename of header template (misc/file_list_header.asc) header - filename of header template (misc/file_list_header.asc)
entry - filename of entry template (misc/file_list_entry.asc) entry - filename of entry template (misc/file_list_entry.asc)
Header template variables: Header template variables:
nowTs, boardName, totalFileCount, totalFileSize, nowTs, boardName, totalFileCount, totalFileSize,
filterAreaTag, filterAreaName, filterAreaDesc, filterAreaTag, filterAreaName, filterAreaDesc,
filterTerms, filterHashTags filterTerms, filterHashTags
Entry template variables: Entry template variables:
fileId, areaName, areaDesc, userRating, fileName, fileId, areaName, areaDesc, userRating, fileName,
fileSize, fileDesc, fileDescShort, fileSha256, fileCrc32, fileSize, fileDesc, fileDescShort, fileSha256, fileCrc32,
fileMd5, fileSha1, uploadBy, fileUploadTs, fileHashTags, fileMd5, fileSha1, uploadBy, fileUploadTs, fileHashTags,
currentFile, progress, currentFile, progress,
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Base List Export', name : 'File Base List Export',
desc : 'Exports file base listings for download', desc : 'Exports file base listings for download',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const FormIds = { const FormIds = {
main : 0, main : 0,
}; };
const MciViewIds = { const MciViewIds = {
main : { main : {
status : 1, status : 1,
progressBar : 2, progressBar : 2,
customRangeStart : 10, customRangeStart : 10,
} }
}; };
@ -70,11 +70,11 @@ exports.getModule = class FileBaseListExport extends MenuModule {
super(options); super(options);
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs);
this.config.templateEncoding = this.config.templateEncoding || 'utf8'; this.config.templateEncoding = this.config.templateEncoding || 'utf8';
this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short');
this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ
this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1);
this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :) this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :)
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
@ -154,7 +154,7 @@ exports.getModule = class FileBaseListExport extends MenuModule {
async.waterfall( async.waterfall(
[ [
function buildList(callback) { function buildList(callback) {
// this may take quite a while; temp disable of idle monitor // this may take quite a while; temp disable of idle monitor
self.client.stopIdleMonitor(); self.client.stopIdleMonitor();
self.client.on('key press', keyPressHandler); self.client.on('key press', keyPressHandler);
@ -165,12 +165,12 @@ exports.getModule = class FileBaseListExport extends MenuModule {
} }
const opts = { const opts = {
templateEncoding : self.config.templateEncoding, templateEncoding : self.config.templateEncoding,
headerTemplate : _.get(self.config, 'templates.header', 'file_list_header.asc'), headerTemplate : _.get(self.config, 'templates.header', 'file_list_header.asc'),
entryTemplate : _.get(self.config, 'templates.entry', 'file_list_entry.asc'), entryTemplate : _.get(self.config, 'templates.entry', 'file_list_entry.asc'),
tsFormat : self.config.tsFormat, tsFormat : self.config.tsFormat,
descWidth : self.config.descWidth, descWidth : self.config.descWidth,
progress : exportListProgress, progress : exportListProgress,
}; };
exportFileList(filterCriteria, opts, (err, listBody) => { exportFileList(filterCriteria, opts, (err, listBody) => {
@ -180,8 +180,8 @@ exports.getModule = class FileBaseListExport extends MenuModule {
function persistList(listBody, callback) { function persistList(listBody, callback) {
updateStatus('Persisting list'); updateStatus('Persisting list');
const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads);
const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea);
fse.mkdirs(sysTempDownloadDir, err => { fse.mkdirs(sysTempDownloadDir, err => {
if(err) { if(err) {
@ -206,14 +206,14 @@ exports.getModule = class FileBaseListExport extends MenuModule {
}, },
function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) { function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) {
const newEntry = new FileEntry({ const newEntry = new FileEntry({
areaTag : sysTempDownloadArea.areaTag, areaTag : sysTempDownloadArea.areaTag,
fileName : paths.basename(outputFileName), fileName : paths.basename(outputFileName),
storageTag : sysTempDownloadArea.storageTags[0], storageTag : sysTempDownloadArea.storageTags[0],
meta : { meta : {
upload_by_username : self.client.user.username, upload_by_username : self.client.user.username,
upload_by_user_id : self.client.user.userId, upload_by_user_id : self.client.user.userId,
byte_size : fileSize, byte_size : fileSize,
session_temp_dl : 1, // download is valid until session is over session_temp_dl : 1, // download is valid until session is over
} }
}); });
@ -221,11 +221,11 @@ exports.getModule = class FileBaseListExport extends MenuModule {
newEntry.persist(err => { newEntry.persist(err => {
if(!err) { if(!err) {
// queue it! // queue it!
const dlQueue = new DownloadQueue(self.client); const dlQueue = new DownloadQueue(self.client);
dlQueue.add(newEntry, true); // true=systemFile dlQueue.add(newEntry, true); // true=systemFile
// clean up after ourselves when the session ends // clean up after ourselves when the session ends
const thisClientId = self.client.session.id; const thisClientId = self.client.session.id;
Events.once(Events.getSystemEvents().ClientDisconnected, evt => { Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
if(thisClientId === _.get(evt, 'client.session.id')) { if(thisClientId === _.get(evt, 'client.session.id')) {
@ -243,7 +243,7 @@ exports.getModule = class FileBaseListExport extends MenuModule {
}); });
}, },
function done(callback) { function done(callback) {
// re-enable idle monitor // re-enable idle monitor
self.client.startIdleMonitor(); self.client.startIdleMonitor();
updateStatus('Exported list has been added to your download queue'); updateStatus('Exported list has been added to your download queue');
@ -264,7 +264,7 @@ exports.getModule = class FileBaseListExport extends MenuModule {
} }
if(stats.size < this.config.compressThreshold) { if(stats.size < this.config.compressThreshold) {
// small enough, keep orig // small enough, keep orig
return cb(null, filePath, stats.size); return cb(null, filePath, stats.size);
} }
@ -276,13 +276,13 @@ exports.getModule = class FileBaseListExport extends MenuModule {
const outZipFile = fs.createWriteStream(zipFilePath); const outZipFile = fs.createWriteStream(zipFilePath);
zipFile.outputStream.pipe(outZipFile); zipFile.outputStream.pipe(outZipFile);
zipFile.outputStream.on('finish', () => { zipFile.outputStream.on('finish', () => {
// delete the original // delete the original
fse.unlink(filePath, err => { fse.unlink(filePath, err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
// finally stat the new output // finally stat the new output
fse.stat(zipFilePath, (err, stats) => { fse.stat(zipFilePath, (err, stats) => {
return cb(err, zipFilePath, stats ? stats.size : 0); return cb(err, zipFilePath, stats ? stats.size : 0);
}); });

View File

@ -1,39 +1,39 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const DownloadQueue = require('./download_queue.js'); const DownloadQueue = require('./download_queue.js');
const theme = require('./theme.js'); const theme = require('./theme.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const FileAreaWeb = require('./file_area_web.js'); const FileAreaWeb = require('./file_area_web.js');
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
const Config = require('./config.js').get; const Config = require('./config.js').get;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Base Download Web Queue Manager', name : 'File Base Download Web Queue Manager',
desc : 'Module for interacting with web backed download queue/batch', desc : 'Module for interacting with web backed download queue/batch',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const FormIds = { const FormIds = {
queueManager : 0 queueManager : 0
}; };
const MciViewIds = { const MciViewIds = {
queueManager : { queueManager : {
queue : 1, queue : 1,
navMenu : 2, navMenu : 2,
customRangeStart : 10, customRangeStart : 10,
} }
}; };
@ -53,13 +53,13 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
this.dlQueue.removeItems(selectedItem.fileId); this.dlQueue.removeItems(selectedItem.fileId);
// :TODO: broken: does not redraw menu properly - needs fixed! // :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb);
}, },
clearQueue : (formData, extraArgs, cb) => { clearQueue : (formData, extraArgs, cb) => {
this.dlQueue.clear(); this.dlQueue.clear();
// :TODO: broken: does not redraw menu properly - needs fixed! // :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView('all', cb); return this.removeItemsFromDownloadQueueView('all', cb);
}, },
getBatchLink : (formData, extraArgs, cb) => { getBatchLink : (formData, extraArgs, cb) => {
@ -111,7 +111,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
this.updateCustomViewTextsWithFilter( this.updateCustomViewTextsWithFilter(
'queueManager', 'queueManager',
MciViewIds.queueManager.customRangeStart, fileEntry, MciViewIds.queueManager.customRangeStart, fileEntry,
{ filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others.... { filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others....
); );
} }
@ -121,8 +121,8 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
return cb(Errors.DoesNotExist('Queue view does not exist')); return cb(Errors.DoesNotExist('Queue view does not exist'));
} }
const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}'; const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}';
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
@ -148,7 +148,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
expireTime : expireTime expireTime : expireTime
}, },
(err, webBatchDlLink) => { (err, webBatchDlLink) => {
// :TODO: handle not enabled -> display such // :TODO: handle not enabled -> display such
if(err) { if(err) {
return cb(err); return cb(err);
} }
@ -156,8 +156,8 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
const formatObj = { const formatObj = {
webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink, webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink,
webBatchDlExpire : expireTime.format(webDlExpireTimeFormat), webBatchDlExpire : expireTime.format(webDlExpireTimeFormat),
}; };
this.updateCustomViewTextsWithFilter( this.updateCustomViewTextsWithFilter(
@ -188,7 +188,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => { FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => {
if(err) { if(err) {
if(ErrNotEnabled === err.reasonCode) { if(ErrNotEnabled === err.reasonCode) {
return nextFileEntry(err); // we should have caught this prior return nextFileEntry(err); // we should have caught this prior
} }
const expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes'); const expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes');
@ -202,17 +202,17 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
return nextFileEntry(err); return nextFileEntry(err);
} }
fileEntry.webDlLinkRaw = url; fileEntry.webDlLinkRaw = url;
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url;
fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat); fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat);
return nextFileEntry(null); return nextFileEntry(null);
} }
); );
} else { } else {
fileEntry.webDlLinkRaw = serveItem.url; fileEntry.webDlLinkRaw = serveItem.url;
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url; fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url;
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
return nextFileEntry(null); return nextFileEntry(null);
} }
}); });
@ -233,8 +233,8 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
} }
displayArtAndPrepViewController(name, options, cb) { displayArtAndPrepViewController(name, options, cb) {
const self = this; const self = this;
const config = this.menuConfig.config; const config = this.menuConfig.config;
async.waterfall( async.waterfall(
[ [
@ -255,8 +255,8 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
function prepeareViewController(artData, callback) { function prepeareViewController(artData, callback) {
if(_.isUndefined(self.viewControllers[name])) { if(_.isUndefined(self.viewControllers[name])) {
const vcOpts = { const vcOpts = {
client : self.client, client : self.client,
formId : FormIds[name], formId : FormIds[name],
}; };
if(!_.isUndefined(options.noInput)) { if(!_.isUndefined(options.noInput)) {
@ -266,9 +266,9 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
const vc = self.addViewController(name, new ViewController(vcOpts)); const vc = self.addViewController(name, new ViewController(vcOpts));
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds[name], formId : FormIds[name],
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);

View File

@ -1,57 +1,57 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const fileDb = require('./database.js').dbs.file; const fileDb = require('./database.js').dbs.file;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const { const {
getISOTimestampString, getISOTimestampString,
sanatizeString sanatizeString
} = require('./database.js'); } = require('./database.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const paths = require('path'); const paths = require('path');
const fse = require('fs-extra'); const fse = require('fs-extra');
const { unlink, readFile } = require('graceful-fs'); const { unlink, readFile } = require('graceful-fs');
const crypto = require('crypto'); const crypto = require('crypto');
const moment = require('moment'); const moment = require('moment');
const FILE_TABLE_MEMBERS = [ const FILE_TABLE_MEMBERS = [
'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', 'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag',
'desc', 'desc_long', 'upload_timestamp' 'desc', 'desc_long', 'upload_timestamp'
]; ];
const FILE_WELL_KNOWN_META = { const FILE_WELL_KNOWN_META = {
// name -> *read* converter, if any // name -> *read* converter, if any
upload_by_username : null, upload_by_username : null,
upload_by_user_id : (u) => parseInt(u) || 0, upload_by_user_id : (u) => parseInt(u) || 0,
file_md5 : null, file_md5 : null,
file_sha1 : null, file_sha1 : null,
file_crc32 : null, file_crc32 : null,
est_release_year : (y) => parseInt(y) || new Date().getFullYear(), est_release_year : (y) => parseInt(y) || new Date().getFullYear(),
dl_count : (d) => parseInt(d) || 0, dl_count : (d) => parseInt(d) || 0,
byte_size : (b) => parseInt(b) || 0, byte_size : (b) => parseInt(b) || 0,
archive_type : null, archive_type : null,
short_file_name : null, // e.g. DOS 8.3 filename, avail in some scenarios such as TIC import short_file_name : null, // e.g. DOS 8.3 filename, avail in some scenarios such as TIC import
tic_origin : null, // TIC "Origin" tic_origin : null, // TIC "Origin"
tic_desc : null, // TIC "Desc" tic_desc : null, // TIC "Desc"
tic_ldesc : null, // TIC "Ldesc" joined by '\n' tic_ldesc : null, // TIC "Ldesc" joined by '\n'
session_temp_dl : (v) => parseInt(v) ? true : false, session_temp_dl : (v) => parseInt(v) ? true : false,
}; };
module.exports = class FileEntry { module.exports = class FileEntry {
constructor(options) { constructor(options) {
options = options || {}; options = options || {};
this.fileId = options.fileId || 0; this.fileId = options.fileId || 0;
this.areaTag = options.areaTag || ''; this.areaTag = options.areaTag || '';
this.meta = Object.assign( { dl_count : 0 }, options.meta); this.meta = Object.assign( { dl_count : 0 }, options.meta);
this.hashTags = options.hashTags || new Set(); this.hashTags = options.hashTags || new Set();
this.fileName = options.fileName; this.fileName = options.fileName;
this.storageTag = options.storageTag; this.storageTag = options.storageTag;
this.fileSha256 = options.fileSha256; this.fileSha256 = options.fileSha256;
} }
static loadBasicEntry(fileId, dest, cb) { static loadBasicEntry(fileId, dest, cb) {
@ -59,9 +59,9 @@ module.exports = class FileEntry {
fileDb.get( fileDb.get(
`SELECT ${FILE_TABLE_MEMBERS.join(', ')} `SELECT ${FILE_TABLE_MEMBERS.join(', ')}
FROM file FROM file
WHERE file_id=? WHERE file_id=?
LIMIT 1;`, LIMIT 1;`,
[ fileId ], [ fileId ],
(err, file) => { (err, file) => {
if(err) { if(err) {
@ -72,7 +72,7 @@ module.exports = class FileEntry {
return cb(Errors.DoesNotExist('No file is available by that ID')); return cb(Errors.DoesNotExist('No file is available by that ID'));
} }
// assign props from |file| // assign props from |file|
FILE_TABLE_MEMBERS.forEach(prop => { FILE_TABLE_MEMBERS.forEach(prop => {
dest[_.camelCase(prop)] = file[prop]; dest[_.camelCase(prop)] = file[prop];
}); });
@ -149,7 +149,7 @@ module.exports = class FileEntry {
if(isUpdate) { if(isUpdate) {
trans.run( trans.run(
`REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) `REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
[ self.fileId, self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], [ self.fileId, self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ],
err => { err => {
return callback(err, trans); return callback(err, trans);
@ -158,9 +158,9 @@ module.exports = class FileEntry {
} else { } else {
trans.run( trans.run(
`REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) `REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
VALUES(?, ?, ?, ?, ?, ?, ?);`, VALUES(?, ?, ?, ?, ?, ?, ?);`,
[ self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], [ self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ],
function inserted(err) { // use non-arrow func for 'this' scope / lastID function inserted(err) { // use non-arrow func for 'this' scope / lastID
if(!err) { if(!err) {
self.fileId = this.lastID; self.fileId = this.lastID;
} }
@ -189,7 +189,7 @@ module.exports = class FileEntry {
} }
], ],
(err, trans) => { (err, trans) => {
// :TODO: Log orig err // :TODO: Log orig err
if(trans) { if(trans) {
trans[err ? 'rollback' : 'commit'](transErr => { trans[err ? 'rollback' : 'commit'](transErr => {
return cb(transErr ? transErr : err); return cb(transErr ? transErr : err);
@ -205,12 +205,12 @@ module.exports = class FileEntry {
const config = Config(); const config = Config();
const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]); const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]);
// absolute paths as-is // absolute paths as-is
if(storageLocation && '/' === storageLocation.charAt(0)) { if(storageLocation && '/' === storageLocation.charAt(0)) {
return storageLocation; return storageLocation;
} }
// relative to |areaStoragePrefix| // relative to |areaStoragePrefix|
return paths.join(config.fileBase.areaStoragePrefix, storageLocation || ''); return paths.join(config.fileBase.areaStoragePrefix, storageLocation || '');
} }
@ -222,9 +222,9 @@ module.exports = class FileEntry {
static quickCheckExistsByPath(fullPath, cb) { static quickCheckExistsByPath(fullPath, cb) {
fileDb.get( fileDb.get(
`SELECT COUNT() AS count `SELECT COUNT() AS count
FROM file FROM file
WHERE file_name = ? WHERE file_name = ?
LIMIT 1;`, LIMIT 1;`,
[ paths.basename(fullPath) ], [ paths.basename(fullPath) ],
(err, rows) => { (err, rows) => {
return err ? cb(err) : cb(null, rows.count > 0 ? true : false); return err ? cb(err) : cb(null, rows.count > 0 ? true : false);
@ -235,7 +235,7 @@ module.exports = class FileEntry {
static persistUserRating(fileId, userId, rating, cb) { static persistUserRating(fileId, userId, rating, cb) {
return fileDb.run( return fileDb.run(
`REPLACE INTO file_user_rating (file_id, user_id, rating) `REPLACE INTO file_user_rating (file_id, user_id, rating)
VALUES (?, ?, ?);`, VALUES (?, ?, ?);`,
[ fileId, userId, rating ], [ fileId, userId, rating ],
cb cb
); );
@ -249,7 +249,7 @@ module.exports = class FileEntry {
return transOrDb.run( return transOrDb.run(
`REPLACE INTO file_meta (file_id, meta_name, meta_value) `REPLACE INTO file_meta (file_id, meta_name, meta_value)
VALUES (?, ?, ?);`, VALUES (?, ?, ?);`,
[ fileId, name, value ], [ fileId, name, value ],
cb cb
); );
@ -259,8 +259,8 @@ module.exports = class FileEntry {
incrementBy = incrementBy || 1; incrementBy = incrementBy || 1;
fileDb.run( fileDb.run(
`UPDATE file_meta `UPDATE file_meta
SET meta_value = meta_value + ? SET meta_value = meta_value + ?
WHERE file_id = ? AND meta_name = ?;`, WHERE file_id = ? AND meta_name = ?;`,
[ incrementBy, fileId, name ], [ incrementBy, fileId, name ],
err => { err => {
if(cb) { if(cb) {
@ -273,8 +273,8 @@ module.exports = class FileEntry {
loadMeta(cb) { loadMeta(cb) {
fileDb.each( fileDb.each(
`SELECT meta_name, meta_value `SELECT meta_name, meta_value
FROM file_meta FROM file_meta
WHERE file_id=?;`, WHERE file_id=?;`,
[ this.fileId ], [ this.fileId ],
(err, meta) => { (err, meta) => {
if(meta) { if(meta) {
@ -297,18 +297,18 @@ module.exports = class FileEntry {
transOrDb.serialize( () => { transOrDb.serialize( () => {
transOrDb.run( transOrDb.run(
`INSERT OR IGNORE INTO hash_tag (hash_tag) `INSERT OR IGNORE INTO hash_tag (hash_tag)
VALUES (?);`, VALUES (?);`,
[ hashTag ] [ hashTag ]
); );
transOrDb.run( transOrDb.run(
`REPLACE INTO file_hash_tag (hash_tag_id, file_id) `REPLACE INTO file_hash_tag (hash_tag_id, file_id)
VALUES ( VALUES (
(SELECT hash_tag_id (SELECT hash_tag_id
FROM hash_tag FROM hash_tag
WHERE hash_tag = ?), WHERE hash_tag = ?),
? ?
);`, );`,
[ hashTag, fileId ], [ hashTag, fileId ],
err => { err => {
return cb(err); return cb(err);
@ -320,12 +320,12 @@ module.exports = class FileEntry {
loadHashTags(cb) { loadHashTags(cb) {
fileDb.each( fileDb.each(
`SELECT ht.hash_tag_id, ht.hash_tag `SELECT ht.hash_tag_id, ht.hash_tag
FROM hash_tag ht FROM hash_tag ht
WHERE ht.hash_tag_id IN ( WHERE ht.hash_tag_id IN (
SELECT hash_tag_id SELECT hash_tag_id
FROM file_hash_tag FROM file_hash_tag
WHERE file_id=? WHERE file_id=?
);`, );`,
[ this.fileId ], [ this.fileId ],
(err, hashTag) => { (err, hashTag) => {
if(hashTag) { if(hashTag) {
@ -341,10 +341,10 @@ module.exports = class FileEntry {
loadRating(cb) { loadRating(cb) {
fileDb.get( fileDb.get(
`SELECT AVG(fur.rating) AS avg_rating `SELECT AVG(fur.rating) AS avg_rating
FROM file_user_rating fur FROM file_user_rating fur
INNER JOIN file f INNER JOIN file f
ON f.file_id = fur.file_id ON f.file_id = fur.file_id
AND f.file_id = ?`, AND f.file_id = ?`,
[ this.fileId ], [ this.fileId ],
(err, result) => { (err, result) => {
if(result) { if(result) {
@ -370,12 +370,12 @@ module.exports = class FileEntry {
} }
static findFileBySha(sha, cb) { static findFileBySha(sha, cb) {
// full or partial SHA-256 // full or partial SHA-256
fileDb.all( fileDb.all(
`SELECT file_id `SELECT file_id
FROM file FROM file
WHERE file_sha256 LIKE "${sha}%" WHERE file_sha256 LIKE "${sha}%"
LIMIT 2;`, // limit 2 such that we can find if there are dupes LIMIT 2;`, // limit 2 such that we can find if there are dupes
(err, fileIdRows) => { (err, fileIdRows) => {
if(err) { if(err) {
return cb(err); return cb(err);
@ -398,14 +398,14 @@ module.exports = class FileEntry {
} }
static findByFileNameWildcard(wc, cb) { static findByFileNameWildcard(wc, cb) {
// convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html
wc = wc.replace(/\*/g, '%').replace(/\?/g, '_'); wc = wc.replace(/\*/g, '%').replace(/\?/g, '_');
fileDb.all( fileDb.all(
`SELECT file_id `SELECT file_id
FROM file FROM file
WHERE file_name LIKE "${wc}" WHERE file_name LIKE "${wc}"
`, `,
(err, fileIdRows) => { (err, fileIdRows) => {
if(err) { if(err) {
return cb(err); return cb(err);
@ -462,38 +462,38 @@ module.exports = class FileEntry {
} }
if(filter.sort && filter.sort.length > 0) { if(filter.sort && filter.sort.length > 0) {
if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value? if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value?
sql = sql =
`SELECT DISTINCT f.file_id `SELECT DISTINCT f.file_id
FROM file f, file_meta m`; FROM file f, file_meta m`;
appendWhereClause(`f.file_id = m.file_id AND m.meta_name = "${filter.sort}"`); appendWhereClause(`f.file_id = m.file_id AND m.meta_name = "${filter.sort}"`);
sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`; sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`;
} else { } else {
// additional special treatment for user ratings: we need to average them // additional special treatment for user ratings: we need to average them
if('user_rating' === filter.sort) { if('user_rating' === filter.sort) {
sql = sql =
`SELECT DISTINCT f.file_id, `SELECT DISTINCT f.file_id,
(SELECT IFNULL(AVG(rating), 0) rating (SELECT IFNULL(AVG(rating), 0) rating
FROM file_user_rating FROM file_user_rating
WHERE file_id = f.file_id) WHERE file_id = f.file_id)
AS avg_rating AS avg_rating
FROM file f`; FROM file f`;
sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`; sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`;
} else { } else {
sql = sql =
`SELECT DISTINCT f.file_id `SELECT DISTINCT f.file_id
FROM file f`; FROM file f`;
sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir; sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir;
} }
} }
} else { } else {
sql = sql =
`SELECT DISTINCT f.file_id `SELECT DISTINCT f.file_id
FROM file f`; FROM file f`;
sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`; sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`;
} }
@ -511,22 +511,22 @@ module.exports = class FileEntry {
filter.metaPairs.forEach(mp => { filter.metaPairs.forEach(mp => {
if(mp.wildcards) { if(mp.wildcards) {
// convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html
mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_'); mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_');
appendWhereClause( appendWhereClause(
`f.file_id IN ( `f.file_id IN (
SELECT file_id SELECT file_id
FROM file_meta FROM file_meta
WHERE meta_name = "${mp.name}" AND meta_value LIKE "${mp.value}" WHERE meta_name = "${mp.name}" AND meta_value LIKE "${mp.value}"
)` )`
); );
} else { } else {
appendWhereClause( appendWhereClause(
`f.file_id IN ( `f.file_id IN (
SELECT file_id SELECT file_id
FROM file_meta FROM file_meta
WHERE meta_name = "${mp.name}" AND meta_value = "${mp.value}" WHERE meta_name = "${mp.name}" AND meta_value = "${mp.value}"
)` )`
); );
} }
}); });
@ -537,30 +537,30 @@ module.exports = class FileEntry {
} }
if(filter.terms && filter.terms.length > 0) { if(filter.terms && filter.terms.length > 0) {
// note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex
appendWhereClause( appendWhereClause(
`f.file_id IN ( `f.file_id IN (
SELECT rowid SELECT rowid
FROM file_fts FROM file_fts
WHERE file_fts MATCH ":${sanatizeString(filter.terms)}" WHERE file_fts MATCH ":${sanatizeString(filter.terms)}"
)` )`
); );
} }
if(filter.tags && filter.tags.length > 0) { if(filter.tags && filter.tags.length > 0) {
// build list of quoted tags; filter.tags comes in as a space and/or comma separated values // build list of quoted tags; filter.tags comes in as a space and/or comma separated values
const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${sanatizeString(tag)}"` ).join(','); const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${sanatizeString(tag)}"` ).join(',');
appendWhereClause( appendWhereClause(
`f.file_id IN ( `f.file_id IN (
SELECT file_id SELECT file_id
FROM file_hash_tag FROM file_hash_tag
WHERE hash_tag_id IN ( WHERE hash_tag_id IN (
SELECT hash_tag_id SELECT hash_tag_id
FROM hash_tag FROM hash_tag
WHERE hash_tag IN (${tags}) WHERE hash_tag IN (${tags})
) )
)` )`
); );
} }
@ -585,7 +585,7 @@ module.exports = class FileEntry {
return cb(err); return cb(err);
} }
if(!rows || 0 === rows.length) { if(!rows || 0 === rows.length) {
return cb(null, []); // no matches return cb(null, []); // no matches
} }
return cb(null, rows.map(r => r.file_id)); return cb(null, rows.map(r => r.file_id));
}); });
@ -602,7 +602,7 @@ module.exports = class FileEntry {
function removeFromDatabase(callback) { function removeFromDatabase(callback) {
fileDb.run( fileDb.run(
`DELETE FROM file `DELETE FROM file
WHERE file_id = ?;`, WHERE file_id = ?;`,
[ srcFileEntry.fileId ], [ srcFileEntry.fileId ],
err => { err => {
return callback(err); return callback(err);
@ -631,20 +631,20 @@ module.exports = class FileEntry {
destFileName = srcFileEntry.fileName; destFileName = srcFileEntry.fileName;
} }
const srcPath = srcFileEntry.filePath; const srcPath = srcFileEntry.filePath;
const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag); const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag);
if(!dstDir) { if(!dstDir) {
return cb(Errors.Invalid('Invalid storage tag')); return cb(Errors.Invalid('Invalid storage tag'));
} }
const dstPath = paths.join(dstDir, destFileName); const dstPath = paths.join(dstDir, destFileName);
async.series( async.series(
[ [
function movePhysFile(callback) { function movePhysFile(callback) {
if(srcPath === dstPath) { if(srcPath === dstPath) {
return callback(null); // don't need to move file, but may change areas return callback(null); // don't need to move file, but may change areas
} }
fse.move(srcPath, dstPath, err => { fse.move(srcPath, dstPath, err => {
@ -654,8 +654,8 @@ module.exports = class FileEntry {
function updateDatabase(callback) { function updateDatabase(callback) {
fileDb.run( fileDb.run(
`UPDATE file `UPDATE file
SET area_tag = ?, file_name = ?, storage_tag = ? SET area_tag = ?, file_name = ?, storage_tag = ?
WHERE file_id = ?;`, WHERE file_id = ?;`,
[ destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId ], [ destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId ],
err => { err => {
return callback(err); return callback(err);

View File

@ -1,50 +1,50 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const DownloadQueue = require('./download_queue.js'); const DownloadQueue = require('./download_queue.js');
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const Events = require('./events.js'); const Events = require('./events.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const pty = require('node-pty'); const pty = require('node-pty');
const temptmp = require('temptmp').createTrackedSession('transfer_file'); const temptmp = require('temptmp').createTrackedSession('transfer_file');
const paths = require('path'); const paths = require('path');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const fse = require('fs-extra'); const fse = require('fs-extra');
// some consts // some consts
const SYSTEM_EOL = require('os').EOL; const SYSTEM_EOL = require('os').EOL;
const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc. const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc.
/* /*
Notes Notes
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
See core/config.js for external protocol configuration See core/config.js for external protocol configuration
Resources Resources
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
ZModem ZModem
* http://gallium.inria.fr/~doligez/zmodem/zmodem.txt * http://gallium.inria.fr/~doligez/zmodem/zmodem.txt
* https://github.com/protomouse/synchronet/blob/master/src/sbbs3/zmodem.c * https://github.com/protomouse/synchronet/blob/master/src/sbbs3/zmodem.c
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'Transfer file', name : 'Transfer file',
desc : 'Sends or receives a file(s)', desc : 'Sends or receives a file(s)',
author : 'NuSkooler', author : 'NuSkooler',
}; };
exports.getModule = class TransferFileModule extends MenuModule { exports.getModule = class TransferFileModule extends MenuModule {
@ -54,7 +54,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
this.config = this.menuConfig.config || {}; this.config = this.menuConfig.config || {};
// //
// Most options can be set via extraArgs or config block // Most options can be set via extraArgs or config block
// //
const config = Config(); const config = Config();
if(options.extraArgs) { if(options.extraArgs) {
@ -99,11 +99,11 @@ exports.getModule = class TransferFileModule extends MenuModule {
} }
} }
this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something* this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something*
this.direction = this.direction || 'send'; this.direction = this.direction || 'send';
this.sendQueue = this.sendQueue || []; this.sendQueue = this.sendQueue || [];
// Ensure sendQueue is an array of objects that contain at least a 'path' member // Ensure sendQueue is an array of objects that contain at least a 'path' member
this.sendQueue = this.sendQueue.map(item => { this.sendQueue = this.sendQueue.map(item => {
if(_.isString(item)) { if(_.isString(item)) {
return { path : item }; return { path : item };
@ -128,8 +128,8 @@ exports.getModule = class TransferFileModule extends MenuModule {
} }
sendFiles(cb) { sendFiles(cb) {
// assume *sending* can always batch // assume *sending* can always batch
// :TODO: Look into this further // :TODO: Look into this further
const allFiles = this.sendQueue.map(f => f.path); const allFiles = this.sendQueue.map(f => f.path);
this.executeExternalProtocolHandlerForSend(allFiles, err => { this.executeExternalProtocolHandlerForSend(allFiles, err => {
if(err) { if(err) {
@ -149,64 +149,64 @@ exports.getModule = class TransferFileModule extends MenuModule {
} }
/* /*
sendFiles(cb) { sendFiles(cb) {
// :TODO: built in/native protocol support // :TODO: built in/native protocol support
if(this.protocolConfig.external.supportsBatch) { if(this.protocolConfig.external.supportsBatch) {
const allFiles = this.sendQueue.map(f => f.path); const allFiles = this.sendQueue.map(f => f.path);
this.executeExternalProtocolHandlerForSend(allFiles, err => { this.executeExternalProtocolHandlerForSend(allFiles, err => {
if(err) { if(err) {
this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' ); this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' );
} else { } else {
const sentFiles = []; const sentFiles = [];
this.sendQueue.forEach(f => { this.sendQueue.forEach(f => {
f.sent = true; f.sent = true;
sentFiles.push(f.path); sentFiles.push(f.path);
}); });
this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` );
} }
return cb(err); return cb(err);
}); });
} else { } else {
// :TODO: we need to prompt between entries such that users can prepare their clients // :TODO: we need to prompt between entries such that users can prepare their clients
async.eachSeries(this.sendQueue, (queueItem, next) => { async.eachSeries(this.sendQueue, (queueItem, next) => {
this.executeExternalProtocolHandlerForSend(queueItem.path, err => { this.executeExternalProtocolHandlerForSend(queueItem.path, err => {
if(err) { if(err) {
this.client.log.warn( { file : queueItem.path, error : err.message }, 'Error sending file' ); this.client.log.warn( { file : queueItem.path, error : err.message }, 'Error sending file' );
} else { } else {
queueItem.sent = true; queueItem.sent = true;
this.client.log.info( { sentFile : queueItem.path }, 'Successfully sent file' ); this.client.log.info( { sentFile : queueItem.path }, 'Successfully sent file' );
} }
return next(err); return next(err);
}); });
}, err => { }, err => {
return cb(err); return cb(err);
}); });
} }
} }
*/ */
moveFileWithCollisionHandling(src, dst, cb) { moveFileWithCollisionHandling(src, dst, cb) {
// //
// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc.
// in the case of collisions. // in the case of collisions.
// //
const dstPath = paths.dirname(dst); const dstPath = paths.dirname(dst);
const dstFileExt = paths.extname(dst); const dstFileExt = paths.extname(dst);
const dstFileSuffix = paths.basename(dst, dstFileExt); const dstFileSuffix = paths.basename(dst, dstFileExt);
let renameIndex = 0; let renameIndex = 0;
let movedOk = false; let movedOk = false;
let tryDstPath; let tryDstPath;
async.until( async.until(
() => movedOk, // until moved OK () => movedOk, // until moved OK
(cb) => { (cb) => {
if(0 === renameIndex) { if(0 === renameIndex) {
// try originally supplied path first // try originally supplied path first
tryDstPath = dst; tryDstPath = dst;
} else { } else {
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`);
@ -216,7 +216,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
if(err) { if(err) {
if('EEXIST' === err.code) { if('EEXIST' === err.code) {
renameIndex += 1; renameIndex += 1;
return cb(null); // keep trying return cb(null); // keep trying
} }
return cb(err); return cb(err);
@ -242,8 +242,8 @@ exports.getModule = class TransferFileModule extends MenuModule {
if(this.recvFileName) { if(this.recvFileName) {
// //
// file name specified - we expect a single file in |this.recvDirectory| // file name specified - we expect a single file in |this.recvDirectory|
// by the name of |this.recvFileName| // by the name of |this.recvFileName|
// //
const recvFullPath = paths.join(this.recvDirectory, this.recvFileName); const recvFullPath = paths.join(this.recvDirectory, this.recvFileName);
fs.stat(recvFullPath, (err, stats) => { fs.stat(recvFullPath, (err, stats) => {
@ -260,21 +260,21 @@ exports.getModule = class TransferFileModule extends MenuModule {
}); });
} else { } else {
// //
// Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already // Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already
// //
fs.readdir(this.recvDirectory, (err, files) => { fs.readdir(this.recvDirectory, (err, files) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
// stat each to grab files only // stat each to grab files only
async.each(files, (fileName, nextFile) => { async.each(files, (fileName, nextFile) => {
const recvFullPath = paths.join(this.recvDirectory, fileName); const recvFullPath = paths.join(this.recvDirectory, fileName);
fs.stat(recvFullPath, (err, stats) => { fs.stat(recvFullPath, (err, stats) => {
if(err) { if(err) {
this.client.log.warn('Failed to stat file', { path : recvFullPath } ); this.client.log.warn('Failed to stat file', { path : recvFullPath } );
return nextFile(null); // just try the next one return nextFile(null); // just try the next one
} }
if(stats.isFile()) { if(stats.isFile()) {
@ -299,7 +299,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
} }
prepAndBuildSendArgs(filePaths, cb) { prepAndBuildSendArgs(filePaths, cb) {
const externalArgs = this.protocolConfig.external['sendArgs']; const externalArgs = this.protocolConfig.external['sendArgs'];
async.waterfall( async.waterfall(
[ [
@ -311,7 +311,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => { temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => {
if(err) { if(err) {
return callback(err); // failed to create it return callback(err); // failed to create it
} }
fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL)); fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL));
@ -321,16 +321,16 @@ exports.getModule = class TransferFileModule extends MenuModule {
}); });
}, },
function createArgs(tempFileListPath, callback) { function createArgs(tempFileListPath, callback) {
// initial args: ignore {filePaths} as we must break that into it's own sep array items // initial args: ignore {filePaths} as we must break that into it's own sep array items
const args = externalArgs.map(arg => { const args = externalArgs.map(arg => {
return '{filePaths}' === arg ? arg : stringFormat(arg, { return '{filePaths}' === arg ? arg : stringFormat(arg, {
fileListPath : tempFileListPath || '', fileListPath : tempFileListPath || '',
}); });
}); });
const filePathsPos = args.indexOf('{filePaths}'); const filePathsPos = args.indexOf('{filePaths}');
if(filePathsPos > -1) { if(filePathsPos > -1) {
// replace {filePaths} with 0:n individual entries in |args| // replace {filePaths} with 0:n individual entries in |args|
args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) ); args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) );
} }
@ -344,19 +344,19 @@ exports.getModule = class TransferFileModule extends MenuModule {
} }
prepAndBuildRecvArgs(cb) { prepAndBuildRecvArgs(cb) {
const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs'; const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs';
const externalArgs = this.protocolConfig.external[argsKey]; const externalArgs = this.protocolConfig.external[argsKey];
const args = externalArgs.map(arg => stringFormat(arg, { const args = externalArgs.map(arg => stringFormat(arg, {
uploadDir : this.recvDirectory, uploadDir : this.recvDirectory,
fileName : this.recvFileName || '', fileName : this.recvFileName || '',
})); }));
return cb(null, args); return cb(null, args);
} }
executeExternalProtocolHandler(args, cb) { executeExternalProtocolHandler(args, cb) {
const external = this.protocolConfig.external; const external = this.protocolConfig.external;
const cmd = external[`${this.direction}Cmd`]; const cmd = external[`${this.direction}Cmd`];
this.client.log.debug( this.client.log.debug(
{ cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction }, { cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction },
@ -364,18 +364,18 @@ exports.getModule = class TransferFileModule extends MenuModule {
); );
const spawnOpts = { const spawnOpts = {
cols : this.client.term.termWidth, cols : this.client.term.termWidth,
rows : this.client.term.termHeight, rows : this.client.term.termHeight,
cwd : this.recvDirectory, cwd : this.recvDirectory,
encoding : null, // don't bork our data! encoding : null, // don't bork our data!
}; };
const externalProc = pty.spawn(cmd, args, spawnOpts); const externalProc = pty.spawn(cmd, args, spawnOpts);
this.client.setTemporaryDirectDataHandler(data => { this.client.setTemporaryDirectDataHandler(data => {
// needed for things like sz/rz // needed for things like sz/rz
if(external.escapeTelnet) { if(external.escapeTelnet) {
const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape
externalProc.write(Buffer.from(tmp, 'binary')); externalProc.write(Buffer.from(tmp, 'binary'));
} else { } else {
externalProc.write(data); externalProc.write(data);
@ -383,9 +383,9 @@ exports.getModule = class TransferFileModule extends MenuModule {
}); });
externalProc.on('data', data => { externalProc.on('data', data => {
// needed for things like sz/rz // needed for things like sz/rz
if(external.escapeTelnet) { if(external.escapeTelnet) {
const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape
this.client.term.rawWrite(Buffer.from(tmp, 'binary')); this.client.term.rawWrite(Buffer.from(tmp, 'binary'));
} else { } else {
this.client.term.rawWrite(data); this.client.term.rawWrite(data);
@ -443,9 +443,9 @@ exports.getModule = class TransferFileModule extends MenuModule {
} }
updateSendStats(cb) { updateSendStats(cb) {
let downloadBytes = 0; let downloadBytes = 0;
let downloadCount = 0; let downloadCount = 0;
let fileIds = []; let fileIds = [];
async.each(this.sendQueue, (queueItem, next) => { async.each(this.sendQueue, (queueItem, next) => {
if(!queueItem.sent) { if(!queueItem.sent) {
@ -462,7 +462,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
return next(null); return next(null);
} }
// we just have a path - figure it out // we just have a path - figure it out
fs.stat(queueItem.path, (err, stats) => { fs.stat(queueItem.path, (err, stats) => {
if(err) { if(err) {
this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' ); this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' );
@ -474,7 +474,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
return next(null); return next(null);
}); });
}, () => { }, () => {
// All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks // All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks
StatLog.incrementUserStat(this.client.user, 'dl_total_count', downloadCount); StatLog.incrementUserStat(this.client.user, 'dl_total_count', downloadCount);
StatLog.incrementUserStat(this.client.user, 'dl_total_bytes', downloadBytes); StatLog.incrementUserStat(this.client.user, 'dl_total_bytes', downloadBytes);
StatLog.incrementSystemStat('dl_total_count', downloadCount); StatLog.incrementSystemStat('dl_total_count', downloadCount);
@ -489,16 +489,16 @@ exports.getModule = class TransferFileModule extends MenuModule {
} }
updateRecvStats(cb) { updateRecvStats(cb) {
let uploadBytes = 0; let uploadBytes = 0;
let uploadCount = 0; let uploadCount = 0;
async.each(this.recvFilePaths, (filePath, next) => { async.each(this.recvFilePaths, (filePath, next) => {
// we just have a path - figure it out // we just have a path - figure it out
fs.stat(filePath, (err, stats) => { fs.stat(filePath, (err, stats) => {
if(err) { if(err) {
this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' ); this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' );
} else { } else {
uploadCount += 1; uploadCount += 1;
uploadBytes += stats.size; uploadBytes += stats.size;
} }
@ -517,7 +517,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
initSequence() { initSequence() {
const self = this; const self = this;
// :TODO: break this up to send|recv // :TODO: break this up to send|recv
async.series( async.series(
[ [
@ -545,16 +545,16 @@ exports.getModule = class TransferFileModule extends MenuModule {
}); });
if(sentFileIds.length > 0) { if(sentFileIds.length > 0) {
// remove items we sent from the D/L queue // remove items we sent from the D/L queue
const dlQueue = new DownloadQueue(self.client); const dlQueue = new DownloadQueue(self.client);
const dlFileEntries = dlQueue.removeItems(sentFileIds); const dlFileEntries = dlQueue.removeItems(sentFileIds);
// fire event for downloaded entries // fire event for downloaded entries
Events.emit( Events.emit(
Events.getSystemEvents().UserDownload, Events.getSystemEvents().UserDownload,
{ {
user : self.client.user, user : self.client.user,
files : dlFileEntries files : dlFileEntries
} }
); );

View File

@ -1,24 +1,24 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File transfer protocol selection', name : 'File transfer protocol selection',
desc : 'Select protocol / method for file transfer', desc : 'Select protocol / method for file transfer',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
protList : 1, protList : 1,
}; };
exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
@ -36,7 +36,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
this.config.direction = this.config.direction || 'send'; this.config.direction = this.config.direction || 'send';
this.extraArgs = options.extraArgs; this.extraArgs = options.extraArgs;
if(_.has(options, 'lastMenuResult.sentFileIds')) { if(_.has(options, 'lastMenuResult.sentFileIds')) {
this.sentFileIds = options.lastMenuResult.sentFileIds; this.sentFileIds = options.lastMenuResult.sentFileIds;
@ -46,13 +46,13 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
this.recvFilePaths = options.lastMenuResult.recvFilePaths; this.recvFilePaths = options.lastMenuResult.recvFilePaths;
} }
this.fallbackOnly = options.lastMenuResult ? true : false; this.fallbackOnly = options.lastMenuResult ? true : false;
this.loadAvailProtocols(); this.loadAvailProtocols();
this.menuMethods = { this.menuMethods = {
selectProtocol : (formData, extraArgs, cb) => { selectProtocol : (formData, extraArgs, cb) => {
const protocol = this.protocols[formData.value.protocol]; const protocol = this.protocols[formData.value.protocol];
const finalExtraArgs = this.extraArgs || {}; const finalExtraArgs = this.extraArgs || {};
Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs ); Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs );
@ -81,7 +81,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
initSequence() { initSequence() {
if(this.sentFileIds || this.recvFilePaths) { if(this.sentFileIds || this.recvFilePaths) {
// nothing to do here; move along (we're just falling through) // nothing to do here; move along (we're just falling through)
this.prevMenu(); this.prevMenu();
} else { } else {
super.initSequence(); super.initSequence();
@ -94,15 +94,15 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu mciMap : mciData.menu
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -110,8 +110,8 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
function populateList(callback) { function populateList(callback) {
const protListView = vc.getView(MciViewIds.protList); const protListView = vc.getView(MciViewIds.protList);
const protListFormat = self.config.protListFormat || '{name}'; const protListFormat = self.config.protListFormat || '{name}';
const protListFocusFormat = self.config.protListFocusFormat || protListFormat; const protListFocusFormat = self.config.protListFocusFormat || protListFormat;
protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) ); protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) );
protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) ); protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) );
@ -131,22 +131,22 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
loadAvailProtocols() { loadAvailProtocols() {
this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => { this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => {
return { return {
protocol : protocol, protocol : protocol,
name : protInfo.name, name : protInfo.name,
hasBatch : _.has(protInfo, 'external.recvArgs'), hasBatch : _.has(protInfo, 'external.recvArgs'),
hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'), hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'),
sort : protInfo.sort, sort : protInfo.sort,
}; };
}); });
// Filter out batch vs non-batch only protocols // Filter out batch vs non-batch only protocols
if(this.extraArgs.recvFileName) { // non-batch aka non-blind if(this.extraArgs.recvFileName) { // non-batch aka non-blind
this.protocols = this.protocols.filter( prot => prot.hasNonBatch ); this.protocols = this.protocols.filter( prot => prot.hasNonBatch );
} else { } else {
this.protocols = this.protocols.filter( prot => prot.hasBatch ); this.protocols = this.protocols.filter( prot => prot.hasBatch );
} }
// natural sort taking explicit orders into consideration // natural sort taking explicit orders into consideration
this.protocols.sort( (a, b) => { this.protocols.sort( (a, b) => {
if(_.isNumber(a.sort) && _.isNumber(b.sort)) { if(_.isNumber(a.sort) && _.isNumber(b.sort)) {
return a.sort - b.sort; return a.sort - b.sort;

View File

@ -1,28 +1,28 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const EnigAssert = require('./enigma_assert.js'); const EnigAssert = require('./enigma_assert.js');
// deps // deps
const fse = require('fs-extra'); const fse = require('fs-extra');
const paths = require('path'); const paths = require('path');
const async = require('async'); const async = require('async');
exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling; exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling;
exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling; exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling;
exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator; exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator;
function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) {
operation = operation || 'copy'; operation = operation || 'copy';
const dstPath = paths.dirname(dst); const dstPath = paths.dirname(dst);
const dstFileExt = paths.extname(dst); const dstFileExt = paths.extname(dst);
const dstFileSuffix = paths.basename(dst, dstFileExt); const dstFileSuffix = paths.basename(dst, dstFileExt);
EnigAssert('move' === operation || 'copy' === operation); EnigAssert('move' === operation || 'copy' === operation);
let renameIndex = 0; let renameIndex = 0;
let opOk = false; let opOk = false;
let tryDstPath; let tryDstPath;
function tryOperation(src, dst, callback) { function tryOperation(src, dst, callback) {
@ -38,10 +38,10 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) {
} }
async.until( async.until(
() => opOk, // until moved OK () => opOk, // until moved OK
(cb) => { (cb) => {
if(0 === renameIndex) { if(0 === renameIndex) {
// try originally supplied path first // try originally supplied path first
tryDstPath = dst; tryDstPath = dst;
} else { } else {
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`);
@ -49,11 +49,11 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) {
tryOperation(src, tryDstPath, err => { tryOperation(src, tryDstPath, err => {
if(err) { if(err) {
// for some reason fs-extra copy doesn't pass err.code // for some reason fs-extra copy doesn't pass err.code
// :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST // :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST
if('EEXIST' === err.code || 'copy' === operation) { if('EEXIST' === err.code || 'copy' === operation) {
renameIndex += 1; renameIndex += 1;
return cb(null); // keep trying return cb(null); // keep trying
} }
return cb(err); return cb(err);
@ -70,8 +70,8 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) {
} }
// //
// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc.
// in the case of collisions. // in the case of collisions.
// //
function moveFileWithCollisionHandling(src, dst, cb) { function moveFileWithCollisionHandling(src, dst, cb) {
return moveOrCopyFileWithCollisionHandling(src, dst, 'move', cb); return moveOrCopyFileWithCollisionHandling(src, dst, 'move', cb);

View File

@ -1,9 +1,9 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
let _ = require('lodash'); let _ = require('lodash');
// FNV-1a based on work here: https://github.com/wiedi/node-fnv // FNV-1a based on work here: https://github.com/wiedi/node-fnv
module.exports = class FNV1a { module.exports = class FNV1a {
constructor(data) { constructor(data) {
this.hash = 0x811c9dc5; this.hash = 0x811c9dc5;
@ -29,8 +29,8 @@ module.exports = class FNV1a {
for(let b of data) { for(let b of data) {
this.hash = this.hash ^ b; this.hash = this.hash ^ b;
this.hash += this.hash +=
(this.hash << 24) + (this.hash << 8) + (this.hash << 7) + (this.hash << 24) + (this.hash << 8) + (this.hash << 7) +
(this.hash << 4) + (this.hash << 1); (this.hash << 4) + (this.hash << 1);
} }
return this; return this;

View File

@ -1,118 +1,118 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const theme = require('./theme.js'); const theme = require('./theme.js');
const Message = require('./message.js'); const Message = require('./message.js');
const updateMessageAreaLastReadId = require('./message_area.js').updateMessageAreaLastReadId; const updateMessageAreaLastReadId = require('./message_area.js').updateMessageAreaLastReadId;
const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag;
const User = require('./user.js'); const User = require('./user.js');
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher;
const { isAnsi, cleanControlCodes, insert } = require('./string_util.js'); const { isAnsi, cleanControlCodes, insert } = require('./string_util.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const { getAddressedToInfo } = require('./mail_util.js'); const { getAddressedToInfo } = require('./mail_util.js');
// deps // deps
const async = require('async'); const async = require('async');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Full Screen Editor (FSE)', name : 'Full Screen Editor (FSE)',
desc : 'A full screen editor/viewer', desc : 'A full screen editor/viewer',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
header : { header : {
from : 1, from : 1,
to : 2, to : 2,
subject : 3, subject : 3,
errorMsg : 4, errorMsg : 4,
modTimestamp : 5, modTimestamp : 5,
msgNum : 6, msgNum : 6,
msgTotal : 7, msgTotal : 7,
customRangeStart : 10, // 10+ = customs customRangeStart : 10, // 10+ = customs
}, },
body : { body : {
message : 1, message : 1,
}, },
// :TODO: quote builder MCIs - remove all magic #'s // :TODO: quote builder MCIs - remove all magic #'s
// :TODO: consolidate all footer MCI's - remove all magic #'s // :TODO: consolidate all footer MCI's - remove all magic #'s
ViewModeFooter : { ViewModeFooter : {
MsgNum : 6, MsgNum : 6,
MsgTotal : 7, MsgTotal : 7,
// :TODO: Just use custom ranges // :TODO: Just use custom ranges
}, },
quoteBuilder : { quoteBuilder : {
quotedMsg : 1, quotedMsg : 1,
// 2 NYI // 2 NYI
quoteLines : 3, quoteLines : 3,
} }
}; };
/* /*
Custom formatting: Custom formatting:
header header
fromUserName fromUserName
toUserName toUserName
fromRealName (may be fromUserName) NYI fromRealName (may be fromUserName) NYI
toRealName (may be toUserName) NYI toRealName (may be toUserName) NYI
fromRemoteUser (may be "N/A") fromRemoteUser (may be "N/A")
toRemoteUser (may be "N/A") toRemoteUser (may be "N/A")
subject subject
modTimestamp modTimestamp
msgNum msgNum
msgTotal (in area) msgTotal (in area)
messageId messageId
*/ */
// :TODO: convert code in this class to newer styles, conventions, etc. There is a lot of experimental stuff here that has better (DRY) alternatives // :TODO: convert code in this class to newer styles, conventions, etc. There is a lot of experimental stuff here that has better (DRY) alternatives
exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModule extends MessageAreaConfTempSwitcher(MenuModule) { exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModule extends MessageAreaConfTempSwitcher(MenuModule) {
constructor(options) { constructor(options) {
super(options); super(options);
const self = this; const self = this;
const config = this.menuConfig.config; const config = this.menuConfig.config;
// //
// menuConfig.config: // menuConfig.config:
// editorType : email | area // editorType : email | area
// editorMode : view | edit | quote // editorMode : view | edit | quote
// //
// menuConfig.config or extraArgs // menuConfig.config or extraArgs
// messageAreaTag // messageAreaTag
// messageIndex / messageTotal // messageIndex / messageTotal
// toUserId // toUserId
// //
this.editorType = config.editorType; this.editorType = config.editorType;
this.editorMode = config.editorMode; this.editorMode = config.editorMode;
if(config.messageAreaTag) { if(config.messageAreaTag) {
// :TODO: swtich to this.config.messageAreaTag so we can follow Object.assign pattern for config/extraArgs // :TODO: swtich to this.config.messageAreaTag so we can follow Object.assign pattern for config/extraArgs
this.messageAreaTag = config.messageAreaTag; this.messageAreaTag = config.messageAreaTag;
} }
this.messageIndex = config.messageIndex || 0; this.messageIndex = config.messageIndex || 0;
this.messageTotal = config.messageTotal || 0; this.messageTotal = config.messageTotal || 0;
this.toUserId = config.toUserId || 0; this.toUserId = config.toUserId || 0;
// extraArgs can override some config // extraArgs can override some config
if(_.isObject(options.extraArgs)) { if(_.isObject(options.extraArgs)) {
if(options.extraArgs.messageAreaTag) { if(options.extraArgs.messageAreaTag) {
this.messageAreaTag = options.extraArgs.messageAreaTag; this.messageAreaTag = options.extraArgs.messageAreaTag;
@ -140,7 +140,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
this.menuMethods = { this.menuMethods = {
// //
// Validation stuff // Validation stuff
// //
viewValidationListener : function(err, cb) { viewValidationListener : function(err, cb) {
var errMsgView = self.viewControllers.header.getView(MciViewIds.header.errorMsg); var errMsgView = self.viewControllers.header.getView(MciViewIds.header.errorMsg);
@ -150,7 +150,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
errMsgView.setText(err.message); errMsgView.setText(err.message);
if(MciViewIds.header.subject === err.view.getId()) { if(MciViewIds.header.subject === err.view.getId()) {
// :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel) // :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel)
} }
} else { } else {
errMsgView.clearText(); errMsgView.clearText();
@ -201,19 +201,19 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
if(self.newQuoteBlock) { if(self.newQuoteBlock) {
self.newQuoteBlock = false; self.newQuoteBlock = false;
// :TODO: If replying to ANSI, add a blank sepration line here // :TODO: If replying to ANSI, add a blank sepration line here
quoteMsgView.addText(self.getQuoteByHeader()); quoteMsgView.addText(self.getQuoteByHeader());
} }
const quoteListView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines); const quoteListView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines);
const quoteText = quoteListView.getItem(formData.value.quote); const quoteText = quoteListView.getItem(formData.value.quote);
quoteMsgView.addText(quoteText); quoteMsgView.addText(quoteText);
// //
// If this is *not* the last item, advance. Otherwise, do nothing as we // If this is *not* the last item, advance. Otherwise, do nothing as we
// don't want to jump back to the top and repeat already quoted lines // don't want to jump back to the top and repeat already quoted lines
// //
if(quoteListView.getData() !== quoteListView.getCount() - 1) { if(quoteListView.getData() !== quoteListView.getCount() - 1) {
@ -229,18 +229,18 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
return cb(null); return cb(null);
}, },
/* /*
replyDiscard : function(formData, extraArgs) { replyDiscard : function(formData, extraArgs) {
// :TODO: need to prompt yes/no // :TODO: need to prompt yes/no
// :TODO: @method for fallback would be better // :TODO: @method for fallback would be better
self.prevMenu(); self.prevMenu();
}, },
*/ */
editModeMenuHelp : function(formData, extraArgs, cb) { editModeMenuHelp : function(formData, extraArgs, cb) {
self.viewControllers.footerEditorMenu.setFocus(false); self.viewControllers.footerEditorMenu.setFocus(false);
return self.displayHelp(cb); return self.displayHelp(cb);
}, },
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
// View Mode // View Mode
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
viewModeMenuHelp : function(formData, extraArgs, cb) { viewModeMenuHelp : function(formData, extraArgs, cb) {
self.viewControllers.footerView.setFocus(false); self.viewControllers.footerView.setFocus(false);
@ -266,43 +266,43 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
} }
getFooterName() { getFooterName() {
return 'footer' + _.upperFirst(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ... return 'footer' + _.upperFirst(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ...
} }
getFormId(name) { getFormId(name) {
return { return {
header : 0, header : 0,
body : 1, body : 1,
footerEditor : 2, footerEditor : 2,
footerEditorMenu : 3, footerEditorMenu : 3,
footerView : 4, footerView : 4,
quoteBuilder : 5, quoteBuilder : 5,
help : 50, help : 50,
}[name]; }[name];
} }
getHeaderFormatObj() { getHeaderFormatObj() {
const remoteUserNotAvail = this.menuConfig.config.remoteUserNotAvail || 'N/A'; const remoteUserNotAvail = this.menuConfig.config.remoteUserNotAvail || 'N/A';
const localUserIdNotAvail = this.menuConfig.config.localUserIdNotAvail || 'N/A'; const localUserIdNotAvail = this.menuConfig.config.localUserIdNotAvail || 'N/A';
const modTimestampFormat = this.menuConfig.config.modTimestampFormat || this.client.currentTheme.helpers.getDateTimeFormat(); const modTimestampFormat = this.menuConfig.config.modTimestampFormat || this.client.currentTheme.helpers.getDateTimeFormat();
return { return {
// :TODO: ensure we show real names for form/to if they are enforced in the area // :TODO: ensure we show real names for form/to if they are enforced in the area
fromUserName : this.message.fromUserName, fromUserName : this.message.fromUserName,
toUserName : this.message.toUserName, toUserName : this.message.toUserName,
// :TODO: // :TODO:
//fromRealName //fromRealName
//toRealName //toRealName
fromUserId : _.get(this.message, 'meta.System.local_from_user_id', localUserIdNotAvail), fromUserId : _.get(this.message, 'meta.System.local_from_user_id', localUserIdNotAvail),
toUserId : _.get(this.message, 'meta.System.local_to_user_id', localUserIdNotAvail), toUserId : _.get(this.message, 'meta.System.local_to_user_id', localUserIdNotAvail),
fromRemoteUser : _.get(this.message, 'meta.System.remote_from_user', remoteUserNotAvail), fromRemoteUser : _.get(this.message, 'meta.System.remote_from_user', remoteUserNotAvail),
toRemoteUser : _.get(this.messgae, 'meta.System.remote_to_user', remoteUserNotAvail), toRemoteUser : _.get(this.messgae, 'meta.System.remote_to_user', remoteUserNotAvail),
subject : this.message.subject, subject : this.message.subject,
modTimestamp : this.message.modTimestamp.format(modTimestampFormat), modTimestamp : this.message.modTimestamp.format(modTimestampFormat),
msgNum : this.messageIndex + 1, msgNum : this.messageIndex + 1,
msgTotal : this.messageTotal, msgTotal : this.messageTotal,
messageId : this.message.messageId, messageId : this.message.messageId,
}; };
} }
@ -317,26 +317,26 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
const headerValues = this.viewControllers.header.getFormData().value; const headerValues = this.viewControllers.header.getFormData().value;
const msgOpts = { const msgOpts = {
areaTag : this.messageAreaTag, areaTag : this.messageAreaTag,
toUserName : headerValues.to, toUserName : headerValues.to,
fromUserName : this.client.user.username, fromUserName : this.client.user.username,
subject : headerValues.subject, subject : headerValues.subject,
// :TODO: don't hard code 1 here: // :TODO: don't hard code 1 here:
message : this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ), message : this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ),
}; };
if(this.isReply()) { if(this.isReply()) {
msgOpts.replyToMsgId = this.replyToMessage.messageId; msgOpts.replyToMsgId = this.replyToMessage.messageId;
if(this.replyIsAnsi) { if(this.replyIsAnsi) {
// //
// Ensure first characters indicate ANSI for detection down // Ensure first characters indicate ANSI for detection down
// the line (other boards/etc.). We also set explicit_encoding // the line (other boards/etc.). We also set explicit_encoding
// to packetAnsiMsgEncoding (generally cp437) as various boards // to packetAnsiMsgEncoding (generally cp437) as various boards
// really don't like ANSI messages in UTF-8 encoding (they should!) // really don't like ANSI messages in UTF-8 encoding (they should!)
// //
msgOpts.meta = { System : { 'explicit_encoding' : _.get(Config(), 'scannerTossers.ftn_bso.packetAnsiMsgEncoding', 'cp437') } }; msgOpts.meta = { System : { 'explicit_encoding' : _.get(Config(), 'scannerTossers.ftn_bso.packetAnsiMsgEncoding', 'cp437') } };
msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${msgOpts.message}`; msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${msgOpts.message}`;
} }
} }
@ -363,18 +363,18 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
this.initHeaderViewMode(); this.initHeaderViewMode();
this.initFooterViewMode(); this.initFooterViewMode();
const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message);
let msg = this.message.message; let msg = this.message.message;
if(bodyMessageView && _.has(this, 'message.message')) { if(bodyMessageView && _.has(this, 'message.message')) {
// //
// We handle ANSI messages differently than standard messages -- this is required as // We handle ANSI messages differently than standard messages -- this is required as
// we don't want to do things like word wrap ANSI, but instead, trust that it's formatted // we don't want to do things like word wrap ANSI, but instead, trust that it's formatted
// how the author wanted it // how the author wanted it
// //
if(isAnsi(msg)) { if(isAnsi(msg)) {
// //
// Find tearline - we want to color it differently. // Find tearline - we want to color it differently.
// //
const tearLinePos = this.message.getTearLinePosition(msg); const tearLinePos = this.message.getTearLinePosition(msg);
@ -383,10 +383,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
} }
bodyMessageView.setAnsi( bodyMessageView.setAnsi(
msg.replace(/\r?\n/g, '\r\n'), // messages are stored with CRLF -> LF msg.replace(/\r?\n/g, '\r\n'), // messages are stored with CRLF -> LF
{ {
prepped : false, prepped : false,
forceLineTerm : true, forceLineTerm : true,
} }
); );
} else { } else {
@ -404,7 +404,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
[ [
function buildIfNecessary(callback) { function buildIfNecessary(callback) {
if(self.isEditMode()) { if(self.isEditMode()) {
return self.buildMessage(callback); // creates initial self.message return self.buildMessage(callback); // creates initial self.message
} }
return callback(null); return callback(null);
@ -422,9 +422,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
} }
// //
// If the message we're replying to is from a remote user // If the message we're replying to is from a remote user
// don't try to look up the local user ID. Instead, mark the mail // don't try to look up the local user ID. Instead, mark the mail
// for export with the remote to address. // for export with the remote to address.
// //
if(self.replyToMessage && self.replyToMessage.isFromRemoteUser()) { if(self.replyToMessage && self.replyToMessage.isFromRemoteUser()) {
self.message.setRemoteToUser(self.replyToMessage.meta.System[Message.SystemMetaNames.RemoteFromUser]); self.message.setRemoteToUser(self.replyToMessage.meta.System[Message.SystemMetaNames.RemoteFromUser]);
@ -433,9 +433,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
} }
// //
// Detect if the user is attempting to send to a remote mail type that we support // Detect if the user is attempting to send to a remote mail type that we support
// //
// :TODO: how to plug in support without tying to various types here? isSupportedExteranlType() or such // :TODO: how to plug in support without tying to various types here? isSupportedExteranlType() or such
const addressedToInfo = getAddressedToInfo(self.message.toUserName); const addressedToInfo = getAddressedToInfo(self.message.toUserName);
if(addressedToInfo.name && Message.AddressFlavor.FTN === addressedToInfo.flavor) { if(addressedToInfo.name && Message.AddressFlavor.FTN === addressedToInfo.flavor) {
self.message.setRemoteToUser(addressedToInfo.remote); self.message.setRemoteToUser(addressedToInfo.remote);
@ -444,7 +444,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
return callback(null); return callback(null);
} }
// we need to look it up // we need to look it up
User.getUserIdAndNameByLookup(self.message.toUserName, (err, toUserId) => { User.getUserIdAndNameByLookup(self.message.toUserName, (err, toUserId) => {
if(err) { if(err) {
return callback(err); return callback(err);
@ -466,7 +466,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
if(cb) { if(cb) {
cb(null); cb(null);
} }
return; // don't inc stats for private messages return; // don't inc stats for private messages
} }
return StatLog.incrementUserStat(this.client.user, 'post_count', 1, cb); return StatLog.incrementUserStat(this.client.user, 'post_count', 1, cb);
@ -479,9 +479,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
[ [
function moveToFooterPosition(callback) { function moveToFooterPosition(callback) {
// //
// Calculate footer starting position // Calculate footer starting position
// //
// row = (header height + body height) // row = (header height + body height)
// //
var footerRow = self.header.height + self.body.height; var footerRow = self.header.height + self.body.height;
self.client.term.rawWrite(ansi.goto(footerRow, 1)); self.client.term.rawWrite(ansi.goto(footerRow, 1));
@ -489,10 +489,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}, },
function clearFooterArea(callback) { function clearFooterArea(callback) {
if(options.clear) { if(options.clear) {
// footer up to 3 rows in height // footer up to 3 rows in height
// :TODO: We'd like to delete up to N rows, but this does not work // :TODO: We'd like to delete up to N rows, but this does not work
// in NetRunner: // in NetRunner:
self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3)); self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3));
self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2)); self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2));
@ -519,9 +519,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
} }
redrawScreen(cb) { redrawScreen(cb) {
var comps = [ 'header', 'body' ]; var comps = [ 'header', 'body' ];
const self = this; const self = this;
var art = self.menuConfig.config.art; var art = self.menuConfig.config.art;
self.client.term.rawWrite(ansi.resetScreen()); self.client.term.rawWrite(ansi.resetScreen());
@ -543,7 +543,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}); });
}, },
function displayFooter(callback) { function displayFooter(callback) {
// we have to treat the footer special // we have to treat the footer special
self.redrawFooter( { clear : false, footerName : self.getFooterName() }, function footerDisplayed(err) { self.redrawFooter( { clear : false, footerName : self.getFooterName() }, function footerDisplayed(err) {
callback(err); callback(err);
}); });
@ -577,9 +577,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
if(_.isUndefined(this.viewControllers[footerName])) { if(_.isUndefined(this.viewControllers[footerName])) {
var menuLoadOpts = { var menuLoadOpts = {
callingMenu : this, callingMenu : this,
formId : formId, formId : formId,
mciMap : artData.mciMap mciMap : artData.mciMap
}; };
this.addViewController( this.addViewController(
@ -597,8 +597,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
initSequence() { initSequence() {
var mciData = { }; var mciData = { };
const self = this; const self = this;
var art = self.menuConfig.config.art; var art = self.menuConfig.config.art;
assert(_.isObject(art)); assert(_.isObject(art));
@ -659,7 +659,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
[ [
function header(callback) { function header(callback) {
menuLoadOpts.formId = self.getFormId('header'); menuLoadOpts.formId = self.getFormId('header');
menuLoadOpts.mciMap = mciData.header.mciMap; menuLoadOpts.mciMap = mciData.header.mciMap;
self.addViewController( self.addViewController(
'header', 'header',
@ -669,8 +669,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}); });
}, },
function body(callback) { function body(callback) {
menuLoadOpts.formId = self.getFormId('body'); menuLoadOpts.formId = self.getFormId('body');
menuLoadOpts.mciMap = mciData.body.mciMap; menuLoadOpts.mciMap = mciData.body.mciMap;
self.addViewController( self.addViewController(
'body', 'body',
@ -698,12 +698,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
from.acceptsFocus = false; from.acceptsFocus = false;
//from.setText(self.client.user.username); //from.setText(self.client.user.username);
// :TODO: make this a method // :TODO: make this a method
var body = self.viewControllers.body.getView(MciViewIds.body.message); var body = self.viewControllers.body.getView(MciViewIds.body.message);
self.updateTextEditMode(body.getTextEditMode()); self.updateTextEditMode(body.getTextEditMode());
self.updateEditModePosition(body.getEditPosition()); self.updateEditModePosition(body.getEditPosition());
// :TODO: If view mode, set body to read only... which needs an impl... // :TODO: If view mode, set body to read only... which needs an impl...
callback(null); callback(null);
}, },
@ -767,22 +767,22 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
mciReadyHandler(mciData, cb) { mciReadyHandler(mciData, cb) {
this.createInitialViews(mciData, err => { this.createInitialViews(mciData, err => {
// :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in // :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in
// place - if this is for existing usernames else validate spec // place - if this is for existing usernames else validate spec
/* /*
self.viewControllers.header.on('leave', function headerViewLeave(view) { self.viewControllers.header.on('leave', function headerViewLeave(view) {
if(2 === view.id) { // "to" field if(2 === view.id) { // "to" field
self.validateToUserName(view.getData(), function result(err) { self.validateToUserName(view.getData(), function result(err) {
if(err) { if(err) {
// :TODO: display a error in a %TL area or such // :TODO: display a error in a %TL area or such
view.clearText(); view.clearText();
self.viewControllers.headers.switchFocus(2); self.viewControllers.headers.switchFocus(2);
} }
}); });
} }
});*/ });*/
cb(err); cb(err);
}); });
@ -793,7 +793,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
var posView = this.viewControllers.footerEditor.getView(1); var posView = this.viewControllers.footerEditor.getView(1);
if(posView) { if(posView) {
this.client.term.rawWrite(ansi.savePos()); this.client.term.rawWrite(ansi.savePos());
// :TODO: Use new formatting techniques here, e.g. state.cursorPositionRow, cursorPositionCol and cursorPositionFormat // :TODO: Use new formatting techniques here, e.g. state.cursorPositionRow, cursorPositionCol and cursorPositionFormat
posView.setText(_.padStart(String(pos.row + 1), 2, '0') + ',' + _.padEnd(String(pos.col + 1), 2, '0')); posView.setText(_.padStart(String(pos.row + 1), 2, '0') + ',' + _.padEnd(String(pos.col + 1), 2, '0'));
this.client.term.rawWrite(ansi.restorePos()); this.client.term.rawWrite(ansi.restorePos());
} }
@ -816,16 +816,16 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
} }
initHeaderViewMode() { initHeaderViewMode() {
this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); this.setHeaderText(MciViewIds.header.from, this.message.fromUserName);
this.setHeaderText(MciViewIds.header.to, this.message.toUserName); this.setHeaderText(MciViewIds.header.to, this.message.toUserName);
this.setHeaderText(MciViewIds.header.subject, this.message.subject); this.setHeaderText(MciViewIds.header.subject, this.message.subject);
this.setHeaderText(MciViewIds.header.modTimestamp, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat())); this.setHeaderText(MciViewIds.header.modTimestamp, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat()));
this.setHeaderText(MciViewIds.header.msgNum, (this.messageIndex + 1).toString()); this.setHeaderText(MciViewIds.header.msgNum, (this.messageIndex + 1).toString());
this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString()); this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString());
this.updateCustomViewTextsWithFilter('header', MciViewIds.header.customRangeStart, this.getHeaderFormatObj()); this.updateCustomViewTextsWithFilter('header', MciViewIds.header.customRangeStart, this.getHeaderFormatObj());
// if we changed conf/area we need to update any related standard MCI view // if we changed conf/area we need to update any related standard MCI view
this.refreshPredefinedMciViewsByCode('header', [ 'MA', 'MC', 'ML', 'CM' ] ); this.refreshPredefinedMciViewsByCode('header', [ 'MA', 'MC', 'ML', 'CM' ] );
} }
@ -835,15 +835,15 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
this.setHeaderText(MciViewIds.header.to, this.replyToMessage.fromUserName); this.setHeaderText(MciViewIds.header.to, this.replyToMessage.fromUserName);
// //
// We want to prefix the subject with "RE: " only if it's not already // We want to prefix the subject with "RE: " only if it's not already
// that way -- avoid RE: RE: RE: RE: ... // that way -- avoid RE: RE: RE: RE: ...
// //
let newSubj = this.replyToMessage.subject; let newSubj = this.replyToMessage.subject;
if(false === /^RE:\s+/i.test(newSubj)) { if(false === /^RE:\s+/i.test(newSubj)) {
newSubj = `RE: ${newSubj}`; newSubj = `RE: ${newSubj}`;
} }
this.setHeaderText(MciViewIds.header.subject, newSubj); this.setHeaderText(MciViewIds.header.subject, newSubj);
} }
initFooterViewMode() { initFooterViewMode() {
@ -869,7 +869,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
displayQuoteBuilder() { displayQuoteBuilder() {
// //
// Clear body area // Clear body area
// //
this.newQuoteBlock = true; this.newQuoteBlock = true;
const self = this; const self = this;
@ -877,10 +877,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
async.waterfall( async.waterfall(
[ [
function clearAndDisplayArt(callback) { function clearAndDisplayArt(callback) {
// :TODO: NetRunner does NOT support delete line, so this does not work: // :TODO: NetRunner does NOT support delete line, so this does not work:
self.client.term.rawWrite( self.client.term.rawWrite(
ansi.goto(self.header.height + 1, 1) + ansi.goto(self.header.height + 1, 1) +
ansi.deleteLine((self.client.term.termHeight - self.header.height) - 1)); ansi.deleteLine((self.client.term.termHeight - self.header.height) - 1));
theme.displayThemeArt( { name : self.menuConfig.config.art.quote, client : self.client }, function displayed(err, artData) { theme.displayThemeArt( { name : self.menuConfig.config.art.quote, client : self.client }, function displayed(err, artData) {
callback(err, artData); callback(err, artData);
@ -891,9 +891,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
if(_.isUndefined(self.viewControllers.quoteBuilder)) { if(_.isUndefined(self.viewControllers.quoteBuilder)) {
var menuLoadOpts = { var menuLoadOpts = {
callingMenu : self, callingMenu : self,
formId : formId, formId : formId,
mciMap : artData.mciMap, mciMap : artData.mciMap,
}; };
self.addViewController( self.addViewController(
@ -909,16 +909,16 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}, },
function loadQuoteLines(callback) { function loadQuoteLines(callback) {
const quoteView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines); const quoteView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines);
const bodyView = self.viewControllers.body.getView(MciViewIds.body.message); const bodyView = self.viewControllers.body.getView(MciViewIds.body.message);
self.replyToMessage.getQuoteLines( self.replyToMessage.getQuoteLines(
{ {
termWidth : self.client.term.termWidth, termWidth : self.client.term.termWidth,
termHeight : self.client.term.termHeight, termHeight : self.client.term.termHeight,
cols : quoteView.dimens.width, cols : quoteView.dimens.width,
startCol : quoteView.position.col, startCol : quoteView.position.col,
ansiResetSgr : bodyView.styleSGR1, ansiResetSgr : bodyView.styleSGR1,
ansiFocusPrefixSgr : quoteView.styleSGR2, ansiFocusPrefixSgr : quoteView.styleSGR2,
}, },
(err, quoteLines, focusQuoteLines, replyIsAnsi) => { (err, quoteLines, focusQuoteLines, replyIsAnsi) => {
if(err) { if(err) {
@ -959,16 +959,16 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
} }
/* /*
this.observeViewPosition = function() { this.observeViewPosition = function() {
self.viewControllers.body.getView(MciViewIds.body.message).on('edit position', function positionUpdate(pos) { self.viewControllers.body.getView(MciViewIds.body.message).on('edit position', function positionUpdate(pos) {
console.log(pos.percent + ' / ' + pos.below) console.log(pos.percent + ' / ' + pos.below)
}); });
}; };
*/ */
switchToHeader() { switchToHeader() {
this.viewControllers.body.setFocus(false); this.viewControllers.body.setFocus(false);
this.viewControllers.header.switchFocus(2); // to this.viewControllers.header.switchFocus(2); // to
} }
switchToBody() { switchToBody() {
@ -982,7 +982,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
this.viewControllers.header.setFocus(false); this.viewControllers.header.setFocus(false);
this.viewControllers.body.setFocus(false); this.viewControllers.body.setFocus(false);
this.viewControllers[this.getFooterName()].switchFocus(1); // HM1 this.viewControllers[this.getFooterName()].switchFocus(1); // HM1
} }
switchFromQuoteBuilderToBody() { switchFromQuoteBuilderToBody() {
@ -991,7 +991,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
body.redraw(); body.redraw();
this.viewControllers.body.switchFocus(1); this.viewControllers.body.switchFocus(1);
// :TODO: create method (DRY) // :TODO: create method (DRY)
this.updateTextEditMode(body.getTextEditMode()); this.updateTextEditMode(body.getTextEditMode());
this.updateEditModePosition(body.getEditPosition()); this.updateEditModePosition(body.getEditPosition());
@ -1000,11 +1000,11 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
} }
quoteBuilderFinalize() { quoteBuilderFinalize() {
// :TODO: fix magic #'s // :TODO: fix magic #'s
const quoteMsgView = this.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg); const quoteMsgView = this.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg);
const msgView = this.viewControllers.body.getView(MciViewIds.body.message); const msgView = this.viewControllers.body.getView(MciViewIds.body.message);
let quoteLines = quoteMsgView.getData().trim(); let quoteLines = quoteMsgView.getData().trim();
if(quoteLines.length > 0) { if(quoteLines.length > 0) {
if(this.replyIsAnsi) { if(this.replyIsAnsi) {
@ -1034,8 +1034,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
const dtFormat = this.menuConfig.config.quoteDateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat(); const dtFormat = this.menuConfig.config.quoteDateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat();
return stringFormat(quoteFormat, { return stringFormat(quoteFormat, {
dateTime : moment(this.replyToMessage.modTimestamp).format(dtFormat), dateTime : moment(this.replyToMessage.modTimestamp).format(dtFormat),
userName : this.replyToMessage.fromUserName, userName : this.replyToMessage.fromUserName,
}); });
} }

View File

@ -1,7 +1,7 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const _ = require('lodash'); const _ = require('lodash');
const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-.]+)?$/i; const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-.]+)?$/i;
const FTN_PATTERN_REGEXP = /^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i; const FTN_PATTERN_REGEXP = /^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i;
@ -25,7 +25,7 @@ module.exports = class Address {
} }
isValid() { isValid() {
// FTN address is valid if we have at least a net/node // FTN address is valid if we have at least a net/node
return _.isNumber(this.net) && _.isNumber(this.node); return _.isNumber(this.net) && _.isNumber(this.node);
} }
@ -36,10 +36,10 @@ module.exports = class Address {
return ( return (
this.net === other.net && this.net === other.net &&
this.node === other.node && this.node === other.node &&
this.zone === other.zone && this.zone === other.zone &&
this.point === other.point && this.point === other.point &&
this.domain === other.domain this.domain === other.domain
); );
} }
@ -95,36 +95,36 @@ module.exports = class Address {
} }
/* /*
getMatchScore(pattern) { getMatchScore(pattern) {
let score = 0; let score = 0;
const addr = this.getMatchAddr(pattern); const addr = this.getMatchAddr(pattern);
if(addr) { if(addr) {
const PARTS = [ 'net', 'node', 'zone', 'point', 'domain' ]; const PARTS = [ 'net', 'node', 'zone', 'point', 'domain' ];
for(let i = 0; i < PARTS.length; ++i) { for(let i = 0; i < PARTS.length; ++i) {
const member = PARTS[i]; const member = PARTS[i];
if(this[member] === addr[member]) { if(this[member] === addr[member]) {
score += 2; score += 2;
} else if('*' === addr[member]) { } else if('*' === addr[member]) {
score += 1; score += 1;
} else { } else {
break; break;
} }
} }
} }
return score; return score;
} }
*/ */
isPatternMatch(pattern) { isPatternMatch(pattern) {
const addr = this.getMatchAddr(pattern); const addr = this.getMatchAddr(pattern);
if(addr) { if(addr) {
return ( return (
('*' === addr.net || this.net === addr.net) && ('*' === addr.net || this.net === addr.net) &&
('*' === addr.node || this.node === addr.node) && ('*' === addr.node || this.node === addr.node) &&
('*' === addr.zone || this.zone === addr.zone) && ('*' === addr.zone || this.zone === addr.zone) &&
('*' === addr.point || this.point === addr.point) && ('*' === addr.point || this.point === addr.point) &&
('*' === addr.domain || this.domain === addr.domain) ('*' === addr.domain || this.domain === addr.domain)
); );
} }
@ -137,8 +137,8 @@ module.exports = class Address {
if(m) { if(m) {
// start with a 2D // start with a 2D
let addr = { let addr = {
net : parseInt(m[2]), net : parseInt(m[2]),
node : parseInt(m[3].substr(1)), node : parseInt(m[3].substr(1)),
}; };
// 3D: Addition of zone if present // 3D: Addition of zone if present
@ -165,14 +165,14 @@ module.exports = class Address {
let addrStr = `${this.zone}:${this.net}`; let addrStr = `${this.zone}:${this.net}`;
// allow for e.g. '4D' or 5 // allow for e.g. '4D' or 5
const dim = parseInt(dimensions.toString()[0]); const dim = parseInt(dimensions.toString()[0]);
if(dim >= 3) { if(dim >= 3) {
addrStr += `/${this.node}`; addrStr += `/${this.node}`;
} }
// missing & .0 are equiv for point // missing & .0 are equiv for point
if(dim >= 4 && this.point) { if(dim >= 4 && this.point) {
addrStr += `.${this.point}`; addrStr += `.${this.point}`;
} }

View File

@ -1,75 +1,75 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const ftn = require('./ftn_util.js'); const ftn = require('./ftn_util.js');
const Message = require('./message.js'); const Message = require('./message.js');
const sauce = require('./sauce.js'); const sauce = require('./sauce.js');
const Address = require('./ftn_address.js'); const Address = require('./ftn_address.js');
const strUtil = require('./string_util.js'); const strUtil = require('./string_util.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const ansiPrep = require('./ansi_prep.js'); const ansiPrep = require('./ansi_prep.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
const { Parser } = require('binary-parser'); const { Parser } = require('binary-parser');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const async = require('async'); const async = require('async');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const moment = require('moment'); const moment = require('moment');
exports.Packet = Packet; exports.Packet = Packet;
const FTN_PACKET_HEADER_SIZE = 58; // fixed header size const FTN_PACKET_HEADER_SIZE = 58; // fixed header size
const FTN_PACKET_HEADER_TYPE = 2; const FTN_PACKET_HEADER_TYPE = 2;
const FTN_PACKET_MESSAGE_TYPE = 2; const FTN_PACKET_MESSAGE_TYPE = 2;
const FTN_PACKET_BAUD_TYPE_2_2 = 2; const FTN_PACKET_BAUD_TYPE_2_2 = 2;
// SAUCE magic header + version ("00") // SAUCE magic header + version ("00")
const FTN_MESSAGE_SAUCE_HEADER = Buffer.from('SAUCE00'); const FTN_MESSAGE_SAUCE_HEADER = Buffer.from('SAUCE00');
const FTN_MESSAGE_KLUDGE_PREFIX = '\x01'; const FTN_MESSAGE_KLUDGE_PREFIX = '\x01';
class PacketHeader { class PacketHeader {
constructor(origAddr, destAddr, version, createdMoment) { constructor(origAddr, destAddr, version, createdMoment) {
const EMPTY_ADDRESS = { const EMPTY_ADDRESS = {
node : 0, node : 0,
net : 0, net : 0,
zone : 0, zone : 0,
point : 0, point : 0,
}; };
this.version = version || '2+'; this.version = version || '2+';
this.origAddress = origAddr || EMPTY_ADDRESS; this.origAddress = origAddr || EMPTY_ADDRESS;
this.destAddress = destAddr || EMPTY_ADDRESS; this.destAddress = destAddr || EMPTY_ADDRESS;
this.created = createdMoment || moment(); this.created = createdMoment || moment();
// uncommon to set the following explicitly // uncommon to set the following explicitly
this.prodCodeLo = 0xfe; // http://ftsc.org/docs/fta-1005.003 this.prodCodeLo = 0xfe; // http://ftsc.org/docs/fta-1005.003
this.prodRevLo = 0; this.prodRevLo = 0;
this.baud = 0; this.baud = 0;
this.packetType = FTN_PACKET_HEADER_TYPE; this.packetType = FTN_PACKET_HEADER_TYPE;
this.password = ''; this.password = '';
this.prodData = 0x47694e45; // "ENiG" this.prodData = 0x47694e45; // "ENiG"
this.capWord = 0x0001; this.capWord = 0x0001;
this.capWordValidate = ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); // swap this.capWordValidate = ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); // swap
this.prodCodeHi = 0xfe; // see above this.prodCodeHi = 0xfe; // see above
this.prodRevHi = 0; this.prodRevHi = 0;
} }
get origAddress() { get origAddress() {
let addr = new Address({ let addr = new Address({
node : this.origNode, node : this.origNode,
zone : this.origZone, zone : this.origZone,
}); });
if(this.origPoint) { if(this.origPoint) {
addr.point = this.origPoint; addr.point = this.origPoint;
addr.net = this.auxNet; addr.net = this.auxNet;
} else { } else {
addr.net = this.origNet; addr.net = this.origNet;
} }
return addr; return addr;
@ -82,29 +82,29 @@ class PacketHeader {
this.origNode = address.node; this.origNode = address.node;
// See FSC-48 // See FSC-48
// :TODO: disabled for now until we have separate packet writers for 2, 2+, 2+48, and 2.2 // :TODO: disabled for now until we have separate packet writers for 2, 2+, 2+48, and 2.2
/*if(address.point) { /*if(address.point) {
this.auxNet = address.origNet; this.auxNet = address.origNet;
this.origNet = -1; this.origNet = -1;
} else { } else {
this.origNet = address.net; this.origNet = address.net;
this.auxNet = 0; this.auxNet = 0;
} }
*/ */
this.origNet = address.net; this.origNet = address.net;
this.auxNet = 0; this.auxNet = 0;
this.origZone = address.zone; this.origZone = address.zone;
this.origZone2 = address.zone; this.origZone2 = address.zone;
this.origPoint = address.point || 0; this.origPoint = address.point || 0;
} }
get destAddress() { get destAddress() {
let addr = new Address({ let addr = new Address({
node : this.destNode, node : this.destNode,
net : this.destNet, net : this.destNet,
zone : this.destZone, zone : this.destZone,
}); });
if(this.destPoint) { if(this.destPoint) {
@ -119,21 +119,21 @@ class PacketHeader {
address = Address.fromString(address); address = Address.fromString(address);
} }
this.destNode = address.node; this.destNode = address.node;
this.destNet = address.net; this.destNet = address.net;
this.destZone = address.zone; this.destZone = address.zone;
this.destZone2 = address.zone; this.destZone2 = address.zone;
this.destPoint = address.point || 0; this.destPoint = address.point || 0;
} }
get created() { get created() {
return moment({ return moment({
year : this.year, year : this.year,
month : this.month - 1, // moment uses 0 indexed months month : this.month - 1, // moment uses 0 indexed months
date : this.day, date : this.day,
hour : this.hour, hour : this.hour,
minute : this.minute, minute : this.minute,
second : this.second second : this.second
}); });
} }
@ -142,28 +142,28 @@ class PacketHeader {
momentCreated = moment(momentCreated); momentCreated = moment(momentCreated);
} }
this.year = momentCreated.year(); this.year = momentCreated.year();
this.month = momentCreated.month() + 1; // moment uses 0 indexed months this.month = momentCreated.month() + 1; // moment uses 0 indexed months
this.day = momentCreated.date(); // day of month this.day = momentCreated.date(); // day of month
this.hour = momentCreated.hour(); this.hour = momentCreated.hour();
this.minute = momentCreated.minute(); this.minute = momentCreated.minute();
this.second = momentCreated.second(); this.second = momentCreated.second();
} }
} }
exports.PacketHeader = PacketHeader; exports.PacketHeader = PacketHeader;
// //
// Read/Write FTN packets with support for the following formats: // Read/Write FTN packets with support for the following formats:
// //
// * Type 2 FTS-0001 @ http://ftsc.org/docs/fts-0001.016 (Obsolete) // * Type 2 FTS-0001 @ http://ftsc.org/docs/fts-0001.016 (Obsolete)
// * Type 2.2 FSC-0045 @ http://ftsc.org/docs/fsc-0045.001 // * Type 2.2 FSC-0045 @ http://ftsc.org/docs/fsc-0045.001
// * Type 2+ FSC-0039 and FSC-0048 @ http://ftsc.org/docs/fsc-0039.004 // * Type 2+ FSC-0039 and FSC-0048 @ http://ftsc.org/docs/fsc-0039.004
// and http://ftsc.org/docs/fsc-0048.002 // and http://ftsc.org/docs/fsc-0048.002
// //
// Additional resources: // Additional resources:
// * Writeup on differences between type 2, 2.2, and 2+: // * Writeup on differences between type 2, 2.2, and 2+:
// http://walon.org/pub/fidonet/FTSC-nodelists-etc./pkt-types.txt // http://walon.org/pub/fidonet/FTSC-nodelists-etc./pkt-types.txt
// //
function Packet(options) { function Packet(options) {
var self = this; var self = this;
@ -189,13 +189,13 @@ function Packet(options) {
.uint16le('origNet') .uint16le('origNet')
.uint16le('destNet') .uint16le('destNet')
.int8('prodCodeLo') .int8('prodCodeLo')
.int8('prodRevLo') // aka serialNo .int8('prodRevLo') // aka serialNo
.buffer('password', { length : 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33 .buffer('password', { length : 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33
.uint16le('origZone') .uint16le('origZone')
.uint16le('destZone') .uint16le('destZone')
// //
// The following is "filler" in FTS-0001, specifics in // The following is "filler" in FTS-0001, specifics in
// FSC-0045 and FSC-0048 // FSC-0045 and FSC-0048
// //
.uint16le('auxNet') .uint16le('auxNet')
.uint16le('capWordValidate') .uint16le('capWordValidate')
@ -212,7 +212,7 @@ function Packet(options) {
return Errors.Invalid(`Unable to parse FTN packet header: ${e.message}`); return Errors.Invalid(`Unable to parse FTN packet header: ${e.message}`);
} }
// Convert password from NULL padded array to string // Convert password from NULL padded array to string
packetHeader.password = strUtil.stringFromNullTermBuffer(packetHeader.password, 'CP437'); packetHeader.password = strUtil.stringFromNullTermBuffer(packetHeader.password, 'CP437');
if(FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) { if(FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) {
@ -220,50 +220,50 @@ function Packet(options) {
} }
// //
// What kind of packet do we really have here? // What kind of packet do we really have here?
// //
// :TODO: adjust values based on version discovered // :TODO: adjust values based on version discovered
if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) { if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) {
packetHeader.version = '2.2'; packetHeader.version = '2.2';
// See FSC-0045 // See FSC-0045
packetHeader.origPoint = packetHeader.year; packetHeader.origPoint = packetHeader.year;
packetHeader.destPoint = packetHeader.month; packetHeader.destPoint = packetHeader.month;
packetHeader.destDomain = packetHeader.origZone2; packetHeader.destDomain = packetHeader.origZone2;
packetHeader.origDomain = packetHeader.auxNet; packetHeader.origDomain = packetHeader.auxNet;
} else { } else {
// //
// See heuristics described in FSC-0048, "Receiving Type-2+ bundles" // See heuristics described in FSC-0048, "Receiving Type-2+ bundles"
// //
const capWordValidateSwapped = const capWordValidateSwapped =
((packetHeader.capWordValidate & 0xff) << 8) | ((packetHeader.capWordValidate & 0xff) << 8) |
((packetHeader.capWordValidate >> 8) & 0xff); ((packetHeader.capWordValidate >> 8) & 0xff);
if(capWordValidateSwapped === packetHeader.capWord && if(capWordValidateSwapped === packetHeader.capWord &&
0 != packetHeader.capWord && 0 != packetHeader.capWord &&
packetHeader.capWord & 0x0001) packetHeader.capWord & 0x0001)
{ {
packetHeader.version = '2+'; packetHeader.version = '2+';
// See FSC-0048 // See FSC-0048
if(-1 === packetHeader.origNet) { if(-1 === packetHeader.origNet) {
packetHeader.origNet = packetHeader.auxNet; packetHeader.origNet = packetHeader.auxNet;
} }
} else { } else {
packetHeader.version = '2'; packetHeader.version = '2';
// :TODO: should fill bytes be 0? // :TODO: should fill bytes be 0?
} }
} }
packetHeader.created = moment({ packetHeader.created = moment({
year : packetHeader.year, year : packetHeader.year,
month : packetHeader.month - 1, // moment uses 0 indexed months month : packetHeader.month - 1, // moment uses 0 indexed months
date : packetHeader.day, date : packetHeader.day,
hour : packetHeader.hour, hour : packetHeader.hour,
minute : packetHeader.minute, minute : packetHeader.minute,
second : packetHeader.second second : packetHeader.second
}); });
const ph = new PacketHeader(); const ph = new PacketHeader();
@ -352,36 +352,36 @@ function Packet(options) {
this.processMessageBody = function(messageBodyBuffer, cb) { this.processMessageBody = function(messageBodyBuffer, cb) {
// //
// From FTS-0001.16: // From FTS-0001.16:
// "Message text is unbounded and null terminated (note exception below). // "Message text is unbounded and null terminated (note exception below).
// //
// A 'hard' carriage return, 0DH, marks the end of a paragraph, and must // A 'hard' carriage return, 0DH, marks the end of a paragraph, and must
// be preserved. // be preserved.
// //
// So called 'soft' carriage returns, 8DH, may mark a previous // So called 'soft' carriage returns, 8DH, may mark a previous
// processor's automatic line wrap, and should be ignored. Beware that // processor's automatic line wrap, and should be ignored. Beware that
// they may be followed by linefeeds, or may not. // they may be followed by linefeeds, or may not.
// //
// All linefeeds, 0AH, should be ignored. Systems which display message // All linefeeds, 0AH, should be ignored. Systems which display message
// text should wrap long lines to suit their application." // text should wrap long lines to suit their application."
// //
// This can be a bit tricky: // This can be a bit tricky:
// * Decoding as CP437 converts 0x8d -> 0xec, so we'll need to correct for that // * Decoding as CP437 converts 0x8d -> 0xec, so we'll need to correct for that
// * Many kludge lines specify an encoding. If we find one of such lines, we'll // * Many kludge lines specify an encoding. If we find one of such lines, we'll
// likely need to re-decode as the specified encoding // likely need to re-decode as the specified encoding
// * SAUCE is binary-ish data, so we need to inspect for it before any // * SAUCE is binary-ish data, so we need to inspect for it before any
// decoding occurs // decoding occurs
// //
let messageBodyData = { let messageBodyData = {
message : [], message : [],
kludgeLines : {}, // KLUDGE:[value1, value2, ...] map kludgeLines : {}, // KLUDGE:[value1, value2, ...] map
seenBy : [], seenBy : [],
}; };
function addKludgeLine(line) { function addKludgeLine(line) {
// //
// We have to special case INTL/TOPT/FMPT as they don't contain // We have to special case INTL/TOPT/FMPT as they don't contain
// a ':' name/value separator like the rest of the kludge lines... because stupdity. // a ':' name/value separator like the rest of the kludge lines... because stupdity.
// //
let key = line.substr(0, 4).trim(); let key = line.substr(0, 4).trim();
let value; let value;
@ -389,13 +389,13 @@ function Packet(options) {
value = line.substr(key.length).trim(); value = line.substr(key.length).trim();
} else { } else {
const sepIndex = line.indexOf(':'); const sepIndex = line.indexOf(':');
key = line.substr(0, sepIndex).toUpperCase(); key = line.substr(0, sepIndex).toUpperCase();
value = line.substr(sepIndex + 1).trim(); value = line.substr(sepIndex + 1).trim();
} }
// //
// Allow mapped value to be either a key:value if there is only // Allow mapped value to be either a key:value if there is only
// one entry, or key:[value1, value2,...] if there are more // one entry, or key:[value1, value2,...] if there are more
// //
if(messageBodyData.kludgeLines[key]) { if(messageBodyData.kludgeLines[key]) {
if(!_.isArray(messageBodyData.kludgeLines[key])) { if(!_.isArray(messageBodyData.kludgeLines[key])) {
@ -412,21 +412,21 @@ function Packet(options) {
async.series( async.series(
[ [
function extractSauce(callback) { function extractSauce(callback) {
// :TODO: This is wrong: SAUCE may not have EOF marker for one, also if it's // :TODO: This is wrong: SAUCE may not have EOF marker for one, also if it's
// present, we need to extract it but keep the rest of hte message intact as it likely // present, we need to extract it but keep the rest of hte message intact as it likely
// has SEEN-BY, PATH, and other kludge information *appended* // has SEEN-BY, PATH, and other kludge information *appended*
const sauceHeaderPosition = messageBodyBuffer.indexOf(FTN_MESSAGE_SAUCE_HEADER); const sauceHeaderPosition = messageBodyBuffer.indexOf(FTN_MESSAGE_SAUCE_HEADER);
if(sauceHeaderPosition > -1) { if(sauceHeaderPosition > -1) {
sauce.readSAUCE(messageBodyBuffer.slice(sauceHeaderPosition, sauceHeaderPosition + sauce.SAUCE_SIZE), (err, theSauce) => { sauce.readSAUCE(messageBodyBuffer.slice(sauceHeaderPosition, sauceHeaderPosition + sauce.SAUCE_SIZE), (err, theSauce) => {
if(!err) { if(!err) {
// we read some SAUCE - don't re-process that portion into the body // we read some SAUCE - don't re-process that portion into the body
messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition) + messageBodyBuffer.slice(sauceHeaderPosition + sauce.SAUCE_SIZE); messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition) + messageBodyBuffer.slice(sauceHeaderPosition + sauce.SAUCE_SIZE);
// messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition); // messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition);
messageBodyData.sauce = theSauce; messageBodyData.sauce = theSauce;
} else { } else {
Log.warn( { error : err.message }, 'Found what looks like to be a SAUCE record, but failed to read'); Log.warn( { error : err.message }, 'Found what looks like to be a SAUCE record, but failed to read');
} }
return callback(null); // failure to read SAUCE is OK return callback(null); // failure to read SAUCE is OK
}); });
} else { } else {
callback(null); callback(null);
@ -434,21 +434,21 @@ function Packet(options) {
}, },
function extractChrsAndDetermineEncoding(callback) { function extractChrsAndDetermineEncoding(callback) {
// //
// From FTS-5003.001: // From FTS-5003.001:
// "The CHRS control line is formatted as follows: // "The CHRS control line is formatted as follows:
// //
// ^ACHRS: <identifier> <level> // ^ACHRS: <identifier> <level>
// //
// Where <identifier> is a character string of no more than eight (8) // Where <identifier> is a character string of no more than eight (8)
// ASCII characters identifying the character set or character encoding // ASCII characters identifying the character set or character encoding
// scheme used, and level is a positive integer value describing what // scheme used, and level is a positive integer value describing what
// level of CHRS the message is written in." // level of CHRS the message is written in."
// //
// Also according to the spec, the deprecated "CHARSET" value may be used // Also according to the spec, the deprecated "CHARSET" value may be used
// :TODO: Look into CHARSET more - should we bother supporting it? // :TODO: Look into CHARSET more - should we bother supporting it?
// :TODO: See encodingFromHeader() for CHRS/CHARSET support @ https://github.com/Mithgol/node-fidonet-jam // :TODO: See encodingFromHeader() for CHRS/CHARSET support @ https://github.com/Mithgol/node-fidonet-jam
const FTN_CHRS_PREFIX = Buffer.from( [ 0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20 ] ); // "\x01CHRS:" const FTN_CHRS_PREFIX = Buffer.from( [ 0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20 ] ); // "\x01CHRS:"
const FTN_CHRS_SUFFIX = Buffer.from( [ 0x0d ] ); const FTN_CHRS_SUFFIX = Buffer.from( [ 0x0d ] );
let chrsPrefixIndex = messageBodyBuffer.indexOf(FTN_CHRS_PREFIX); let chrsPrefixIndex = messageBodyBuffer.indexOf(FTN_CHRS_PREFIX);
if(chrsPrefixIndex < 0) { if(chrsPrefixIndex < 0) {
@ -476,9 +476,9 @@ function Packet(options) {
}, },
function extractMessageData(callback) { function extractMessageData(callback) {
// //
// Decode |messageBodyBuffer| using |encoding| defaulted or detected above // Decode |messageBodyBuffer| using |encoding| defaulted or detected above
// //
// :TODO: Look into \xec thing more - document // :TODO: Look into \xec thing more - document
let decoded; let decoded;
try { try {
decoded = iconv.decode(messageBodyBuffer, encoding); decoded = iconv.decode(messageBodyBuffer, encoding);
@ -487,8 +487,8 @@ function Packet(options) {
decoded = iconv.decode(messageBodyBuffer, 'ascii'); decoded = iconv.decode(messageBodyBuffer, 'ascii');
} }
const messageLines = strUtil.splitTextAtTerms(decoded.replace(/\xec/g, '')); const messageLines = strUtil.splitTextAtTerms(decoded.replace(/\xec/g, ''));
let endOfMessage = false; let endOfMessage = false;
messageLines.forEach(line => { messageLines.forEach(line => {
if(0 === line.length) { if(0 === line.length) {
@ -499,21 +499,21 @@ function Packet(options) {
if(line.startsWith('AREA:')) { if(line.startsWith('AREA:')) {
messageBodyData.area = line.substring(line.indexOf(':') + 1).trim(); messageBodyData.area = line.substring(line.indexOf(':') + 1).trim();
} else if(line.startsWith('--- ')) { } else if(line.startsWith('--- ')) {
// Tear Lines are tracked allowing for specialized display/etc. // Tear Lines are tracked allowing for specialized display/etc.
messageBodyData.tearLine = line; messageBodyData.tearLine = line;
} else if(/^[ ]{1,2}\* Origin: /.test(line)) { // To spec is " * Origin: ..." } else if(/^[ ]{1,2}\* Origin: /.test(line)) { // To spec is " * Origin: ..."
messageBodyData.originLine = line; messageBodyData.originLine = line;
endOfMessage = true; // Anything past origin is not part of the message body endOfMessage = true; // Anything past origin is not part of the message body
} else if(line.startsWith('SEEN-BY:')) { } else if(line.startsWith('SEEN-BY:')) {
endOfMessage = true; // Anything past the first SEEN-BY is not part of the message body endOfMessage = true; // Anything past the first SEEN-BY is not part of the message body
messageBodyData.seenBy.push(line.substring(line.indexOf(':') + 1).trim()); messageBodyData.seenBy.push(line.substring(line.indexOf(':') + 1).trim());
} else if(FTN_MESSAGE_KLUDGE_PREFIX === line.charAt(0)) { } else if(FTN_MESSAGE_KLUDGE_PREFIX === line.charAt(0)) {
if('PATH:' === line.slice(1, 6)) { if('PATH:' === line.slice(1, 6)) {
endOfMessage = true; // Anything pats the first PATH is not part of the message body endOfMessage = true; // Anything pats the first PATH is not part of the message body
} }
addKludgeLine(line.slice(1)); addKludgeLine(line.slice(1));
} else if(!endOfMessage) { } else if(!endOfMessage) {
// regular ol' message line // regular ol' message line
messageBodyData.message.push(line); messageBodyData.message.push(line);
} }
}); });
@ -530,16 +530,16 @@ function Packet(options) {
this.parsePacketMessages = function(header, packetBuffer, iterator, cb) { this.parsePacketMessages = function(header, packetBuffer, iterator, cb) {
// //
// Check for end-of-messages marker up front before parse so we can easily // Check for end-of-messages marker up front before parse so we can easily
// tell the difference between end and bad header // tell the difference between end and bad header
// //
if(packetBuffer.length < 3) { if(packetBuffer.length < 3) {
const peek = packetBuffer.slice(0, 2); const peek = packetBuffer.slice(0, 2);
if(peek.equals(Buffer.from([ 0x00 ])) || peek.equals(Buffer.from( [ 0x00, 0x00 ]))) { if(peek.equals(Buffer.from([ 0x00 ])) || peek.equals(Buffer.from( [ 0x00, 0x00 ]))) {
// end marker - no more messages // end marker - no more messages
return cb(null); return cb(null);
} }
// else fall through & hit exception below to log error // else fall through & hit exception below to log error
} }
let msgData; let msgData;
@ -552,26 +552,26 @@ function Packet(options) {
.uint16le('ftn_msg_dest_net') .uint16le('ftn_msg_dest_net')
.uint16le('ftn_attr_flags') .uint16le('ftn_attr_flags')
.uint16le('ftn_cost') .uint16le('ftn_cost')
// :TODO: use string() for these if https://github.com/keichi/binary-parser/issues/33 is resolved // :TODO: use string() for these if https://github.com/keichi/binary-parser/issues/33 is resolved
.array('modDateTime', { .array('modDateTime', {
type : 'uint8', type : 'uint8',
readUntil : b => 0x00 === b, readUntil : b => 0x00 === b,
}) })
.array('toUserName', { .array('toUserName', {
type : 'uint8', type : 'uint8',
readUntil : b => 0x00 === b, readUntil : b => 0x00 === b,
}) })
.array('fromUserName', { .array('fromUserName', {
type : 'uint8', type : 'uint8',
readUntil : b => 0x00 === b, readUntil : b => 0x00 === b,
}) })
.array('subject', { .array('subject', {
type : 'uint8', type : 'uint8',
readUntil : b => 0x00 === b, readUntil : b => 0x00 === b,
}) })
.array('message', { .array('message', {
type : 'uint8', type : 'uint8',
readUntil : b => 0x00 === b, readUntil : b => 0x00 === b,
}) })
.parse(packetBuffer); .parse(packetBuffer);
} catch(e) { } catch(e) {
@ -583,49 +583,49 @@ function Packet(options) {
} }
// //
// Convert null terminated arrays to strings // Convert null terminated arrays to strings
// //
[ 'modDateTime', 'toUserName', 'fromUserName', 'subject' ].forEach(k => { [ 'modDateTime', 'toUserName', 'fromUserName', 'subject' ].forEach(k => {
msgData[k] = strUtil.stringFromNullTermBuffer(msgData[k], 'CP437'); msgData[k] = strUtil.stringFromNullTermBuffer(msgData[k], 'CP437');
}); });
// Technically the following fields have length limits as per fts-0001.016: // Technically the following fields have length limits as per fts-0001.016:
// * modDateTime : 20 bytes // * modDateTime : 20 bytes
// * toUserName : 36 bytes // * toUserName : 36 bytes
// * fromUserName : 36 bytes // * fromUserName : 36 bytes
// * subject : 72 bytes // * subject : 72 bytes
// //
// The message body itself is a special beast as it may // The message body itself is a special beast as it may
// contain an origin line, kludges, SAUCE in the case // contain an origin line, kludges, SAUCE in the case
// of ANSI files, etc. // of ANSI files, etc.
// //
const msg = new Message( { const msg = new Message( {
toUserName : msgData.toUserName, toUserName : msgData.toUserName,
fromUserName : msgData.fromUserName, fromUserName : msgData.fromUserName,
subject : msgData.subject, subject : msgData.subject,
modTimestamp : ftn.getDateFromFtnDateTime(msgData.modDateTime), modTimestamp : ftn.getDateFromFtnDateTime(msgData.modDateTime),
}); });
// :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further) // :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further)
msg.meta.FtnProperty = { msg.meta.FtnProperty = {
ftn_orig_node : header.origNode, ftn_orig_node : header.origNode,
ftn_dest_node : header.destNode, ftn_dest_node : header.destNode,
ftn_orig_network : header.origNet, ftn_orig_network : header.origNet,
ftn_dest_network : header.destNet, ftn_dest_network : header.destNet,
ftn_attr_flags : msgData.ftn_attr_flags, ftn_attr_flags : msgData.ftn_attr_flags,
ftn_cost : msgData.ftn_cost, ftn_cost : msgData.ftn_cost,
ftn_msg_orig_node : msgData.ftn_msg_orig_node, ftn_msg_orig_node : msgData.ftn_msg_orig_node,
ftn_msg_dest_node : msgData.ftn_msg_dest_node, ftn_msg_dest_node : msgData.ftn_msg_dest_node,
ftn_msg_orig_net : msgData.ftn_msg_orig_net, ftn_msg_orig_net : msgData.ftn_msg_orig_net,
ftn_msg_dest_net : msgData.ftn_msg_dest_net, ftn_msg_dest_net : msgData.ftn_msg_dest_net,
}; };
self.processMessageBody(msgData.message, messageBodyData => { self.processMessageBody(msgData.message, messageBodyData => {
msg.message = messageBodyData.message; msg.message = messageBodyData.message;
msg.meta.FtnKludge = messageBodyData.kludgeLines; msg.meta.FtnKludge = messageBodyData.kludgeLines;
if(messageBodyData.tearLine) { if(messageBodyData.tearLine) {
msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine; msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine;
@ -652,21 +652,21 @@ function Packet(options) {
} }
// //
// If we have a UTC offset kludge (e.g. TZUTC) then update // If we have a UTC offset kludge (e.g. TZUTC) then update
// modDateTime with it // modDateTime with it
// //
if(_.isString(msg.meta.FtnKludge.TZUTC) && msg.meta.FtnKludge.TZUTC.length > 0) { if(_.isString(msg.meta.FtnKludge.TZUTC) && msg.meta.FtnKludge.TZUTC.length > 0) {
msg.modDateTime = msg.modTimestamp.utcOffset(msg.meta.FtnKludge.TZUTC); msg.modDateTime = msg.modTimestamp.utcOffset(msg.meta.FtnKludge.TZUTC);
} }
// :TODO: Parser should give is this info: // :TODO: Parser should give is this info:
const bytesRead = const bytesRead =
14 + // fixed header size 14 + // fixed header size
msgData.modDateTime.length + 1 + // +1 = NULL msgData.modDateTime.length + 1 + // +1 = NULL
msgData.toUserName.length + 1 + // +1 = NULL msgData.toUserName.length + 1 + // +1 = NULL
msgData.fromUserName.length + 1 + // +1 = NULL msgData.fromUserName.length + 1 + // +1 = NULL
msgData.subject.length + 1 + // +1 = NULL msgData.subject.length + 1 + // +1 = NULL
msgData.message.length; // includes NULL msgData.message.length; // includes NULL
const nextBuf = packetBuffer.slice(bytesRead); const nextBuf = packetBuffer.slice(bytesRead);
if(nextBuf.length > 0) { if(nextBuf.length > 0) {
@ -710,11 +710,11 @@ function Packet(options) {
}; };
this.writeMessageHeader = function(message, buf) { this.writeMessageHeader = function(message, buf) {
// ensure address FtnProperties are numbers // ensure address FtnProperties are numbers
self.sanatizeFtnProperties(message); self.sanatizeFtnProperties(message);
const destNode = message.meta.FtnProperty.ftn_msg_dest_node || message.meta.FtnProperty.ftn_dest_node; const destNode = message.meta.FtnProperty.ftn_msg_dest_node || message.meta.FtnProperty.ftn_dest_node;
const destNet = message.meta.FtnProperty.ftn_msg_dest_net || message.meta.FtnProperty.ftn_dest_network; const destNet = message.meta.FtnProperty.ftn_msg_dest_net || message.meta.FtnProperty.ftn_dest_network;
buf.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); buf.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0);
buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2);
@ -751,43 +751,43 @@ function Packet(options) {
self.writeMessageHeader(message, basicHeader); self.writeMessageHeader(message, basicHeader);
// //
// To, from, and subject must be NULL term'd and have max lengths as per spec. // To, from, and subject must be NULL term'd and have max lengths as per spec.
// //
const toUserNameBuf = strUtil.stringToNullTermBuffer(message.toUserName, { encoding : 'cp437', maxBufLen : 36 } ); const toUserNameBuf = strUtil.stringToNullTermBuffer(message.toUserName, { encoding : 'cp437', maxBufLen : 36 } );
const fromUserNameBuf = strUtil.stringToNullTermBuffer(message.fromUserName, { encoding : 'cp437', maxBufLen : 36 } ); const fromUserNameBuf = strUtil.stringToNullTermBuffer(message.fromUserName, { encoding : 'cp437', maxBufLen : 36 } );
const subjectBuf = strUtil.stringToNullTermBuffer(message.subject, { encoding : 'cp437', maxBufLen : 72 } ); const subjectBuf = strUtil.stringToNullTermBuffer(message.subject, { encoding : 'cp437', maxBufLen : 72 } );
// //
// message: unbound length, NULL term'd // message: unbound length, NULL term'd
// //
// We need to build in various special lines - kludges, area, // We need to build in various special lines - kludges, area,
// seen-by, etc. // seen-by, etc.
// //
let msgBody = ''; let msgBody = '';
// //
// FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001
// AREA:CONFERENCE // AREA:CONFERENCE
// Should be first line in a message // Should be first line in a message
// //
if(message.meta.FtnProperty.ftn_area) { if(message.meta.FtnProperty.ftn_area) {
msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01) msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01)
} }
// :TODO: DRY with similar function in this file! // :TODO: DRY with similar function in this file!
Object.keys(message.meta.FtnKludge).forEach(k => { Object.keys(message.meta.FtnKludge).forEach(k => {
switch(k) { switch(k) {
case 'PATH' : case 'PATH' :
break; // skip & save for last break; // skip & save for last
case 'Via' : case 'Via' :
case 'FMPT' : case 'FMPT' :
case 'TOPT' : case 'TOPT' :
case 'INTL' : case 'INTL' :
msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); // no sepChar msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); // no sepChar
break; break;
default : default :
msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]); msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]);
break; break;
} }
@ -803,10 +803,10 @@ function Packet(options) {
ansiPrep( ansiPrep(
message.message, message.message,
{ {
cols : 80, cols : 80,
rows : 'auto', rows : 'auto',
forceLineTerm : true, forceLineTerm : true,
exportMode : true, exportMode : true,
}, },
(err, preppedMsg) => { (err, preppedMsg) => {
return callback(null, basicHeader, toUserNameBuf, fromUserNameBuf, subjectBuf, msgBody, preppedMsg || message.message); return callback(null, basicHeader, toUserNameBuf, fromUserNameBuf, subjectBuf, msgBody, preppedMsg || message.message);
@ -817,25 +817,25 @@ function Packet(options) {
msgBody += preppedMsg + '\r'; msgBody += preppedMsg + '\r';
// //
// FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001
// Tear line should be near the bottom of a message // Tear line should be near the bottom of a message
// //
if(message.meta.FtnProperty.ftn_tear_line) { if(message.meta.FtnProperty.ftn_tear_line) {
msgBody += `${message.meta.FtnProperty.ftn_tear_line}\r`; msgBody += `${message.meta.FtnProperty.ftn_tear_line}\r`;
} }
// //
// Origin line should be near the bottom of a message // Origin line should be near the bottom of a message
// //
if(message.meta.FtnProperty.ftn_origin) { if(message.meta.FtnProperty.ftn_origin) {
msgBody += `${message.meta.FtnProperty.ftn_origin}\r`; msgBody += `${message.meta.FtnProperty.ftn_origin}\r`;
} }
// //
// FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001
// SEEN-BY and PATH should be the last lines of a message // SEEN-BY and PATH should be the last lines of a message
// //
msgBody += getAppendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01) msgBody += getAppendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01)
msgBody += getAppendMeta('\x01PATH', message.meta.FtnKludge['PATH']); msgBody += getAppendMeta('\x01PATH', message.meta.FtnKludge['PATH']);
let msgBodyEncoded; let msgBodyEncoded;
@ -869,28 +869,28 @@ function Packet(options) {
ws.write(basicHeader); ws.write(basicHeader);
// toUserName & fromUserName: up to 36 bytes in length, NULL term'd // toUserName & fromUserName: up to 36 bytes in length, NULL term'd
// :TODO: DRY... // :TODO: DRY...
let encBuf = iconv.encode(message.toUserName + '\0', 'CP437').slice(0, 36); let encBuf = iconv.encode(message.toUserName + '\0', 'CP437').slice(0, 36);
encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd
ws.write(encBuf); ws.write(encBuf);
encBuf = iconv.encode(message.fromUserName + '\0', 'CP437').slice(0, 36); encBuf = iconv.encode(message.fromUserName + '\0', 'CP437').slice(0, 36);
encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd
ws.write(encBuf); ws.write(encBuf);
// subject: up to 72 bytes in length, NULL term'd // subject: up to 72 bytes in length, NULL term'd
encBuf = iconv.encode(message.subject + '\0', 'CP437').slice(0, 72); encBuf = iconv.encode(message.subject + '\0', 'CP437').slice(0, 72);
encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd
ws.write(encBuf); ws.write(encBuf);
// //
// message: unbound length, NULL term'd // message: unbound length, NULL term'd
// //
// We need to build in various special lines - kludges, area, // We need to build in various special lines - kludges, area,
// seen-by, etc. // seen-by, etc.
// //
// :TODO: Put this in it's own method // :TODO: Put this in it's own method
let msgBody = ''; let msgBody = '';
function appendMeta(k, m, sepChar=':') { function appendMeta(k, m, sepChar=':') {
@ -906,54 +906,54 @@ function Packet(options) {
} }
// //
// FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001
// AREA:CONFERENCE // AREA:CONFERENCE
// Should be first line in a message // Should be first line in a message
// //
if(message.meta.FtnProperty.ftn_area) { if(message.meta.FtnProperty.ftn_area) {
msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01) msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01)
} }
Object.keys(message.meta.FtnKludge).forEach(k => { Object.keys(message.meta.FtnKludge).forEach(k => {
switch(k) { switch(k) {
case 'PATH' : break; // skip & save for last case 'PATH' : break; // skip & save for last
case 'Via' : case 'Via' :
case 'FMPT' : case 'FMPT' :
case 'TOPT' : case 'TOPT' :
case 'INTL' : appendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); break; // no sepChar case 'INTL' : appendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); break; // no sepChar
default : appendMeta(`\x01${k}`, message.meta.FtnKludge[k]); break; default : appendMeta(`\x01${k}`, message.meta.FtnKludge[k]); break;
} }
}); });
msgBody += message.message + '\r'; msgBody += message.message + '\r';
// //
// FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001
// Tear line should be near the bottom of a message // Tear line should be near the bottom of a message
// //
if(message.meta.FtnProperty.ftn_tear_line) { if(message.meta.FtnProperty.ftn_tear_line) {
msgBody += `${message.meta.FtnProperty.ftn_tear_line}\r`; msgBody += `${message.meta.FtnProperty.ftn_tear_line}\r`;
} }
// //
// Origin line should be near the bottom of a message // Origin line should be near the bottom of a message
// //
if(message.meta.FtnProperty.ftn_origin) { if(message.meta.FtnProperty.ftn_origin) {
msgBody += `${message.meta.FtnProperty.ftn_origin}\r`; msgBody += `${message.meta.FtnProperty.ftn_origin}\r`;
} }
// //
// FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001
// SEEN-BY and PATH should be the last lines of a message // SEEN-BY and PATH should be the last lines of a message
// //
appendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01) appendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01)
appendMeta('\x01PATH', message.meta.FtnKludge['PATH']); appendMeta('\x01PATH', message.meta.FtnKludge['PATH']);
// //
// :TODO: We should encode based on config and add the proper kludge here! // :TODO: We should encode based on config and add the proper kludge here!
ws.write(iconv.encode(msgBody + '\0', options.encoding)); ws.write(iconv.encode(msgBody + '\0', options.encoding));
}; };
@ -981,35 +981,35 @@ function Packet(options) {
callback); callback);
} }
], ],
cb // complete cb // complete
); );
}; };
} }
// //
// Message attributes defined in FTS-0001.016 // Message attributes defined in FTS-0001.016
// http://ftsc.org/docs/fts-0001.016 // http://ftsc.org/docs/fts-0001.016
// //
// See also: // See also:
// * http://www.skepticfiles.org/aj/basics03.htm // * http://www.skepticfiles.org/aj/basics03.htm
// //
Packet.Attribute = { Packet.Attribute = {
Private : 0x0001, // Private message / NetMail Private : 0x0001, // Private message / NetMail
Crash : 0x0002, Crash : 0x0002,
Received : 0x0004, Received : 0x0004,
Sent : 0x0008, Sent : 0x0008,
FileAttached : 0x0010, FileAttached : 0x0010,
InTransit : 0x0020, InTransit : 0x0020,
Orphan : 0x0040, Orphan : 0x0040,
KillSent : 0x0080, KillSent : 0x0080,
Local : 0x0100, // Message is from *this* system Local : 0x0100, // Message is from *this* system
Hold : 0x0200, Hold : 0x0200,
Reserved0 : 0x0400, Reserved0 : 0x0400,
FileRequest : 0x0800, FileRequest : 0x0800,
ReturnReceiptRequest : 0x1000, ReturnReceiptRequest : 0x1000,
ReturnReceipt : 0x2000, ReturnReceipt : 0x2000,
AuditRequest : 0x4000, AuditRequest : 0x4000,
FileUpdateRequest : 0x8000, FileUpdateRequest : 0x8000,
}; };
Object.freeze(Packet.Attribute); Object.freeze(Packet.Attribute);
@ -1051,10 +1051,10 @@ Packet.prototype.writeMessageEntry = function(ws, msgEntry) {
Packet.prototype.writeTerminator = function(ws) { Packet.prototype.writeTerminator = function(ws) {
// //
// From FTS-0001.016: // From FTS-0001.016:
// "A pseudo-message beginning with the word 0000H signifies the end of the packet." // "A pseudo-message beginning with the word 0000H signifies the end of the packet."
// //
ws.write(Buffer.from( [ 0x00, 0x00 ] )); // final extra null term ws.write(Buffer.from( [ 0x00, 0x00 ] )); // final extra null term
return 2; return 2;
}; };
@ -1074,7 +1074,7 @@ Packet.prototype.writeStream = function(ws, messages, options) {
}); });
if(true === options.terminatePacket) { if(true === options.terminatePacket) {
ws.write(Buffer.from( [ 0 ] )); // final extra null term ws.write(Buffer.from( [ 0 ] )); // final extra null term
} }
}; };
@ -1083,10 +1083,10 @@ Packet.prototype.write = function(path, packetHeader, messages, options) {
messages = [ messages ]; messages = [ messages ];
} }
options = options || { encoding : 'utf8' }; // utf-8 = 'CHRS UTF-8 4' options = options || { encoding : 'utf8' }; // utf-8 = 'CHRS UTF-8 4'
this.writeStream( this.writeStream(
fs.createWriteStream(path), // :TODO: specify mode/etc. fs.createWriteStream(path), // :TODO: specify mode/etc.
messages, messages,
Object.assign( { packetHeader : packetHeader, terminatePacket : true }, options) Object.assign( { packetHeader : packetHeader, terminatePacket : true }, options)
); );

View File

@ -1,52 +1,52 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Address = require('./ftn_address.js'); const Address = require('./ftn_address.js');
const FNV1a = require('./fnv1a.js'); const FNV1a = require('./fnv1a.js');
const getCleanEnigmaVersion = require('./misc_util.js').getCleanEnigmaVersion; const getCleanEnigmaVersion = require('./misc_util.js').getCleanEnigmaVersion;
const _ = require('lodash'); const _ = require('lodash');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const moment = require('moment'); const moment = require('moment');
const os = require('os'); const os = require('os');
const packageJson = require('../package.json'); const packageJson = require('../package.json');
// :TODO: Remove "Ftn" from most of these -- it's implied in the module // :TODO: Remove "Ftn" from most of these -- it's implied in the module
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer; exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
exports.getMessageSerialNumber = getMessageSerialNumber; exports.getMessageSerialNumber = getMessageSerialNumber;
exports.getDateFromFtnDateTime = getDateFromFtnDateTime; exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
exports.getDateTimeString = getDateTimeString; exports.getDateTimeString = getDateTimeString;
exports.getMessageIdentifier = getMessageIdentifier; exports.getMessageIdentifier = getMessageIdentifier;
exports.getProductIdentifier = getProductIdentifier; exports.getProductIdentifier = getProductIdentifier;
exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset; exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset;
exports.getOrigin = getOrigin; exports.getOrigin = getOrigin;
exports.getTearLine = getTearLine; exports.getTearLine = getTearLine;
exports.getVia = getVia; exports.getVia = getVia;
exports.getIntl = getIntl; exports.getIntl = getIntl;
exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList; exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList;
exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList; exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList;
exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries; exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries;
exports.getUpdatedPathEntries = getUpdatedPathEntries; exports.getUpdatedPathEntries = getUpdatedPathEntries;
exports.getCharacterSetIdentifierByEncoding = getCharacterSetIdentifierByEncoding; exports.getCharacterSetIdentifierByEncoding = getCharacterSetIdentifierByEncoding;
exports.getEncodingFromCharacterSetIdentifier = getEncodingFromCharacterSetIdentifier; exports.getEncodingFromCharacterSetIdentifier = getEncodingFromCharacterSetIdentifier;
exports.getQuotePrefix = getQuotePrefix; exports.getQuotePrefix = getQuotePrefix;
// //
// Namespace for RFC-4122 name based UUIDs generated from // Namespace for RFC-4122 name based UUIDs generated from
// FTN kludges MSGID + AREA // FTN kludges MSGID + AREA
// //
//const ENIGMA_FTN_MSGID_NAMESPACE = uuid.parse('a5c7ae11-420c-4469-a116-0e9a6d8d2654'); //const ENIGMA_FTN_MSGID_NAMESPACE = uuid.parse('a5c7ae11-420c-4469-a116-0e9a6d8d2654');
// See list here: https://github.com/Mithgol/node-fidonet-jam // See list here: https://github.com/Mithgol/node-fidonet-jam
function stringToNullPaddedBuffer(s, bufLen) { function stringToNullPaddedBuffer(s, bufLen) {
let buffer = Buffer.alloc(bufLen); let buffer = Buffer.alloc(bufLen);
let enc = iconv.encode(s, 'CP437').slice(0, bufLen); let enc = iconv.encode(s, 'CP437').slice(0, bufLen);
for(let i = 0; i < enc.length; ++i) { for(let i = 0; i < enc.length; ++i) {
buffer[i] = enc[i]; buffer[i] = enc[i];
} }
@ -54,37 +54,37 @@ function stringToNullPaddedBuffer(s, bufLen) {
} }
// //
// Convert a FTN style DateTime string to a Date object // Convert a FTN style DateTime string to a Date object
// //
// :TODO: Name the next couple methods better - for FTN *packets* // :TODO: Name the next couple methods better - for FTN *packets*
function getDateFromFtnDateTime(dateTime) { function getDateFromFtnDateTime(dateTime) {
// //
// Examples seen in the wild (Working): // Examples seen in the wild (Working):
// "12 Sep 88 18:17:59" // "12 Sep 88 18:17:59"
// "Tue 01 Jan 80 00:00" // "Tue 01 Jan 80 00:00"
// "27 Feb 15 00:00:03" // "27 Feb 15 00:00:03"
// //
// :TODO: Use moment.js here // :TODO: Use moment.js here
return moment(Date.parse(dateTime)); // Date.parse() allows funky formats return moment(Date.parse(dateTime)); // Date.parse() allows funky formats
// return (new Date(Date.parse(dateTime))).toISOString(); // return (new Date(Date.parse(dateTime))).toISOString();
} }
function getDateTimeString(m) { function getDateTimeString(m) {
// //
// From http://ftsc.org/docs/fts-0001.016: // From http://ftsc.org/docs/fts-0001.016:
// DateTime = (* a character string 20 characters long *) // DateTime = (* a character string 20 characters long *)
// (* 01 Jan 86 02:34:56 *) // (* 01 Jan 86 02:34:56 *)
// DayOfMonth " " Month " " Year " " // DayOfMonth " " Month " " Year " "
// " " HH ":" MM ":" SS // " " HH ":" MM ":" SS
// Null // Null
// //
// DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *) // DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *)
// Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" | // Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" |
// "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec" // "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec"
// Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00" // Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00"
// HH = "00" | .. | "23" // HH = "00" | .. | "23"
// MM = "00" | .. | "59" // MM = "00" | .. | "59"
// SS = "00" | .. | "59" // SS = "00" | .. | "59"
// //
if(!moment.isMoment(m)) { if(!moment.isMoment(m)) {
m = moment(m); m = moment(m);
@ -95,52 +95,52 @@ function getDateTimeString(m) {
function getMessageSerialNumber(messageId) { function getMessageSerialNumber(messageId) {
const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1)); const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1));
const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16); const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16);
return `00000000${hash}`.substr(-8); return `00000000${hash}`.substr(-8);
} }
// //
// Return a FTS-0009.001 compliant MSGID value given a message // Return a FTS-0009.001 compliant MSGID value given a message
// See http://ftsc.org/docs/fts-0009.001 // See http://ftsc.org/docs/fts-0009.001
// //
// "A MSGID line consists of the string "^AMSGID:" (where ^A is a // "A MSGID line consists of the string "^AMSGID:" (where ^A is a
// control-A (hex 01) and the double-quotes are not part of the // control-A (hex 01) and the double-quotes are not part of the
// string), followed by a space, the address of the originating // string), followed by a space, the address of the originating
// system, and a serial number unique to that message on the // system, and a serial number unique to that message on the
// originating system, i.e.: // originating system, i.e.:
// //
// ^AMSGID: origaddr serialno // ^AMSGID: origaddr serialno
// //
// The originating address should be specified in a form that // The originating address should be specified in a form that
// constitutes a valid return address for the originating network. // constitutes a valid return address for the originating network.
// If the originating address is enclosed in double-quotes, the // If the originating address is enclosed in double-quotes, the
// entire string between the beginning and ending double-quotes is // entire string between the beginning and ending double-quotes is
// considered to be the orginating address. A double-quote character // considered to be the orginating address. A double-quote character
// within a quoted address is represented by by two consecutive // within a quoted address is represented by by two consecutive
// double-quote characters. The serial number may be any eight // double-quote characters. The serial number may be any eight
// character hexadecimal number, as long as it is unique - no two // character hexadecimal number, as long as it is unique - no two
// messages from a given system may have the same serial number // messages from a given system may have the same serial number
// within a three years. The manner in which this serial number is // within a three years. The manner in which this serial number is
// generated is left to the implementor." // generated is left to the implementor."
// //
// //
// Examples & Implementations // Examples & Implementations
// //
// Synchronet: <msgNum>.<conf+area>@<ftnAddr> <serial> // Synchronet: <msgNum>.<conf+area>@<ftnAddr> <serial>
// 2606.agora-agn_tst@46:1/142 19609217 // 2606.agora-agn_tst@46:1/142 19609217
// //
// Mystic: <ftnAddress> <serial> // Mystic: <ftnAddress> <serial>
// 46:3/102 46686263 // 46:3/102 46686263
// //
// ENiGMA½: <messageId>.<areaTag>@<5dFtnAddress> <serial> // ENiGMA½: <messageId>.<areaTag>@<5dFtnAddress> <serial>
// //
// 0.0.8-alpha: // 0.0.8-alpha:
// Made compliant with FTN spec *when exporting NetMail* due to // Made compliant with FTN spec *when exporting NetMail* due to
// Mystic rejecting messages with the true-unique version. // Mystic rejecting messages with the true-unique version.
// Strangely, Synchronet uses the unique format and Mystic does // Strangely, Synchronet uses the unique format and Mystic does
// OK with it. Will need to research further. Note also that // OK with it. Will need to research further. Note also that
// g00r00 was kind enough to fix Mystic to allow for the Sync/Enig // g00r00 was kind enough to fix Mystic to allow for the Sync/Enig
// format, but that will only help when using newer Mystic versions. // format, but that will only help when using newer Mystic versions.
// //
function getMessageIdentifier(message, address, isNetMail = false) { function getMessageIdentifier(message, address, isNetMail = false) {
const addrStr = new Address(address).toString('5D'); const addrStr = new Address(address).toString('5D');
@ -151,42 +151,42 @@ function getMessageIdentifier(message, address, isNetMail = false) {
} }
// //
// Return a FSC-0046.005 Product Identifier or "PID" // Return a FSC-0046.005 Product Identifier or "PID"
// http://ftsc.org/docs/fsc-0046.005 // http://ftsc.org/docs/fsc-0046.005
// //
// Note that we use a variant on the spec for <serial> // Note that we use a variant on the spec for <serial>
// in which (<os>; <arch>; <nodeVer>) is used instead // in which (<os>; <arch>; <nodeVer>) is used instead
// //
function getProductIdentifier() { function getProductIdentifier() {
const version = getCleanEnigmaVersion(); const version = getCleanEnigmaVersion();
const nodeVer = process.version.substr(1); // remove 'v' prefix const nodeVer = process.version.substr(1); // remove 'v' prefix
return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
} }
// //
// Return a FRL-1004 style time zone offset for a // Return a FRL-1004 style time zone offset for a
// 'TZUTC' kludge line // 'TZUTC' kludge line
// //
// http://ftsc.org/docs/frl-1004.002 // http://ftsc.org/docs/frl-1004.002
// //
function getUTCTimeZoneOffset() { function getUTCTimeZoneOffset() {
return moment().format('ZZ').replace(/\+/, ''); return moment().format('ZZ').replace(/\+/, '');
} }
// //
// Get a FSC-0032 style quote prefix // Get a FSC-0032 style quote prefix
// http://ftsc.org/docs/fsc-0032.001 // http://ftsc.org/docs/fsc-0032.001
// //
function getQuotePrefix(name) { function getQuotePrefix(name) {
let initials; let initials;
const parts = name.split(' '); const parts = name.split(' ');
if(parts.length > 1) { if(parts.length > 1) {
// First & Last initials - (Bryan Ashby -> BA) // First & Last initials - (Bryan Ashby -> BA)
initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase(); initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase();
} else { } else {
// Just use the first two - (NuSkooler -> Nu) // Just use the first two - (NuSkooler -> Nu)
initials = _.capitalize(name.slice(0, 2)); initials = _.capitalize(name.slice(0, 2));
} }
@ -194,8 +194,8 @@ function getQuotePrefix(name) {
} }
// //
// Return a FTS-0004 Origin line // Return a FTS-0004 Origin line
// http://ftsc.org/docs/fts-0004.001 // http://ftsc.org/docs/fts-0004.001
// //
function getOrigin(address) { function getOrigin(address) {
const config = Config(); const config = Config();
@ -208,38 +208,38 @@ function getOrigin(address) {
} }
function getTearLine() { function getTearLine() {
const nodeVer = process.version.substr(1); // remove 'v' prefix const nodeVer = process.version.substr(1); // remove 'v' prefix
return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
} }
// //
// Return a FRL-1005.001 "Via" line // Return a FRL-1005.001 "Via" line
// http://ftsc.org/docs/frl-1005.001 // http://ftsc.org/docs/frl-1005.001
// //
function getVia(address) { function getVia(address) {
/* /*
FRL-1005.001 states teh following format: FRL-1005.001 states teh following format:
^AVia: <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone] ^AVia: <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone]
<Program Name> <Version> [Serial Number]<CR> <Program Name> <Version> [Serial Number]<CR>
*/ */
const addrStr = new Address(address).toString('5D'); const addrStr = new Address(address).toString('5D');
const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC'); const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC');
const version = getCleanEnigmaVersion(); const version = getCleanEnigmaVersion();
return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`; return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`;
} }
// //
// Creates a INTL kludge value as per FTS-4001 // Creates a INTL kludge value as per FTS-4001
// http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac // http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac
// //
function getIntl(toAddress, fromAddress) { function getIntl(toAddress, fromAddress) {
// //
// INTL differs from 'standard' kludges in that there is no ':' after "INTL" // INTL differs from 'standard' kludges in that there is no ':' after "INTL"
// //
// "<SOH>"INTL "<destination address>" "<origin address><CR>" // "<SOH>"INTL "<destination address>" "<origin address><CR>"
// "...These addresses shall be given on the form <zone>:<net>/<node>" // "...These addresses shall be given on the form <zone>:<net>/<node>"
// //
return `${toAddress.toString('3D')} ${fromAddress.toString('3D')}`; return `${toAddress.toString('3D')} ${fromAddress.toString('3D')}`;
} }
@ -258,11 +258,11 @@ function getAbbreviatedNetNodeList(netNodes) {
abbrList += `${netNode.node} `; abbrList += `${netNode.node} `;
}); });
return abbrList.trim(); // remove trailing space return abbrList.trim(); // remove trailing space
} }
// //
// Parse an abbreviated net/node list commonly used for SEEN-BY and PATH // Parse an abbreviated net/node list commonly used for SEEN-BY and PATH
// //
function parseAbbreviatedNetNodeList(netNodes) { function parseAbbreviatedNetNodeList(netNodes) {
const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g; const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g;
@ -282,39 +282,39 @@ function parseAbbreviatedNetNodeList(netNodes) {
} }
// //
// Return a FTS-0004.001 SEEN-BY entry(s) that include // Return a FTS-0004.001 SEEN-BY entry(s) that include
// all pre-existing SEEN-BY entries with the addition // all pre-existing SEEN-BY entries with the addition
// of |additions|. // of |additions|.
// //
// See http://ftsc.org/docs/fts-0004.001 // See http://ftsc.org/docs/fts-0004.001
// and notes at http://ftsc.org/docs/fsc-0043.002. // and notes at http://ftsc.org/docs/fsc-0043.002.
// //
// For a great write up, see http://www.skepticfiles.org/aj/basics03.htm // For a great write up, see http://www.skepticfiles.org/aj/basics03.htm
// //
// This method returns an sorted array of values, but // This method returns an sorted array of values, but
// not the "SEEN-BY" prefix itself // not the "SEEN-BY" prefix itself
// //
function getUpdatedSeenByEntries(existingEntries, additions) { function getUpdatedSeenByEntries(existingEntries, additions) {
/* /*
From FTS-0004: From FTS-0004:
"There can be many seen-by lines at the end of Conference "There can be many seen-by lines at the end of Conference
Mail messages, and they are the real "meat" of the control Mail messages, and they are the real "meat" of the control
information. They are used to determine the systems to information. They are used to determine the systems to
receive the exported messages. The format of the line is: receive the exported messages. The format of the line is:
SEEN-BY: 132/101 113 136/601 1014/1 SEEN-BY: 132/101 113 136/601 1014/1
The net/node numbers correspond to the net/node numbers of The net/node numbers correspond to the net/node numbers of
the systems having already received the message. In this way the systems having already received the message. In this way
a message is never sent to a system twice. In a conference a message is never sent to a system twice. In a conference
with many participants the number of seen-by lines can be with many participants the number of seen-by lines can be
very large. This line is added if it is not already a part very large. This line is added if it is not already a part
of the message, or added to if it already exists, each time of the message, or added to if it already exists, each time
a message is exported to other systems. This is a REQUIRED a message is exported to other systems. This is a REQUIRED
field, and Conference Mail will not function correctly if field, and Conference Mail will not function correctly if
this field is not put in place by other Echomail compatible this field is not put in place by other Echomail compatible
programs." programs."
*/ */
existingEntries = existingEntries || []; existingEntries = existingEntries || [];
if(!_.isArray(existingEntries)) { if(!_.isArray(existingEntries)) {
@ -328,15 +328,15 @@ function getUpdatedSeenByEntries(existingEntries, additions) {
additions = additions.sort(Address.getComparator()); additions = additions.sort(Address.getComparator());
// //
// For now, we'll just append a new SEEN-BY entry // For now, we'll just append a new SEEN-BY entry
// //
// :TODO: we should at least try and update what is already there in a smart way // :TODO: we should at least try and update what is already there in a smart way
existingEntries.push(getAbbreviatedNetNodeList(additions)); existingEntries.push(getAbbreviatedNetNodeList(additions));
return existingEntries; return existingEntries;
} }
function getUpdatedPathEntries(existingEntries, localAddress) { function getUpdatedPathEntries(existingEntries, localAddress) {
// :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line // :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line
existingEntries = existingEntries || []; existingEntries = existingEntries || [];
if(!_.isArray(existingEntries)) { if(!_.isArray(existingEntries)) {
@ -350,23 +350,23 @@ function getUpdatedPathEntries(existingEntries, localAddress) {
} }
// //
// Return FTS-5000.001 "CHRS" value // Return FTS-5000.001 "CHRS" value
// http://ftsc.org/docs/fts-5003.001 // http://ftsc.org/docs/fts-5003.001
// //
const ENCODING_TO_FTS_5003_001_CHARS = { const ENCODING_TO_FTS_5003_001_CHARS = {
// level 1 - generally should not be used // level 1 - generally should not be used
ascii : [ 'ASCII', 1 ], ascii : [ 'ASCII', 1 ],
'us-ascii' : [ 'ASCII', 1 ], 'us-ascii' : [ 'ASCII', 1 ],
// level 2 - 8 bit, ASCII based // level 2 - 8 bit, ASCII based
cp437 : [ 'CP437', 2 ], cp437 : [ 'CP437', 2 ],
cp850 : [ 'CP850', 2 ], cp850 : [ 'CP850', 2 ],
// level 3 - reserved // level 3 - reserved
// level 4 // level 4
utf8 : [ 'UTF-8', 4 ], utf8 : [ 'UTF-8', 4 ],
'utf-8' : [ 'UTF-8', 4 ], 'utf-8' : [ 'UTF-8', 4 ],
}; };
@ -378,47 +378,47 @@ function getCharacterSetIdentifierByEncoding(encodingName) {
function getEncodingFromCharacterSetIdentifier(chrs) { function getEncodingFromCharacterSetIdentifier(chrs) {
const ident = chrs.split(' ')[0].toUpperCase(); const ident = chrs.split(' ')[0].toUpperCase();
// :TODO: fill in the rest!!! // :TODO: fill in the rest!!!
return { return {
// level 1 // level 1
'ASCII' : 'iso-646-1', 'ASCII' : 'iso-646-1',
'DUTCH' : 'iso-646', 'DUTCH' : 'iso-646',
'FINNISH' : 'iso-646-10', 'FINNISH' : 'iso-646-10',
'FRENCH' : 'iso-646', 'FRENCH' : 'iso-646',
'CANADIAN' : 'iso-646', 'CANADIAN' : 'iso-646',
'GERMAN' : 'iso-646', 'GERMAN' : 'iso-646',
'ITALIAN' : 'iso-646', 'ITALIAN' : 'iso-646',
'NORWEIG' : 'iso-646', 'NORWEIG' : 'iso-646',
'PORTU' : 'iso-646', 'PORTU' : 'iso-646',
'SPANISH' : 'iso-656', 'SPANISH' : 'iso-656',
'SWEDISH' : 'iso-646-10', 'SWEDISH' : 'iso-646-10',
'SWISS' : 'iso-646', 'SWISS' : 'iso-646',
'UK' : 'iso-646', 'UK' : 'iso-646',
'ISO-10' : 'iso-646-10', 'ISO-10' : 'iso-646-10',
// level 2 // level 2
'CP437' : 'cp437', 'CP437' : 'cp437',
'CP850' : 'cp850', 'CP850' : 'cp850',
'CP852' : 'cp852', 'CP852' : 'cp852',
'CP866' : 'cp866', 'CP866' : 'cp866',
'CP848' : 'cp848', 'CP848' : 'cp848',
'CP1250' : 'cp1250', 'CP1250' : 'cp1250',
'CP1251' : 'cp1251', 'CP1251' : 'cp1251',
'CP1252' : 'cp1252', 'CP1252' : 'cp1252',
'CP10000' : 'macroman', 'CP10000' : 'macroman',
'LATIN-1' : 'iso-8859-1', 'LATIN-1' : 'iso-8859-1',
'LATIN-2' : 'iso-8859-2', 'LATIN-2' : 'iso-8859-2',
'LATIN-5' : 'iso-8859-9', 'LATIN-5' : 'iso-8859-9',
'LATIN-9' : 'iso-8859-15', 'LATIN-9' : 'iso-8859-15',
// level 4 // level 4
'UTF-8' : 'utf8', 'UTF-8' : 'utf8',
// deprecated stuff // deprecated stuff
'IBMPC' : 'cp1250', // :TODO: validate 'IBMPC' : 'cp1250', // :TODO: validate
'+7_FIDO' : 'cp866', '+7_FIDO' : 'cp866',
'+7' : 'cp866', '+7' : 'cp866',
'MAC' : 'macroman', // :TODO: validate 'MAC' : 'macroman', // :TODO: validate
}[ident]; }[ident];
} }

View File

@ -1,21 +1,21 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const MenuView = require('./menu_view.js').MenuView; const MenuView = require('./menu_view.js').MenuView;
const strUtil = require('./string_util.js'); const strUtil = require('./string_util.js');
const formatString = require('./string_format'); const formatString = require('./string_format');
const { pipeToAnsi } = require('./color_codes.js'); const { pipeToAnsi } = require('./color_codes.js');
const { goto } = require('./ansi_term.js'); const { goto } = require('./ansi_term.js');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
exports.HorizontalMenuView = HorizontalMenuView; exports.HorizontalMenuView = HorizontalMenuView;
// :TODO: Update this to allow scrolling if number of items cannot fit in width (similar to VerticalMenuView) // :TODO: Update this to allow scrolling if number of items cannot fit in width (similar to VerticalMenuView)
function HorizontalMenuView(options) { function HorizontalMenuView(options) {
options.cursor = options.cursor || 'hide'; options.cursor = options.cursor || 'hide';
if(!_.isNumber(options.itemSpacing)) { if(!_.isNumber(options.itemSpacing)) {
options.itemSpacing = 1; options.itemSpacing = 1;
@ -23,7 +23,7 @@ function HorizontalMenuView(options) {
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;
@ -33,8 +33,8 @@ function HorizontalMenuView(options) {
this.performAutoScale = function() { this.performAutoScale = function() {
if(self.autoScale.width) { if(self.autoScale.width) {
var spacer = self.getSpacer(); var spacer = self.getSpacer();
var width = self.items.join(spacer).length + (spacer.length * 2); var width = self.items.join(spacer).length + (spacer.length * 2);
assert(width <= self.client.term.termWidth - self.position.col); assert(width <= self.client.term.termWidth - self.position.col);
self.dimens.width = width; self.dimens.width = width;
} }
@ -44,8 +44,8 @@ function HorizontalMenuView(options) {
this.cachePositions = function() { this.cachePositions = function() {
if(this.positionCacheExpired) { if(this.positionCacheExpired) {
var col = self.position.col; var col = self.position.col;
var spacer = self.getSpacer(); var spacer = self.getSpacer();
for(var i = 0; i < self.items.length; ++i) { for(var i = 0; i < self.items.length; ++i) {
self.items[i].col = col; self.items[i].col = col;
@ -90,7 +90,7 @@ require('util').inherits(HorizontalMenuView, MenuView);
HorizontalMenuView.prototype.setHeight = function(height) { HorizontalMenuView.prototype.setHeight = function(height) {
height = parseInt(height, 10); height = parseInt(height, 10);
assert(1 === height); // nothing else allowed here assert(1 === height); // nothing else allowed here
HorizontalMenuView.super_.prototype.setHeight(this, height); HorizontalMenuView.super_.prototype.setHeight(this, height);
}; };
@ -130,7 +130,7 @@ HorizontalMenuView.prototype.focusNext = function() {
this.focusedItemIndex++; this.focusedItemIndex++;
} }
// :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes
this.redraw(); this.redraw();
HorizontalMenuView.super_.prototype.focusNext.call(this); HorizontalMenuView.super_.prototype.focusNext.call(this);
@ -144,7 +144,7 @@ HorizontalMenuView.prototype.focusPrevious = function() {
this.focusedItemIndex--; this.focusedItemIndex--;
} }
// :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes
this.redraw(); this.redraw();
HorizontalMenuView.super_.prototype.focusPrevious.call(this); HorizontalMenuView.super_.prototype.focusPrevious.call(this);

View File

@ -1,12 +1,12 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const View = require('./view.js').View; const View = require('./view.js').View;
const valueWithDefault = require('./misc_util.js').valueWithDefault; const valueWithDefault = require('./misc_util.js').valueWithDefault;
const isPrintable = require('./string_util.js').isPrintable; const isPrintable = require('./string_util.js').isPrintable;
const stylizeString = require('./string_util.js').stylizeString; const stylizeString = require('./string_util.js').stylizeString;
const _ = require('lodash'); const _ = require('lodash');
module.exports = class KeyEntryView extends View { module.exports = class KeyEntryView extends View {
constructor(options) { constructor(options) {
@ -15,8 +15,8 @@ module.exports = class KeyEntryView extends View {
super(options); super(options);
this.eatTabKey = options.eatTabKey || true; this.eatTabKey = options.eatTabKey || true;
this.caseInsensitive = options.caseInsensitive || true; this.caseInsensitive = options.caseInsensitive || true;
if(Array.isArray(options.keys)) { if(Array.isArray(options.keys)) {
if(this.caseInsensitive) { if(this.caseInsensitive) {
@ -35,7 +35,7 @@ module.exports = class KeyEntryView extends View {
} }
if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) { if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) {
this.redraw(); // sets position this.redraw(); // sets position
this.client.term.write(stylizeString(ch, this.textStyle)); this.client.term.write(stylizeString(ch, this.textStyle));
} }
@ -46,7 +46,7 @@ module.exports = class KeyEntryView extends View {
} }
this.emit('action', 'accept'); this.emit('action', 'accept');
// NOTE: we don't call super here. KeyEntryView is a special snowflake. // NOTE: we don't call super here. KeyEntryView is a special snowflake.
} }
setPropertyValue(propName, propValue) { setPropertyValue(propName, propValue) {
@ -73,5 +73,5 @@ module.exports = class KeyEntryView extends View {
super.setPropertyValue(propName, propValue); super.setPropertyValue(propName, propValue);
} }
getData() { return this.keyEntered; } getData() { return this.keyEntered; }
}; };

View File

@ -1,37 +1,37 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const User = require('./user.js'); const User = require('./user.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
// deps // deps
const moment = require('moment'); const moment = require('moment');
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
/* /*
Available listFormat object members: Available listFormat object members:
userId userId
userName userName
location location
affiliation affiliation
ts ts
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'Last Callers', name : 'Last Callers',
desc : 'Last callers to the system', desc : 'Last callers to the system',
author : 'NuSkooler', author : 'NuSkooler',
packageName : 'codes.l33t.enigma.lastcallers' packageName : 'codes.l33t.enigma.lastcallers'
}; };
const MciCodeIds = { const MciCodeIds = {
CallerList : 1, CallerList : 1,
}; };
exports.getModule = class LastCallersModule extends MenuModule { exports.getModule = class LastCallersModule extends MenuModule {
@ -45,8 +45,8 @@ exports.getModule = class LastCallersModule extends MenuModule {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
let loginHistory; let loginHistory;
let callersView; let callersView;
@ -55,9 +55,9 @@ exports.getModule = class LastCallersModule extends MenuModule {
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu, mciMap : mciData.menu,
noInput : true, noInput : true,
}; };
vc.loadFromMenuConfig(loadOpts, callback); vc.loadFromMenuConfig(loadOpts, callback);
@ -65,18 +65,18 @@ exports.getModule = class LastCallersModule extends MenuModule {
function fetchHistory(callback) { function fetchHistory(callback) {
callersView = vc.getView(MciCodeIds.CallerList); callersView = vc.getView(MciCodeIds.CallerList);
// fetch up // fetch up
StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => { StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => {
loginHistory = lh; loginHistory = lh;
if(self.menuConfig.config.hideSysOpLogin) { if(self.menuConfig.config.hideSysOpLogin) {
const noOpLoginHistory = loginHistory.filter(lh => { const noOpLoginHistory = loginHistory.filter(lh => {
return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId
}); });
// //
// If we have enough items to display, or hideSysOpLogin is set to 'always', // If we have enough items to display, or hideSysOpLogin is set to 'always',
// then set loginHistory to our filtered list. Else, we'll leave it be. // then set loginHistory to our filtered list. Else, we'll leave it be.
// //
if(noOpLoginHistory.length >= callersView.dimens.height || 'always' === self.menuConfig.config.hideSysOpLogin) { if(noOpLoginHistory.length >= callersView.dimens.height || 'always' === self.menuConfig.config.hideSysOpLogin) {
loginHistory = noOpLoginHistory; loginHistory = noOpLoginHistory;
@ -84,7 +84,7 @@ exports.getModule = class LastCallersModule extends MenuModule {
} }
// //
// Finally, we need to trim up the list to the needed size // Finally, we need to trim up the list to the needed size
// //
loginHistory = loginHistory.slice(0, callersView.dimens.height); loginHistory = loginHistory.slice(0, callersView.dimens.height);
@ -93,7 +93,7 @@ exports.getModule = class LastCallersModule extends MenuModule {
}, },
function getUserNamesAndProperties(callback) { function getUserNamesAndProperties(callback) {
const getPropOpts = { const getPropOpts = {
names : [ 'location', 'affiliation' ] names : [ 'location', 'affiliation' ]
}; };
const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD';
@ -102,7 +102,7 @@ exports.getModule = class LastCallersModule extends MenuModule {
loginHistory, loginHistory,
(item, next) => { (item, next) => {
item.userId = parseInt(item.log_value); item.userId = parseInt(item.log_value);
item.ts = moment(item.timestamp).format(dateTimeFormat); item.ts = moment(item.timestamp).format(dateTimeFormat);
User.getUserName(item.userId, (err, userName) => { User.getUserName(item.userId, (err, userName) => {
if(err) { if(err) {
@ -113,11 +113,11 @@ exports.getModule = class LastCallersModule extends MenuModule {
User.loadProperties(item.userId, getPropOpts, (err, props) => { User.loadProperties(item.userId, getPropOpts, (err, props) => {
if(!err && props) { if(!err && props) {
item.location = props.location || 'N/A'; item.location = props.location || 'N/A';
item.affiliation = item.affils = (props.affiliation || 'N/A'); item.affiliation = item.affils = (props.affiliation || 'N/A');
} else { } else {
item.location = 'N/A'; item.location = 'N/A';
item.affiliation = item.affils = 'N/A'; item.affiliation = item.affils = 'N/A';
} }
return next(null); return next(null);
}); });

View File

@ -1,17 +1,17 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const logger = require('./logger.js'); const logger = require('./logger.js');
// deps // deps
const async = require('async'); const async = require('async');
const listeningServers = {}; // packageName -> info const listeningServers = {}; // packageName -> info
exports.startup = startup; exports.startup = startup;
exports.shutdown = shutdown; exports.shutdown = shutdown;
exports.getServer = getServer; exports.getServer = getServer;
function startup(cb) { function startup(cb) {
return startListening(cb); return startListening(cb);
@ -26,11 +26,11 @@ function getServer(packageName) {
} }
function startListening(cb) { function startListening(cb) {
const moduleUtil = require('./module_util.js'); // late load so we get Config const moduleUtil = require('./module_util.js'); // late load so we get Config
async.each( [ 'login', 'content' ], (category, next) => { async.each( [ 'login', 'content' ], (category, next) => {
moduleUtil.loadModulesForCategory(`${category}Servers`, (err, module) => { moduleUtil.loadModulesForCategory(`${category}Servers`, (err, module) => {
// :TODO: use enig error here! // :TODO: use enig error here!
if(err) { if(err) {
if('EENIGMODDISABLED' === err.code) { if('EENIGMODDISABLED' === err.code) {
logger.log.debug(err.message); logger.log.debug(err.message);
@ -48,8 +48,8 @@ function startListening(cb) {
} }
listeningServers[module.moduleInfo.packageName] = { listeningServers[module.moduleInfo.packageName] = {
instance : moduleInst, instance : moduleInst,
info : module.moduleInfo, info : module.moduleInfo,
}; };
} catch(e) { } catch(e) {

View File

@ -1,21 +1,21 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// deps // deps
const bunyan = require('bunyan'); const bunyan = require('bunyan');
const paths = require('path'); const paths = require('path');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const _ = require('lodash'); const _ = require('lodash');
module.exports = class Log { module.exports = class Log {
static init() { static init() {
const Config = require('./config.js').get(); const Config = require('./config.js').get();
const logPath = Config.paths.logs; const logPath = Config.paths.logs;
const err = this.checkLogPath(logPath); const err = this.checkLogPath(logPath);
if(err) { if(err) {
console.error(err.message); // eslint-disable-line no-console console.error(err.message); // eslint-disable-line no-console
return process.exit(); return process.exit();
} }
@ -26,18 +26,18 @@ module.exports = class Log {
} }
const serializers = { const serializers = {
err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc. err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc.
}; };
// try to remove sensitive info by default, e.g. 'password' fields // try to remove sensitive info by default, e.g. 'password' fields
[ 'formData', 'formValue' ].forEach(keyName => { [ 'formData', 'formValue' ].forEach(keyName => {
serializers[keyName] = (fd) => Log.hideSensitive(fd); serializers[keyName] = (fd) => Log.hideSensitive(fd);
}); });
this.log = bunyan.createLogger({ this.log = bunyan.createLogger({
name : 'ENiGMA½ BBS', name : 'ENiGMA½ BBS',
streams : logStreams, streams : logStreams,
serializers : serializers, serializers : serializers,
}); });
} }
@ -59,7 +59,7 @@ module.exports = class Log {
static hideSensitive(obj) { static hideSensitive(obj) {
try { try {
// //
// Use a regexp -- we don't know how nested fields we want to seek and destroy may be // Use a regexp -- we don't know how nested fields we want to seek and destroy may be
// //
return JSON.parse( return JSON.parse(
JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => { JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => {
@ -67,7 +67,7 @@ module.exports = class Log {
}) })
); );
} catch(e) { } catch(e) {
// be safe and return empty obj! // be safe and return empty obj!
return {}; return {};
} }
} }

View File

@ -1,27 +1,27 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const conf = require('./config.js'); const conf = require('./config.js');
const logger = require('./logger.js'); const logger = require('./logger.js');
const ServerModule = require('./server_module.js').ServerModule; const ServerModule = require('./server_module.js').ServerModule;
const clientConns = require('./client_connections.js'); const clientConns = require('./client_connections.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
module.exports = class LoginServerModule extends ServerModule { module.exports = class LoginServerModule extends ServerModule {
constructor() { constructor() {
super(); super();
} }
// :TODO: we need to max connections -- e.g. from config 'maxConnections' // :TODO: we need to max connections -- e.g. from config 'maxConnections'
prepareClient(client, cb) { prepareClient(client, cb) {
const theme = require('./theme.js'); const theme = require('./theme.js');
// //
// Choose initial theme before we have user context // Choose initial theme before we have user context
// //
if('*' === conf.config.preLoginTheme) { if('*' === conf.config.preLoginTheme) {
client.user.properties.theme_id = theme.getRandomTheme() || ''; client.user.properties.theme_id = theme.getRandomTheme() || '';
@ -35,15 +35,15 @@ module.exports = class LoginServerModule extends ServerModule {
handleNewClient(client, clientSock, modInfo) { handleNewClient(client, clientSock, modInfo) {
// //
// Start tracking the client. We'll assign it an ID which is // Start tracking the client. We'll assign it an ID which is
// just the index in our connections array. // just the index in our connections array.
// //
if(_.isUndefined(client.session)) { if(_.isUndefined(client.session)) {
client.session = {}; client.session = {};
} }
client.session.serverName = modInfo.name; client.session.serverName = modInfo.name;
client.session.isSecure = _.isBoolean(client.isSecure) ? client.isSecure : (modInfo.isSecure || false); client.session.isSecure = _.isBoolean(client.isSecure) ? client.isSecure : (modInfo.isSecure || false);
clientConns.addNewClient(client, clientSock); clientConns.addNewClient(client, clientSock);
@ -51,7 +51,7 @@ module.exports = class LoginServerModule extends ServerModule {
client.startIdleMonitor(); client.startIdleMonitor();
// Go to module -- use default error handler // Go to module -- use default error handler
this.prepareClient(client, () => { this.prepareClient(client, () => {
require('./connect.js').connectEntry(client, readyOptions.firstMenu); require('./connect.js').connectEntry(client, readyOptions.firstMenu);
}); });
@ -77,7 +77,7 @@ module.exports = class LoginServerModule extends ServerModule {
client.menuStack.goto('idleLogoff', err => { client.menuStack.goto('idleLogoff', err => {
if(err) { if(err) {
// likely just doesn't exist // likely just doesn't exist
client.term.write('\nIdle timeout expired. Goodbye!\n'); client.term.write('\nIdle timeout expired. Goodbye!\n');
client.end(); client.end();
} }

View File

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

View File

@ -1,25 +1,25 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const Address = require('./ftn_address.js'); const Address = require('./ftn_address.js');
const Message = require('./message.js'); const Message = require('./message.js');
exports.getAddressedToInfo = getAddressedToInfo; exports.getAddressedToInfo = getAddressedToInfo;
const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
/* /*
Input Output Input Output
---------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------------------
User { name : 'User', flavor : 'local' } User { name : 'User', flavor : 'local' }
Some User { name : 'Some User', flavor : 'local' } Some User { name : 'Some User', flavor : 'local' }
JoeUser @ 1:103/75 { name : 'JoeUser', flavor : 'ftn', remote : '1:103/75' } JoeUser @ 1:103/75 { name : 'JoeUser', flavor : 'ftn', remote : '1:103/75' }
Bob@1:103/705@fidonet.org { name : 'Bob', flavor : 'ftn', remote : '1:103/705@fidonet.org' } Bob@1:103/705@fidonet.org { name : 'Bob', flavor : 'ftn', remote : '1:103/705@fidonet.org' }
1:103/705@fidonet.org { flavor : 'ftn', remote : '1:103/705@fidonet.org' } 1:103/705@fidonet.org { flavor : 'ftn', remote : '1:103/705@fidonet.org' }
Jane <23:4/100> { name : 'Jane', flavor : 'ftn', remote : '23:4/100' } Jane <23:4/100> { name : 'Jane', flavor : 'ftn', remote : '23:4/100' }
43:20/100.2 { flavor : 'ftn', remote : '43:20/100.2' } 43:20/100.2 { flavor : 'ftn', remote : '43:20/100.2' }
foo@host.com { name : 'foo', flavor : 'email', remote : 'foo@host.com' } foo@host.com { name : 'foo', flavor : 'email', remote : 'foo@host.com' }
Bar <baz@foobar.net> { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' } Bar <baz@foobar.net> { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' }
*/ */
function getAddressedToInfo(input) { function getAddressedToInfo(input) {
input = input.trim(); input = input.trim();
@ -50,8 +50,8 @@ function getAddressedToInfo(input) {
return { name : input, flavor : Message.AddressFlavor.Local }; return { name : input, flavor : Message.AddressFlavor.Local };
} }
const lessThanPos = input.indexOf('<'); const lessThanPos = input.indexOf('<');
const greaterThanPos = input.indexOf('>'); const greaterThanPos = input.indexOf('>');
if(lessThanPos > 0 && greaterThanPos > lessThanPos) { if(lessThanPos > 0 && greaterThanPos > lessThanPos) {
const addr = input.slice(lessThanPos + 1, greaterThanPos); const addr = input.slice(lessThanPos + 1, greaterThanPos);
const m = addr.match(EMAIL_REGEX); const m = addr.match(EMAIL_REGEX);
@ -67,7 +67,7 @@ function getAddressedToInfo(input) {
return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input }; return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input };
} }
let addr = Address.fromString(input); // 5D? let addr = Address.fromString(input); // 5D?
if(Address.isValidAddress(addr)) { if(Address.isValidAddress(addr)) {
return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ; return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ;
} }

View File

@ -1,42 +1,42 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var TextView = require('./text_view.js').TextView; var TextView = require('./text_view.js').TextView;
var miscUtil = require('./misc_util.js'); var miscUtil = require('./misc_util.js');
var strUtil = require('./string_util.js'); var strUtil = require('./string_util.js');
var ansi = require('./ansi_term.js'); var ansi = require('./ansi_term.js');
//var util = require('util'); //var util = require('util');
var assert = require('assert'); var assert = require('assert');
var _ = require('lodash'); var _ = require('lodash');
exports.MaskEditTextView = MaskEditTextView; exports.MaskEditTextView = MaskEditTextView;
// ##/##/#### <--styleSGR2 if fillChar // ##/##/#### <--styleSGR2 if fillChar
// ^- styleSGR1 // ^- styleSGR1
// buildPattern -> [ RE, RE, '/', RE, RE, '/', RE, RE, RE, RE ] // buildPattern -> [ RE, RE, '/', RE, RE, '/', RE, RE, RE, RE ]
// patternIndex -----^ // patternIndex -----^
// styleSGR1: Literal's (non-focus) // styleSGR1: Literal's (non-focus)
// styleSGR2: Literals (focused) // styleSGR2: Literals (focused)
// styleSGR3: fillChar // styleSGR3: fillChar
// //
// :TODO: // :TODO:
// * Hint, e.g. YYYY/MM/DD // * Hint, e.g. YYYY/MM/DD
// * Return values with literals in place // * Return values with literals in place
// //
function MaskEditTextView(options) { function MaskEditTextView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
options.resizable = false; options.resizable = false;
TextView.call(this, options); TextView.call(this, options);
this.cursorPos = { x : 0 }; this.cursorPos = { x : 0 };
this.patternArrayPos = 0; this.patternArrayPos = 0;
var self = this; var self = this;
@ -52,7 +52,7 @@ function MaskEditTextView(options) {
assert(textToDraw.length <= self.patternArray.length); assert(textToDraw.length <= self.patternArray.length);
// draw out the text we have so far // draw out the text we have so far
var i = 0; var i = 0;
var t = 0; var t = 0;
while(i < self.patternArray.length) { while(i < self.patternArray.length) {
@ -72,11 +72,11 @@ function MaskEditTextView(options) {
}; };
this.buildPattern = function() { this.buildPattern = function() {
self.patternArray = []; self.patternArray = [];
self.maxLength = 0; self.maxLength = 0;
for(var i = 0; i < self.maskPattern.length; i++) { for(var i = 0; i < self.maskPattern.length; i++) {
// :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark! // :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark!
if(self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) { if(self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) {
self.patternArray.push(MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]); self.patternArray.push(MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]);
++self.maxLength; ++self.maxLength;
@ -97,16 +97,16 @@ function MaskEditTextView(options) {
require('util').inherits(MaskEditTextView, TextView); require('util').inherits(MaskEditTextView, TextView);
MaskEditTextView.maskPatternCharacterRegEx = { MaskEditTextView.maskPatternCharacterRegEx = {
'#' : /[0-9]/, // Numeric '#' : /[0-9]/, // Numeric
'A' : /[a-zA-Z]/, // Alpha 'A' : /[a-zA-Z]/, // Alpha
'@' : /[0-9a-zA-Z]/, // Alphanumeric '@' : /[0-9a-zA-Z]/, // Alphanumeric
'&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255 '&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255
}; };
MaskEditTextView.prototype.setText = function(text) { MaskEditTextView.prototype.setText = function(text) {
MaskEditTextView.super_.prototype.setText.call(this, text); MaskEditTextView.super_.prototype.setText.call(this, text);
if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText() if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText()
this.patternArrayPos = this.patternArray.length; this.patternArrayPos = this.patternArray.length;
} }
}; };
@ -143,9 +143,9 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) {
return; return;
} else if(this.isKeyMapped('clearLine', key.name)) { } else if(this.isKeyMapped('clearLine', key.name)) {
this.text = ''; this.text = '';
this.patternArrayPos = 0; this.patternArrayPos = 0;
this.setFocus(true); // redraw + adjust cursor this.setFocus(true); // redraw + adjust cursor
return; return;
} }
@ -163,7 +163,7 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) {
this.patternArrayPos++; this.patternArrayPos++;
while(this.patternArrayPos < this.patternArray.length && while(this.patternArrayPos < this.patternArray.length &&
!_.isRegExp(this.patternArray[this.patternArrayPos])) !_.isRegExp(this.patternArray[this.patternArrayPos]))
{ {
this.patternArrayPos++; this.patternArrayPos++;
} }
@ -178,7 +178,7 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) {
MaskEditTextView.prototype.setPropertyValue = function(propName, value) { MaskEditTextView.prototype.setPropertyValue = function(propName, value) {
switch(propName) { switch(propName) {
case 'maskPattern' : this.setMaskPattern(value); break; case 'maskPattern' : this.setMaskPattern(value); break;
} }
MaskEditTextView.super_.prototype.setPropertyValue.call(this, propName, value); MaskEditTextView.super_.prototype.setPropertyValue.call(this, propName, value);
@ -191,7 +191,7 @@ MaskEditTextView.prototype.getData = function() {
return rawData; return rawData;
} }
var data = ''; var data = '';
assert(rawData.length <= this.patternArray.length); assert(rawData.length <= this.patternArray.length);

View File

@ -1,25 +1,25 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const TextView = require('./text_view.js').TextView; const TextView = require('./text_view.js').TextView;
const EditTextView = require('./edit_text_view.js').EditTextView; const EditTextView = require('./edit_text_view.js').EditTextView;
const ButtonView = require('./button_view.js').ButtonView; const ButtonView = require('./button_view.js').ButtonView;
const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView;
const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView;
const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView;
const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView;
const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView;
const KeyEntryView = require('./key_entry_view.js'); const KeyEntryView = require('./key_entry_view.js');
const MultiLineEditTextView = require('./multi_line_edit_text_view.js').MultiLineEditTextView; const MultiLineEditTextView = require('./multi_line_edit_text_view.js').MultiLineEditTextView;
const getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue; const getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue;
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
// deps // deps
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
exports.MCIViewFactory = MCIViewFactory; exports.MCIViewFactory = MCIViewFactory;
function MCIViewFactory(client) { function MCIViewFactory(client) {
this.client = client; this.client = client;
@ -29,9 +29,9 @@ MCIViewFactory.UserViewCodes = [
'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE', 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE',
// //
// XY is a special MCI code that allows finding positions // XY is a special MCI code that allows finding positions
// and counts for key lookup, but does not explicitly // and counts for key lookup, but does not explicitly
// represent a visible View on it's own // represent a visible View on it's own
// //
'XY', 'XY',
]; ];
@ -43,14 +43,14 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
var view; var view;
var options = { var options = {
client : this.client, client : this.client,
id : mci.id, id : mci.id,
ansiSGR : mci.SGR, ansiSGR : mci.SGR,
ansiFocusSGR : mci.focusSGR, ansiFocusSGR : mci.focusSGR,
position : { row : mci.position[0], col : mci.position[1] }, position : { row : mci.position[0], col : mci.position[1] },
}; };
// :TODO: These should use setPropertyValue()! // :TODO: These should use setPropertyValue()!
function setOption(pos, name) { function setOption(pos, name) {
if(mci.args.length > pos && mci.args[pos].length > 0) { if(mci.args.length > pos && mci.args[pos].length > 0) {
options[name] = mci.args[pos]; options[name] = mci.args[pos];
@ -73,44 +73,44 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
} }
// //
// Note: Keep this in sync with UserViewCodes above! // Note: Keep this in sync with UserViewCodes above!
// //
switch(mci.code) { switch(mci.code) {
// Text Label (Text View) // Text Label (Text View)
case 'TL' : case 'TL' :
setOption(0, 'textStyle'); setOption(0, 'textStyle');
setOption(1, 'justify'); setOption(1, 'justify');
setWidth(2); setWidth(2);
view = new TextView(options); view = new TextView(options);
break; break;
// Edit Text // Edit Text
case 'ET' : case 'ET' :
setWidth(0); setWidth(0);
setOption(1, 'textStyle'); setOption(1, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new EditTextView(options); view = new EditTextView(options);
break; break;
// Masked Edit Text // Masked Edit Text
case 'ME' : case 'ME' :
setOption(0, 'textStyle'); setOption(0, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new MaskEditTextView(options); view = new MaskEditTextView(options);
break; break;
// Multi Line Edit Text // Multi Line Edit Text
case 'MT' : case 'MT' :
// :TODO: apply params // :TODO: apply params
view = new MultiLineEditTextView(options); view = new MultiLineEditTextView(options);
break; break;
// Pre-defined Label (Text View) // Pre-defined Label (Text View)
// :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove // :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove
case 'PL' : case 'PL' :
if(mci.args.length > 0) { if(mci.args.length > 0) {
options.text = getPredefinedMCIValue(this.client, mci.args[0]); options.text = getPredefinedMCIValue(this.client, mci.args[0]);
@ -124,7 +124,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
} }
break; break;
// Button // Button
case 'BT' : case 'BT' :
if(mci.args.length > 0) { if(mci.args.length > 0) {
options.dimens = { width : parseInt(mci.args[0], 10) }; options.dimens = { width : parseInt(mci.args[0], 10) };
@ -138,32 +138,32 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
view = new ButtonView(options); view = new ButtonView(options);
break; break;
// Vertial Menu // Vertial Menu
case 'VM' : case 'VM' :
setOption(0, 'itemSpacing'); setOption(0, 'itemSpacing');
setOption(1, 'justify'); setOption(1, 'justify');
setOption(2, 'textStyle'); setOption(2, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new VerticalMenuView(options); view = new VerticalMenuView(options);
break; break;
// Horizontal Menu // Horizontal Menu
case 'HM' : case 'HM' :
setOption(0, 'itemSpacing'); setOption(0, 'itemSpacing');
setOption(1, 'textStyle'); setOption(1, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new HorizontalMenuView(options); view = new HorizontalMenuView(options);
break; break;
case 'SM' : case 'SM' :
setOption(0, 'textStyle'); setOption(0, 'textStyle');
setOption(1, 'justify'); setOption(1, 'justify');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new SpinnerMenuView(options); view = new SpinnerMenuView(options);
break; break;
@ -177,7 +177,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
options.styleSG1 = ansi.getSGRFromGraphicRendition(styleSG1, true); options.styleSG1 = ansi.getSGRFromGraphicRendition(styleSG1, true);
} }
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new ToggleMenuView(options); view = new ToggleMenuView(options);
break; break;
@ -191,8 +191,8 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
if(_.isString(options.text)) { if(_.isString(options.text)) {
setWidth(0); setWidth(0);
setOption(1, 'textStyle'); setOption(1, 'textStyle');
setOption(2, 'justify'); setOption(2, 'justify');
view = new TextView(options); view = new TextView(options);
} }

View File

@ -1,37 +1,37 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const PluginModule = require('./plugin_module.js').PluginModule; const PluginModule = require('./plugin_module.js').PluginModule;
const theme = require('./theme.js'); const theme = require('./theme.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const menuUtil = require('./menu_util.js'); const menuUtil = require('./menu_util.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const stringFormat = require('../core/string_format.js'); const stringFormat = require('../core/string_format.js');
const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
const Errors = require('../core/enig_error.js').Errors; const Errors = require('../core/enig_error.js').Errors;
const { getPredefinedMCIValue } = require('../core/predefined_mci.js'); const { getPredefinedMCIValue } = require('../core/predefined_mci.js');
// deps // deps
const async = require('async'); const async = require('async');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
exports.MenuModule = class MenuModule extends PluginModule { exports.MenuModule = class MenuModule extends PluginModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.menuName = options.menuName; this.menuName = options.menuName;
this.menuConfig = options.menuConfig; this.menuConfig = options.menuConfig;
this.client = options.client; this.client = options.client;
this.menuConfig.options = options.menuConfig.options || {}; this.menuConfig.options = options.menuConfig.options || {};
this.menuMethods = {}; // methods called from @method's this.menuMethods = {}; // methods called from @method's
this.menuConfig.config = this.menuConfig.config || {}; this.menuConfig.config = this.menuConfig.config || {};
this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls; this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls;
this.viewControllers = {}; this.viewControllers = {};
} }
enter() { enter() {
@ -43,8 +43,8 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
initSequence() { initSequence() {
const self = this; const self = this;
const mciData = {}; const mciData = {};
let pausePosition; let pausePosition;
async.series( async.series(
@ -67,13 +67,13 @@ exports.MenuModule = class MenuModule extends PluginModule {
mciData.menu = artData.mciMap; mciData.menu = artData.mciMap;
} }
return callback(null); // any errors are non-fatal return callback(null); // any errors are non-fatal
} }
); );
}, },
function moveToPromptLocation(callback) { function moveToPromptLocation(callback) {
if(self.menuConfig.prompt) { if(self.menuConfig.prompt) {
// :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements // :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements
} }
return callback(null); return callback(null);
@ -94,13 +94,13 @@ exports.MenuModule = class MenuModule extends PluginModule {
if(artData) { if(artData) {
mciData.prompt = artData.mciMap; mciData.prompt = artData.mciMap;
} }
return callback(err); // pass err here; prompts *must* have art return callback(err); // pass err here; prompts *must* have art
} }
); );
}, },
function recordCursorPosition(callback) { function recordCursorPosition(callback) {
if(!self.shouldPause()) { if(!self.shouldPause()) {
return callback(null); // cursor position not needed return callback(null); // cursor position not needed
} }
self.client.once('cursor position report', pos => { self.client.once('cursor position report', pos => {
@ -138,7 +138,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
beforeArt(cb) { beforeArt(cb) {
if(_.isNumber(this.menuConfig.options.baudRate)) { if(_.isNumber(this.menuConfig.options.baudRate)) {
// :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here // :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here
this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate)); this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate));
} }
@ -150,30 +150,30 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
// available for sub-classes // available for sub-classes
return cb(null); return cb(null);
} }
finishedLoading() { finishedLoading() {
// nothing in base // nothing in base
} }
getSaveState() { getSaveState() {
// nothing in base // nothing in base
} }
restoreSavedState(/*savedState*/) { restoreSavedState(/*savedState*/) {
// nothing in base // nothing in base
} }
getMenuResult() { getMenuResult() {
// default to the formData that was provided @ a submit, if any // default to the formData that was provided @ a submit, if any
return this.submitFormData; return this.submitFormData;
} }
nextMenu(cb) { nextMenu(cb) {
if(!this.haveNext()) { if(!this.haveNext()) {
return this.prevMenu(cb); // no next, go to prev return this.prevMenu(cb); // no next, go to prev
} }
return this.client.menuStack.next(cb); return this.client.menuStack.next(cb);
@ -236,10 +236,10 @@ exports.MenuModule = class MenuModule extends PluginModule {
standardMCIReadyHandler(mciData, cb) { standardMCIReadyHandler(mciData, cb) {
// //
// A quick rundown: // A quick rundown:
// * We may have mciData.menu, mciData.prompt, or both. // * We may have mciData.menu, mciData.prompt, or both.
// * Prompt form is favored over menu form if both are present. // * Prompt form is favored over menu form if both are present.
// * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve) // * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve)
// //
const self = this; const self = this;
@ -259,9 +259,9 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
const menuLoadOpts = { const menuLoadOpts = {
mciMap : mciData.menu, mciMap : mciData.menu,
callingMenu : self, callingMenu : self,
withoutForm : _.isObject(mciData.prompt), withoutForm : _.isObject(mciData.prompt),
}; };
self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => { self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => {
@ -274,8 +274,8 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
const promptLoadOpts = { const promptLoadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.prompt, mciMap : mciData.prompt,
}; };
self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => { self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => {
@ -314,16 +314,16 @@ exports.MenuModule = class MenuModule extends PluginModule {
prepViewController(name, formId, mciMap, cb) { prepViewController(name, formId, mciMap, cb) {
if(_.isUndefined(this.viewControllers[name])) { if(_.isUndefined(this.viewControllers[name])) {
const vcOpts = { const vcOpts = {
client : this.client, client : this.client,
formId : formId, formId : formId,
}; };
const vc = this.addViewController(name, new ViewController(vcOpts)); const vc = this.addViewController(name, new ViewController(vcOpts));
const loadOpts = { const loadOpts = {
callingMenu : this, callingMenu : this,
mciMap : mciMap, mciMap : mciMap,
formId : formId, formId : formId,
}; };
return vc.loadFromMenuConfig(loadOpts, err => { return vc.loadFromMenuConfig(loadOpts, err => {
@ -371,20 +371,20 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
/* /*
:TODO: this needs quite a bit of work - but would be nice: promptForInput(..., (err, formData) => ... ) :TODO: this needs quite a bit of work - but would be nice: promptForInput(..., (err, formData) => ... )
promptForInput(formName, name, options, cb) { promptForInput(formName, name, options, cb) {
if(!cb && _.isFunction(options)) { if(!cb && _.isFunction(options)) {
cb = options; cb = options;
options = {}; options = {};
} }
options.viewController = this.viewControllers[formName]; options.viewController = this.viewControllers[formName];
this.optionalMoveToPosition(options.position); this.optionalMoveToPosition(options.position);
return theme.displayThemedPrompt(name, this.client, options, cb); return theme.displayThemedPrompt(name, this.client, options, cb);
} }
*/ */
setViewText(formName, mciId, text, appendMultiLine) { setViewText(formName, mciId, text, appendMultiLine) {
const view = this.viewControllers[formName].getView(mciId); const view = this.viewControllers[formName].getView(mciId);
@ -404,12 +404,12 @@ exports.MenuModule = class MenuModule extends PluginModule {
let textView; let textView;
let customMciId = startId; let customMciId = startId;
const config = this.menuConfig.config; const config = this.menuConfig.config;
const endId = options.endId || 99; // we'll fail to get a view before 99 const endId = options.endId || 99; // we'll fail to get a view before 99
while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) { while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) {
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10"
const format = config[key]; const format = config[key];
if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) { if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) {
const text = stringFormat(format, fmtObj); const text = stringFormat(format, fmtObj);

View File

@ -1,20 +1,20 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const loadMenu = require('./menu_util.js').loadMenu; const loadMenu = require('./menu_util.js').loadMenu;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
// :TODO: Stack is backwards.... top should be most recent! :) // :TODO: Stack is backwards.... top should be most recent! :)
module.exports = class MenuStack { module.exports = class MenuStack {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
this.stack = []; this.stack = [];
} }
push(moduleInfo) { push(moduleInfo) {
@ -52,8 +52,8 @@ module.exports = class MenuStack {
const currentModuleInfo = this.top(); const currentModuleInfo = this.top();
assert(currentModuleInfo, 'Empty menu stack!'); assert(currentModuleInfo, 'Empty menu stack!');
const menuConfig = currentModuleInfo.instance.menuConfig; const menuConfig = currentModuleInfo.instance.menuConfig;
const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next'); const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next');
if(!nextMenu) { if(!nextMenu) {
return cb(Array.isArray(menuConfig.next) ? return cb(Array.isArray(menuConfig.next) ?
Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH') : Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH') :
@ -71,16 +71,16 @@ module.exports = class MenuStack {
prev(cb) { prev(cb) {
const menuResult = this.top().instance.getMenuResult(); const menuResult = this.top().instance.getMenuResult();
// :TODO: leave() should really take a cb... // :TODO: leave() should really take a cb...
this.pop().instance.leave(); // leave & remove current this.pop().instance.leave(); // leave & remove current
const previousModuleInfo = this.pop(); // get previous const previousModuleInfo = this.pop(); // get previous
if(previousModuleInfo) { if(previousModuleInfo) {
const opts = { const opts = {
extraArgs : previousModuleInfo.extraArgs, extraArgs : previousModuleInfo.extraArgs,
savedState : previousModuleInfo.savedState, savedState : previousModuleInfo.savedState,
lastMenuResult : menuResult, lastMenuResult : menuResult,
}; };
return this.goto(previousModuleInfo.name, opts, cb); return this.goto(previousModuleInfo.name, opts, cb);
@ -108,8 +108,8 @@ module.exports = class MenuStack {
} }
const loadOpts = { const loadOpts = {
name : name, name : name,
client : self.client, client : self.client,
}; };
if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) { if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) {
@ -117,19 +117,19 @@ module.exports = class MenuStack {
} else { } else {
loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value'); loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value');
} }
loadOpts.lastMenuResult = options.lastMenuResult; loadOpts.lastMenuResult = options.lastMenuResult;
loadMenu(loadOpts, (err, modInst) => { loadMenu(loadOpts, (err, modInst) => {
if(err) { if(err) {
// :TODO: probably should just require a cb... // :TODO: probably should just require a cb...
const errCb = cb || self.client.defaultHandlerMissingMod(); const errCb = cb || self.client.defaultHandlerMissingMod();
errCb(err); errCb(err);
} else { } else {
self.client.log.debug( { menuName : name }, 'Goto menu module'); self.client.log.debug( { menuName : name }, 'Goto menu module');
// //
// If menuFlags were supplied in menu.hjson, they should win over // If menuFlags were supplied in menu.hjson, they should win over
// anything supplied in code. // anything supplied in code.
// //
let menuFlags; let menuFlags;
if(0 === modInst.menuConfig.options.menuFlags.length) { if(0 === modInst.menuConfig.options.menuFlags.length) {
@ -137,14 +137,14 @@ module.exports = class MenuStack {
} else { } else {
menuFlags = modInst.menuConfig.options.menuFlags; menuFlags = modInst.menuConfig.options.menuFlags;
// in code we can ask to merge in // in code we can ask to merge in
if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) { if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) {
menuFlags = _.uniq(menuFlags.concat(options.menuFlags)); menuFlags = _.uniq(menuFlags.concat(options.menuFlags));
} }
} }
if(currentModuleInfo) { if(currentModuleInfo) {
// save stack state // save stack state
currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState(); currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState();
currentModuleInfo.instance.leave(); currentModuleInfo.instance.leave();
@ -154,18 +154,18 @@ module.exports = class MenuStack {
} }
if(menuFlags.includes('popParent')) { if(menuFlags.includes('popParent')) {
this.pop().instance.leave(); // leave & remove current this.pop().instance.leave(); // leave & remove current
} }
} }
self.push({ self.push({
name : name, name : name,
instance : modInst, instance : modInst,
extraArgs : loadOpts.extraArgs, extraArgs : loadOpts.extraArgs,
menuFlags : menuFlags, menuFlags : menuFlags,
}); });
// restore previous state if requested // restore previous state if requested
if(options.savedState) { if(options.savedState) {
modInst.restoreSavedState(options.savedState); modInst.restoreSavedState(options.savedState);
} }

View File

@ -1,22 +1,22 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
var moduleUtil = require('./module_util.js'); var moduleUtil = require('./module_util.js');
var Log = require('./logger.js').log; var Log = require('./logger.js').log;
var Config = require('./config.js').get; var Config = require('./config.js').get;
var asset = require('./asset.js'); var asset = require('./asset.js');
var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory;
var paths = require('path'); var paths = require('path');
var async = require('async'); var async = require('async');
var assert = require('assert'); var assert = require('assert');
var _ = require('lodash'); var _ = require('lodash');
exports.loadMenu = loadMenu; exports.loadMenu = loadMenu;
exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap; exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap;
exports.handleAction = handleAction; exports.handleAction = handleAction;
exports.handleNext = handleNext; exports.handleNext = handleNext;
function getMenuConfig(client, name, cb) { function getMenuConfig(client, name, cb) {
var menuConfig; var menuConfig;
@ -70,20 +70,20 @@ function loadMenu(options, cb) {
menuConfig.options.menuFlags = [ menuConfig.options.menuFlags ]; menuConfig.options.menuFlags = [ menuConfig.options.menuFlags ];
} }
const modAsset = asset.getModuleAsset(menuConfig.module); const modAsset = asset.getModuleAsset(menuConfig.module);
const modSupplied = null !== modAsset; const modSupplied = null !== modAsset;
const modLoadOpts = { const modLoadOpts = {
name : modSupplied ? modAsset.asset : 'standard_menu', name : modSupplied ? modAsset.asset : 'standard_menu',
path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config().paths.mods, path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config().paths.mods,
category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods', category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods',
}; };
moduleUtil.loadModuleEx(modLoadOpts, (err, mod) => { moduleUtil.loadModuleEx(modLoadOpts, (err, mod) => {
const modData = { const modData = {
name : modLoadOpts.name, name : modLoadOpts.name,
config : menuConfig, config : menuConfig,
mod : mod, mod : mod,
}; };
return callback(err, modData); return callback(err, modData);
@ -97,11 +97,11 @@ function loadMenu(options, cb) {
let moduleInstance; let moduleInstance;
try { try {
moduleInstance = new modData.mod.getModule({ moduleInstance = new modData.mod.getModule({
menuName : options.name, menuName : options.name,
menuConfig : modData.config, menuConfig : modData.config,
extraArgs : options.extraArgs, extraArgs : options.extraArgs,
client : options.client, client : options.client,
lastMenuResult : options.lastMenuResult, lastMenuResult : options.lastMenuResult,
}); });
} catch(e) { } catch(e) {
return callback(e); return callback(e);
@ -137,7 +137,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) {
Log.trace( { mciKey : mciReqKey }, 'Looking for MCI configuration key'); Log.trace( { mciKey : mciReqKey }, 'Looking for MCI configuration key');
// //
// Exact, explicit match? // Exact, explicit match?
// //
if(_.isObject(formForId[mciReqKey])) { if(_.isObject(formForId[mciReqKey])) {
Log.trace( { mciKey : mciReqKey }, 'Using exact configuration key match'); Log.trace( { mciKey : mciReqKey }, 'Using exact configuration key match');
@ -146,7 +146,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) {
} }
// //
// Generic match // Generic match
// //
if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) { if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) {
Log.trace('Using generic configuration'); Log.trace('Using generic configuration');
@ -156,7 +156,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) {
cb(new Error('No matching form configuration found for key \'' + mciReqKey + '\'')); cb(new Error('No matching form configuration found for key \'' + mciReqKey + '\''));
} }
// :TODO: Most of this should be moved elsewhere .... DRY... // :TODO: Most of this should be moved elsewhere .... DRY...
function callModuleMenuMethod(client, asset, path, formData, extraArgs, cb) { function callModuleMenuMethod(client, asset, path, formData, extraArgs, cb) {
if('' === paths.extname(path)) { if('' === paths.extname(path)) {
path += '.js'; path += '.js';
@ -194,8 +194,8 @@ function handleAction(client, formData, conf, cb) {
conf.extraArgs, conf.extraArgs,
cb); cb);
} else if('systemMethod' === actionAsset.type) { } else if('systemMethod' === actionAsset.type) {
// :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. () // :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. ()
// :TODO: Probably better as system_method.js // :TODO: Probably better as system_method.js
return callModuleMenuMethod( return callModuleMenuMethod(
client, client,
actionAsset, actionAsset,
@ -204,7 +204,7 @@ function handleAction(client, formData, conf, cb) {
conf.extraArgs, conf.extraArgs,
cb); cb);
} else { } else {
// local to current module // local to current module
const currentModule = client.currentMenuModule; const currentModule = client.currentMenuModule;
if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) { if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) {
return currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs, cb); return currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs, cb);
@ -221,28 +221,28 @@ function handleAction(client, formData, conf, cb) {
} }
function handleNext(client, nextSpec, conf, cb) { function handleNext(client, nextSpec, conf, cb) {
nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); // handle any conditionals nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); // handle any conditionals
const nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu'); const nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu');
// :TODO: getAssetWithShorthand() can return undefined - handle it! // :TODO: getAssetWithShorthand() can return undefined - handle it!
conf = conf || {}; conf = conf || {};
const extraArgs = conf.extraArgs || {}; const extraArgs = conf.extraArgs || {};
// :TODO: DRY this with handleAction() // :TODO: DRY this with handleAction()
switch(nextAsset.type) { switch(nextAsset.type) {
case 'method' : case 'method' :
case 'systemMethod' : case 'systemMethod' :
if(_.isString(nextAsset.location)) { if(_.isString(nextAsset.location)) {
return callModuleMenuMethod(client, nextAsset, paths.join(Config().paths.mods, nextAsset.location), {}, extraArgs, cb); return callModuleMenuMethod(client, nextAsset, paths.join(Config().paths.mods, nextAsset.location), {}, extraArgs, cb);
} else if('systemMethod' === nextAsset.type) { } else if('systemMethod' === nextAsset.type) {
// :TODO: see other notes about system_menu_method.js here // :TODO: see other notes about system_menu_method.js here
return callModuleMenuMethod(client, nextAsset, paths.join(__dirname, 'system_menu_method.js'), {}, extraArgs, cb); return callModuleMenuMethod(client, nextAsset, paths.join(__dirname, 'system_menu_method.js'), {}, extraArgs, cb);
} else { } else {
// local to current module // local to current module
const currentModule = client.currentMenuModule; const currentModule = client.currentMenuModule;
if(_.isFunction(currentModule.menuMethods[nextAsset.asset])) { if(_.isFunction(currentModule.menuMethods[nextAsset.asset])) {
const formData = {}; // we don't have any const formData = {}; // we don't have any
return currentModule.menuMethods[nextAsset.asset]( formData, extraArgs, cb ); return currentModule.menuMethods[nextAsset.asset]( formData, extraArgs, cb );
} }

View File

@ -1,17 +1,17 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const View = require('./view.js').View; const View = require('./view.js').View;
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
const pipeToAnsi = require('./color_codes.js').pipeToAnsi; const pipeToAnsi = require('./color_codes.js').pipeToAnsi;
// deps // deps
const util = require('util'); const util = require('util');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
exports.MenuView = MenuView; exports.MenuView = MenuView;
function MenuView(options) { function MenuView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
@ -38,14 +38,14 @@ function MenuView(options) {
this.focusedItemIndex = options.focusedItemIndex || 0; this.focusedItemIndex = options.focusedItemIndex || 0;
this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0;
this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0;
// :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization
this.focusPrefix = options.focusPrefix || ''; this.focusPrefix = options.focusPrefix || '';
this.focusSuffix = options.focusSuffix || ''; this.focusSuffix = options.focusSuffix || '';
this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1);
this.justify = options.justify || 'none'; this.justify = options.justify || 'none';
this.hasFocusItems = function() { this.hasFocusItems = function() {
return !_.isUndefined(self.focusItems); return !_.isUndefined(self.focusItems);
@ -74,15 +74,15 @@ MenuView.prototype.setItems = function(items) {
this.renderCache = {}; this.renderCache = {};
// //
// Items can be an array of strings or an array of objects. // Items can be an array of strings or an array of objects.
// //
// In the case of objects, items are considered complex and // In the case of objects, items are considered complex and
// may have one or more members that can later be formatted // may have one or more members that can later be formatted
// against. The default member is 'text'. The member 'data' // against. The default member is 'text'. The member 'data'
// may be overridden to provide a form value other than the // may be overridden to provide a form value other than the
// item's index. // item's index.
// //
// Items can be formatted with 'itemFormat' and 'focusItemFormat' // Items can be formatted with 'itemFormat' and 'focusItemFormat'
// //
let text; let text;
let stringItem; let stringItem;
@ -96,7 +96,7 @@ MenuView.prototype.setItems = function(items) {
} }
text = this.disablePipe ? text : pipeToAnsi(text, this.client); text = this.disablePipe ? text : pipeToAnsi(text, this.client);
return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others
}); });
if(this.complexItems) { if(this.complexItems) {
@ -122,7 +122,7 @@ MenuView.prototype.setSort = function(sort) {
const key = true === sort ? 'text' : sort; const key = true === sort ? 'text' : sort;
if('text' !== sort && !this.complexItems) { if('text' !== sort && !this.complexItems) {
return; // need a valid sort key return; // need a valid sort key
} }
this.items.sort( (a, b) => { this.items.sort( (a, b) => {
@ -237,26 +237,26 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) {
itemSpacing = parseInt(itemSpacing); itemSpacing = parseInt(itemSpacing);
assert(_.isNumber(itemSpacing)); assert(_.isNumber(itemSpacing));
this.itemSpacing = itemSpacing; this.itemSpacing = itemSpacing;
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
MenuView.prototype.setPropertyValue = function(propName, value) { MenuView.prototype.setPropertyValue = function(propName, value) {
switch(propName) { switch(propName) {
case 'itemSpacing' : this.setItemSpacing(value); break; case 'itemSpacing' : this.setItemSpacing(value); break;
case 'items' : this.setItems(value); break; case 'items' : this.setItems(value); break;
case 'focusItems' : this.setFocusItems(value); break; case 'focusItems' : this.setFocusItems(value); break;
case 'hotKeys' : this.setHotKeys(value); break; case 'hotKeys' : this.setHotKeys(value); break;
case 'hotKeySubmit' : this.hotKeySubmit = value; break; case 'hotKeySubmit' : this.hotKeySubmit = value; break;
case 'justify' : this.justify = value; break; case 'justify' : this.justify = value; break;
case 'focusItemIndex' : this.focusedItemIndex = value; break; case 'focusItemIndex' : this.focusedItemIndex = value; break;
case 'itemFormat' : case 'itemFormat' :
case 'focusItemFormat' : case 'focusItemFormat' :
this[propName] = value; this[propName] = value;
break; break;
case 'sort' : this.setSort(value); break; case 'sort' : this.setSort(value); break;
} }
MenuView.super_.prototype.setPropertyValue.call(this, propName, value); MenuView.super_.prototype.setPropertyValue.call(this, propName, value);

View File

@ -1,96 +1,96 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const msgDb = require('./database.js').dbs.message; const msgDb = require('./database.js').dbs.message;
const wordWrapText = require('./word_wrap.js').wordWrapText; const wordWrapText = require('./word_wrap.js').wordWrapText;
const ftnUtil = require('./ftn_util.js'); const ftnUtil = require('./ftn_util.js');
const createNamedUUID = require('./uuid_util.js').createNamedUUID; const createNamedUUID = require('./uuid_util.js').createNamedUUID;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const ANSI = require('./ansi_term.js'); const ANSI = require('./ansi_term.js');
const { const {
sanatizeString, sanatizeString,
getISOTimestampString } = require('./database.js'); getISOTimestampString } = require('./database.js');
const { const {
isAnsi, isFormattedLine, isAnsi, isFormattedLine,
splitTextAtTerms, splitTextAtTerms,
renderSubstr renderSubstr
} = require('./string_util.js'); } = require('./string_util.js');
const ansiPrep = require('./ansi_prep.js'); const ansiPrep = require('./ansi_prep.js');
// deps // deps
const uuidParse = require('uuid-parse'); const uuidParse = require('uuid-parse');
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
const moment = require('moment'); const moment = require('moment');
const iconvEncode = require('iconv-lite').encode; const iconvEncode = require('iconv-lite').encode;
const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse('154506df-1df8-46b9-98f8-ebb5815baaf8'); const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse('154506df-1df8-46b9-98f8-ebb5815baaf8');
const WELL_KNOWN_AREA_TAGS = { const WELL_KNOWN_AREA_TAGS = {
Invalid : '', Invalid : '',
Private : 'private_mail', Private : 'private_mail',
Bulletin : 'local_bulletin', Bulletin : 'local_bulletin',
}; };
const SYSTEM_META_NAMES = { const SYSTEM_META_NAMES = {
LocalToUserID : 'local_to_user_id', LocalToUserID : 'local_to_user_id',
LocalFromUserID : 'local_from_user_id', LocalFromUserID : 'local_from_user_id',
StateFlags0 : 'state_flags0', // See Message.StateFlags0 StateFlags0 : 'state_flags0', // See Message.StateFlags0
ExplicitEncoding : 'explicit_encoding', // Explicitly set encoding when exporting/etc. ExplicitEncoding : 'explicit_encoding', // Explicitly set encoding when exporting/etc.
ExternalFlavor : 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor ExternalFlavor : 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor
RemoteToUser : 'remote_to_user', // Opaque value depends on external system, e.g. FTN address RemoteToUser : 'remote_to_user', // Opaque value depends on external system, e.g. FTN address
RemoteFromUser : 'remote_from_user', // Opaque value depends on external system, e.g. FTN address RemoteFromUser : 'remote_from_user', // Opaque value depends on external system, e.g. FTN address
}; };
// Types for Message.SystemMetaNames.ExternalFlavor meta // Types for Message.SystemMetaNames.ExternalFlavor meta
const ADDRESS_FLAVOR = { const ADDRESS_FLAVOR = {
Local : 'local', // local / non-remote addressing Local : 'local', // local / non-remote addressing
FTN : 'ftn', // FTN style FTN : 'ftn', // FTN style
Email : 'email', Email : 'email',
}; };
const STATE_FLAGS0 = { const STATE_FLAGS0 = {
None : 0x00000000, None : 0x00000000,
Imported : 0x00000001, // imported from foreign system Imported : 0x00000001, // imported from foreign system
Exported : 0x00000002, // exported to foreign system Exported : 0x00000002, // exported to foreign system
}; };
// :TODO: these should really live elsewhere... // :TODO: these should really live elsewhere...
const FTN_PROPERTY_NAMES = { const FTN_PROPERTY_NAMES = {
// packet header oriented // packet header oriented
FtnOrigNode : 'ftn_orig_node', FtnOrigNode : 'ftn_orig_node',
FtnDestNode : 'ftn_dest_node', FtnDestNode : 'ftn_dest_node',
// :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping // :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping
FtnOrigNetwork : 'ftn_orig_network', FtnOrigNetwork : 'ftn_orig_network',
FtnDestNetwork : 'ftn_dest_network', FtnDestNetwork : 'ftn_dest_network',
FtnAttrFlags : 'ftn_attr_flags', FtnAttrFlags : 'ftn_attr_flags',
FtnCost : 'ftn_cost', FtnCost : 'ftn_cost',
FtnOrigZone : 'ftn_orig_zone', FtnOrigZone : 'ftn_orig_zone',
FtnDestZone : 'ftn_dest_zone', FtnDestZone : 'ftn_dest_zone',
FtnOrigPoint : 'ftn_orig_point', FtnOrigPoint : 'ftn_orig_point',
FtnDestPoint : 'ftn_dest_point', FtnDestPoint : 'ftn_dest_point',
// message header oriented // message header oriented
FtnMsgOrigNode : 'ftn_msg_orig_node', FtnMsgOrigNode : 'ftn_msg_orig_node',
FtnMsgDestNode : 'ftn_msg_dest_node', FtnMsgDestNode : 'ftn_msg_dest_node',
FtnMsgOrigNet : 'ftn_msg_orig_net', FtnMsgOrigNet : 'ftn_msg_orig_net',
FtnMsgDestNet : 'ftn_msg_dest_net', FtnMsgDestNet : 'ftn_msg_dest_net',
FtnAttribute : 'ftn_attribute', FtnAttribute : 'ftn_attribute',
FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001 FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001
FtnOrigin : 'ftn_origin', // http://ftsc.org/docs/fts-0004.001 FtnOrigin : 'ftn_origin', // http://ftsc.org/docs/fts-0004.001
FtnArea : 'ftn_area', // http://ftsc.org/docs/fts-0004.001 FtnArea : 'ftn_area', // http://ftsc.org/docs/fts-0004.001
FtnSeenBy : 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 FtnSeenBy : 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001
}; };
// :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)! // :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)!
const MESSAGE_ROW_MAP = { const MESSAGE_ROW_MAP = {
reply_to_message_id : 'replyToMsgId', reply_to_message_id : 'replyToMsgId',
modified_timestamp : 'modTimestamp' modified_timestamp : 'modTimestamp'
}; };
module.exports = class Message { module.exports = class Message {
@ -102,28 +102,28 @@ module.exports = class Message {
} = { } } = { }
) )
{ {
this.messageId = messageId; this.messageId = messageId;
this.areaTag = areaTag; this.areaTag = areaTag;
this.uuid = uuid; this.uuid = uuid;
this.replyToMsgId = replyToMsgId; this.replyToMsgId = replyToMsgId;
this.toUserName = toUserName; this.toUserName = toUserName;
this.fromUserName = fromUserName; this.fromUserName = fromUserName;
this.subject = subject; this.subject = subject;
this.message = message; this.message = message;
if(_.isDate(modTimestamp) || _.isString(modTimestamp)) { if(_.isDate(modTimestamp) || _.isString(modTimestamp)) {
modTimestamp = moment(modTimestamp); modTimestamp = moment(modTimestamp);
} }
this.modTimestamp = modTimestamp; this.modTimestamp = modTimestamp;
this.meta = {}; this.meta = {};
_.defaultsDeep(this.meta, { System : {} }, meta); _.defaultsDeep(this.meta, { System : {} }, meta);
this.hashTags = hashTags; this.hashTags = hashTags;
} }
isValid() { return true; } // :TODO: obviously useless; look into this or remove it isValid() { return true; } // :TODO: obviously useless; look into this or remove it
static isPrivateAreaTag(areaTag) { static isPrivateAreaTag(areaTag) {
return areaTag.toLowerCase() === Message.WellKnownAreaTags.Private; return areaTag.toLowerCase() === Message.WellKnownAreaTags.Private;
@ -187,10 +187,10 @@ module.exports = class Message {
modTimestamp = moment(modTimestamp); modTimestamp = moment(modTimestamp);
} }
areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437'); areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437');
modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437'); modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437');
subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); subject = iconvEncode(subject.toUpperCase().trim(), 'CP437');
body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437');
return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] )));
} }
@ -198,7 +198,7 @@ module.exports = class Message {
static getMessageFromRow(row) { static getMessageFromRow(row) {
const msg = {}; const msg = {};
_.each(row, (v, k) => { _.each(row, (v, k) => {
// :TODO: see notes around MESSAGE_ROW_MAP -- clean this up so we can just _camelCase()! // :TODO: see notes around MESSAGE_ROW_MAP -- clean this up so we can just _camelCase()!
k = MESSAGE_ROW_MAP[k] || _.camelCase(k); k = MESSAGE_ROW_MAP[k] || _.camelCase(k);
msg[k] = v; msg[k] = v;
}); });
@ -206,38 +206,38 @@ module.exports = class Message {
} }
/* /*
Find message IDs or UUIDs by filter. Available filters/options: Find message IDs or UUIDs by filter. Available filters/options:
filter.uuids - use with resultType='id' filter.uuids - use with resultType='id'
filter.ids - use with resultType='uuid' filter.ids - use with resultType='uuid'
filter.toUserName filter.toUserName
filter.fromUserName filter.fromUserName
filter.replyToMesageId filter.replyToMesageId
filter.newerThanTimestamp filter.newerThanTimestamp
filter.newerThanMessageId filter.newerThanMessageId
filter.areaTag - note if you want by conf, send in all areas for a conf filter.areaTag - note if you want by conf, send in all areas for a conf
*filter.metaTuples - {category, name, value} *filter.metaTuples - {category, name, value}
filter.terms - FTS search filter.terms - FTS search
filter.sort = modTimestamp | messageId filter.sort = modTimestamp | messageId
filter.order = ascending | (descending) filter.order = ascending | (descending)
filter.limit filter.limit
filter.resultType = (id) | uuid | count filter.resultType = (id) | uuid | count
filter.extraFields = [] filter.extraFields = []
filter.privateTagUserId = <userId> - if set, only private messages belonging to <userId> are processed filter.privateTagUserId = <userId> - if set, only private messages belonging to <userId> are processed
- any other areaTag or confTag filters will be ignored - any other areaTag or confTag filters will be ignored
- if NOT present, private areas are skipped - if NOT present, private areas are skipped
*=NYI *=NYI
*/ */
static findMessages(filter, cb) { static findMessages(filter, cb) {
filter = filter || {}; filter = filter || {};
filter.resultType = filter.resultType || 'id'; filter.resultType = filter.resultType || 'id';
filter.extraFields = filter.extraFields || []; filter.extraFields = filter.extraFields || [];
if('messageList' === filter.resultType) { if('messageList' === filter.resultType) {
filter.extraFields = _.uniq(filter.extraFields.concat( filter.extraFields = _.uniq(filter.extraFields.concat(
@ -254,13 +254,13 @@ module.exports = class Message {
let sql; let sql;
if('count' === filter.resultType) { if('count' === filter.resultType) {
sql = sql =
`SELECT COUNT() AS count `SELECT COUNT() AS count
FROM message m`; FROM message m`;
} else { } else {
sql = sql =
`SELECT DISTINCT m.${field}${filter.extraFields.length > 0 ? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ') : ''} `SELECT DISTINCT m.${field}${filter.extraFields.length > 0 ? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ') : ''}
FROM message m`; FROM message m`;
} }
const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC';
@ -276,7 +276,7 @@ module.exports = class Message {
sqlWhere += clause; sqlWhere += clause;
} }
// currently only avail sort // currently only avail sort
if('modTimestamp' === filter.sort) { if('modTimestamp' === filter.sort) {
sqlOrderBy = `ORDER BY m.modified_timestamp ${sqlOrderDir}`; sqlOrderBy = `ORDER BY m.modified_timestamp ${sqlOrderDir}`;
} else { } else {
@ -297,10 +297,10 @@ module.exports = class Message {
appendWhereClause(`m.area_tag = "${Message.WellKnownAreaTags.Private}"`); appendWhereClause(`m.area_tag = "${Message.WellKnownAreaTags.Private}"`);
appendWhereClause( appendWhereClause(
`m.message_id IN ( `m.message_id IN (
SELECT message_id SELECT message_id
FROM message_meta FROM message_meta
WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId} WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId}
)`); )`);
} else { } else {
if(filter.areaTag && filter.areaTag.length > 0) { if(filter.areaTag && filter.areaTag.length > 0) {
if(Array.isArray(filter.areaTag)) { if(Array.isArray(filter.areaTag)) {
@ -315,7 +315,7 @@ module.exports = class Message {
} }
} }
// explicit exclude of Private // explicit exclude of Private
appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`); appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`);
} }
@ -338,13 +338,13 @@ module.exports = class Message {
} }
if(filter.terms && filter.terms.length > 0) { if(filter.terms && filter.terms.length > 0) {
// note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex
appendWhereClause( appendWhereClause(
`m.message_id IN ( `m.message_id IN (
SELECT rowid SELECT rowid
FROM message_fts FROM message_fts
WHERE message_fts MATCH ":${sanatizeString(filter.terms)}" WHERE message_fts MATCH ":${sanatizeString(filter.terms)}"
)` )`
); );
} }
@ -376,13 +376,13 @@ module.exports = class Message {
} }
} }
// :TODO: use findMessages, by uuid, limit=1 // :TODO: use findMessages, by uuid, limit=1
static getMessageIdByUuid(uuid, cb) { static getMessageIdByUuid(uuid, cb) {
msgDb.get( msgDb.get(
`SELECT message_id `SELECT message_id
FROM message FROM message
WHERE message_uuid = ? WHERE message_uuid = ?
LIMIT 1;`, LIMIT 1;`,
[ uuid ], [ uuid ],
(err, row) => { (err, row) => {
if(err) { if(err) {
@ -398,27 +398,27 @@ module.exports = class Message {
); );
} }
// :TODO: use findMessages // :TODO: use findMessages
static getMessageIdsByMetaValue(category, name, value, cb) { static getMessageIdsByMetaValue(category, name, value, cb) {
msgDb.all( msgDb.all(
`SELECT message_id `SELECT message_id
FROM message_meta FROM message_meta
WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`, WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`,
[ category, name, value ], [ category, name, value ],
(err, rows) => { (err, rows) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
return cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s) return cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s)
} }
); );
} }
static getMetaValuesByMessageId(messageId, category, name, cb) { static getMetaValuesByMessageId(messageId, category, name, cb) {
const sql = const sql =
`SELECT meta_value `SELECT meta_value
FROM message_meta FROM message_meta
WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`; WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`;
msgDb.all(sql, [ messageId, category, name ], (err, rows) => { msgDb.all(sql, [ messageId, category, name ], (err, rows) => {
if(err) { if(err) {
@ -429,12 +429,12 @@ module.exports = class Message {
return cb(Errors.DoesNotExist('No value for category/name')); return cb(Errors.DoesNotExist('No value for category/name'));
} }
// single values are returned without an array // single values are returned without an array
if(1 === rows.length) { if(1 === rows.length) {
return cb(null, rows[0].meta_value); return cb(null, rows[0].meta_value);
} }
return cb(null, rows.map(r => r.meta_value)); // map to array of values only return cb(null, rows.map(r => r.meta_value)); // map to array of values only
}); });
} }
@ -460,23 +460,23 @@ module.exports = class Message {
loadMeta(cb) { loadMeta(cb) {
/* /*
Example of loaded this.meta: Example of loaded this.meta:
meta: { meta: {
System: { System: {
local_to_user_id: 1234, local_to_user_id: 1234,
}, },
FtnProperty: { FtnProperty: {
ftn_seen_by: [ "1/102 103", "2/42 52 65" ] ftn_seen_by: [ "1/102 103", "2/42 52 65" ]
} }
} }
*/ */
const sql = const sql =
`SELECT meta_category, meta_name, meta_value `SELECT meta_category, meta_name, meta_value
FROM message_meta FROM message_meta
WHERE message_id = ?;`; WHERE message_id = ?;`;
const self = this; // :TODO: not required - arrow functions below: const self = this; // :TODO: not required - arrow functions below:
msgDb.each(sql, [ this.messageId ], (err, row) => { msgDb.each(sql, [ this.messageId ], (err, row) => {
if(!(row.meta_category in self.meta)) { if(!(row.meta_category in self.meta)) {
self.meta[row.meta_category] = { }; self.meta[row.meta_category] = { };
@ -497,7 +497,7 @@ module.exports = class Message {
}); });
} }
// :TODO: this should only take a UUID... // :TODO: this should only take a UUID...
load(options, cb) { load(options, cb) {
assert(_.isString(options.uuid)); assert(_.isString(options.uuid));
@ -508,10 +508,10 @@ module.exports = class Message {
function loadMessage(callback) { function loadMessage(callback) {
msgDb.get( msgDb.get(
`SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, `SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject,
message, modified_timestamp, view_count message, modified_timestamp, view_count
FROM message FROM message
WHERE message_uuid=? WHERE message_uuid=?
LIMIT 1;`, LIMIT 1;`,
[ options.uuid ], [ options.uuid ],
(err, msgRow) => { (err, msgRow) => {
if(err) { if(err) {
@ -522,15 +522,15 @@ module.exports = class Message {
return callback(Errors.DoesNotExist('Message (no longer) available')); return callback(Errors.DoesNotExist('Message (no longer) available'));
} }
self.messageId = msgRow.message_id; self.messageId = msgRow.message_id;
self.areaTag = msgRow.area_tag; self.areaTag = msgRow.area_tag;
self.messageUuid = msgRow.message_uuid; self.messageUuid = msgRow.message_uuid;
self.replyToMsgId = msgRow.reply_to_message_id; self.replyToMsgId = msgRow.reply_to_message_id;
self.toUserName = msgRow.to_user_name; self.toUserName = msgRow.to_user_name;
self.fromUserName = msgRow.from_user_name; self.fromUserName = msgRow.from_user_name;
self.subject = msgRow.subject; self.subject = msgRow.subject;
self.message = msgRow.message; self.message = msgRow.message;
self.modTimestamp = moment(msgRow.modified_timestamp); self.modTimestamp = moment(msgRow.modified_timestamp);
return callback(err); return callback(err);
} }
@ -542,7 +542,7 @@ module.exports = class Message {
}); });
}, },
function loadHashTags(callback) { function loadHashTags(callback) {
// :TODO: // :TODO:
return callback(null); return callback(null);
} }
], ],
@ -560,7 +560,7 @@ module.exports = class Message {
const metaStmt = transOrDb.prepare( const metaStmt = transOrDb.prepare(
`INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value)
VALUES (?, ?, ?, ?);`); VALUES (?, ?, ?, ?);`);
if(!_.isArray(value)) { if(!_.isArray(value)) {
value = [ value ]; value = [ value ];
@ -590,7 +590,7 @@ module.exports = class Message {
return msgDb.beginTransaction(callback); return msgDb.beginTransaction(callback);
}, },
function storeMessage(trans, callback) { function storeMessage(trans, callback) {
// generate a UUID for this message if required (general case) // generate a UUID for this message if required (general case)
const msgTimestamp = moment(); const msgTimestamp = moment();
if(!self.uuid) { if(!self.uuid) {
self.uuid = Message.createMessageUUID( self.uuid = Message.createMessageUUID(
@ -603,9 +603,9 @@ module.exports = class Message {
trans.run( trans.run(
`INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, VALUES (?, ?, ?, ?, ?, ?, ?, ?);`,
[ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ], [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ],
function inserted(err) { // use non-arrow function for 'this' scope function inserted(err) { // use non-arrow function for 'this' scope
if(!err) { if(!err) {
self.messageId = this.lastID; self.messageId = this.lastID;
} }
@ -619,17 +619,17 @@ module.exports = class Message {
return callback(null, trans); return callback(null, trans);
} }
/* /*
Example of self.meta: Example of self.meta:
meta: { meta: {
System: { System: {
local_to_user_id: 1234, local_to_user_id: 1234,
}, },
FtnProperty: { FtnProperty: {
ftn_seen_by: [ "1/102 103", "2/42 52 65" ] ftn_seen_by: [ "1/102 103", "2/42 52 65" ]
} }
} }
*/ */
async.each(Object.keys(self.meta), (category, nextCat) => { async.each(Object.keys(self.meta), (category, nextCat) => {
async.each(Object.keys(self.meta[category]), (name, nextName) => { async.each(Object.keys(self.meta[category]), (name, nextName) => {
self.persistMetaValue(category, name, self.meta[category][name], trans, err => { self.persistMetaValue(category, name, self.meta[category][name], trans, err => {
@ -644,7 +644,7 @@ module.exports = class Message {
}); });
}, },
function storeHashTags(trans, callback) { function storeHashTags(trans, callback) {
// :TODO: hash tag support // :TODO: hash tag support
return callback(null, trans); return callback(null, trans);
} }
], ],
@ -660,7 +660,7 @@ module.exports = class Message {
); );
} }
// :TODO: FTN stuff doesn't have any business here // :TODO: FTN stuff doesn't have any business here
getFTNQuotePrefix(source) { getFTNQuotePrefix(source) {
source = source || 'fromUserName'; source = source || 'fromUserName';
@ -677,32 +677,32 @@ module.exports = class Message {
return cb(Errors.MissingParam()); return cb(Errors.MissingParam());
} }
options.startCol = options.startCol || 1; options.startCol = options.startCol || 1;
options.includePrefix = _.get(options, 'includePrefix', true); options.includePrefix = _.get(options, 'includePrefix', true);
options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true);
options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } ); options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } );
options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting
/* /*
Some long text that needs to be wrapped and quoted should look right after Some long text that needs to be wrapped and quoted should look right after
doing so, don't ya think? yeah I think so doing so, don't ya think? yeah I think so
Nu> Some long text that needs to be wrapped and quoted should look right Nu> Some long text that needs to be wrapped and quoted should look right
Nu> after doing so, don't ya think? yeah I think so Nu> after doing so, don't ya think? yeah I think so
Ot> Nu> Some long text that needs to be wrapped and quoted should look Ot> Nu> Some long text that needs to be wrapped and quoted should look
Ot> Nu> right after doing so, don't ya think? yeah I think so Ot> Nu> right after doing so, don't ya think? yeah I think so
*/ */
const quotePrefix = options.includePrefix ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') : ''; const quotePrefix = options.includePrefix ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') : '';
function getWrapped(text, extraPrefix) { function getWrapped(text, extraPrefix) {
extraPrefix = extraPrefix ? ` ${extraPrefix}` : ''; extraPrefix = extraPrefix ? ` ${extraPrefix}` : '';
const wrapOpts = { const wrapOpts = {
width : options.cols - (quotePrefix.length + extraPrefix.length), width : options.cols - (quotePrefix.length + extraPrefix.length),
tabHandling : 'expand', tabHandling : 'expand',
tabWidth : 4, tabWidth : 4,
}; };
return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => { return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => {
@ -711,7 +711,7 @@ module.exports = class Message {
} }
function getFormattedLine(line) { function getFormattedLine(line) {
// for pre-formatted text, we just append a line truncated to fit // for pre-formatted text, we just append a line truncated to fit
let newLen; let newLen;
const total = line.length + quotePrefix.length; const total = line.length + quotePrefix.length;
@ -726,14 +726,14 @@ module.exports = class Message {
if(options.isAnsi) { if(options.isAnsi) {
ansiPrep( ansiPrep(
this.message.replace(/\r?\n/g, '\r\n'), // normalized LF -> CRLF this.message.replace(/\r?\n/g, '\r\n'), // normalized LF -> CRLF
{ {
termWidth : options.termWidth, termWidth : options.termWidth,
termHeight : options.termHeight, termHeight : options.termHeight,
cols : options.cols, cols : options.cols,
rows : 'auto', rows : 'auto',
startCol : options.startCol, startCol : options.startCol,
forceLineTerm : true, forceLineTerm : true,
}, },
(err, prepped) => { (err, prepped) => {
prepped = prepped || this.message; prepped = prepped || this.message;
@ -741,20 +741,20 @@ module.exports = class Message {
let lastSgr = ''; let lastSgr = '';
const split = splitTextAtTerms(prepped); const split = splitTextAtTerms(prepped);
const quoteLines = []; const quoteLines = [];
const focusQuoteLines = []; const focusQuoteLines = [];
// //
// Do not include quote prefixes (e.g. XX> ) on ANSI replies (and therefor quote builder) // Do not include quote prefixes (e.g. XX> ) on ANSI replies (and therefor quote builder)
// as while this works in ENiGMA, other boards such as Mystic, WWIV, etc. will try to // as while this works in ENiGMA, other boards such as Mystic, WWIV, etc. will try to
// strip colors, colorize the lines, etc. If we exclude the prefixes, this seems to do // strip colors, colorize the lines, etc. If we exclude the prefixes, this seems to do
// the trick and allow them to leave them alone! // the trick and allow them to leave them alone!
// //
split.forEach(l => { split.forEach(l => {
quoteLines.push(`${lastSgr}${l}`); quoteLines.push(`${lastSgr}${l}`);
focusQuoteLines.push(`${options.ansiFocusPrefixSgr}>${lastSgr}${renderSubstr(l, 1, l.length - 1)}`); focusQuoteLines.push(`${options.ansiFocusPrefixSgr}>${lastSgr}${renderSubstr(l, 1, l.length - 1)}`);
lastSgr = (l.match(/(?:\x1b\x5b)[?=;0-9]*m(?!.*(?:\x1b\x5b)[?=;0-9]*m)/) || [])[0] || ''; // eslint-disable-line no-control-regex lastSgr = (l.match(/(?:\x1b\x5b)[?=;0-9]*m(?!.*(?:\x1b\x5b)[?=;0-9]*m)/) || [])[0] || ''; // eslint-disable-line no-control-regex
}); });
quoteLines[quoteLines.length - 1] += options.ansiResetSgr; quoteLines[quoteLines.length - 1] += options.ansiResetSgr;
@ -763,23 +763,23 @@ module.exports = class Message {
} }
); );
} else { } else {
const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */; const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */;
const quoted = []; const quoted = [];
const input = _.trimEnd(this.message).replace(/\b/g, ''); const input = _.trimEnd(this.message).replace(/\b/g, '');
// find *last* tearline // find *last* tearline
let tearLinePos = this.getTearLinePosition(input); let tearLinePos = this.getTearLinePosition(input);
tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string
input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => { input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => {
// //
// For each paragraph, a state machine: // For each paragraph, a state machine:
// - New line - line // - New line - line
// - New (pre)quoted line - quote_line // - New (pre)quoted line - quote_line
// - Continuation of new/quoted line // - Continuation of new/quoted line
// //
// Also: // Also:
// - Detect pre-formatted lines & try to keep them as-is // - Detect pre-formatted lines & try to keep them as-is
// //
let state; let state;
let buf = ''; let buf = '';
@ -787,18 +787,18 @@ module.exports = class Message {
if(quoted.length > 0) { if(quoted.length > 0) {
// //
// Preserve paragraph seperation. // Preserve paragraph seperation.
// //
// FSC-0032 states something about leaving blank lines fully blank // FSC-0032 states something about leaving blank lines fully blank
// (without a prefix) but it seems nicer (and more consistent with other systems) // (without a prefix) but it seems nicer (and more consistent with other systems)
// to put 'em in. // to put 'em in.
// //
quoted.push(quotePrefix); quoted.push(quotePrefix);
} }
paragraph.split(/\r?\n/).forEach(line => { paragraph.split(/\r?\n/).forEach(line => {
if(0 === line.trim().length) { if(0 === line.trim().length) {
// see blank line notes above // see blank line notes above
return quoted.push(quotePrefix); return quoted.push(quotePrefix);
} }
@ -839,8 +839,8 @@ module.exports = class Message {
if(isFormattedLine(line)) { if(isFormattedLine(line)) {
quoted.push(getFormattedLine(line)); quoted.push(getFormattedLine(line));
} else { } else {
state = quoteMatch ? 'quote_line' : 'line'; state = quoteMatch ? 'quote_line' : 'line';
buf = 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any buf = 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any
} }
break; break;
} }

View File

@ -1,45 +1,45 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const msgDb = require('./database.js').dbs.message; const msgDb = require('./database.js').dbs.message;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Message = require('./message.js'); const Message = require('./message.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const msgNetRecord = require('./msg_network.js').recordMessage; const msgNetRecord = require('./msg_network.js').recordMessage;
const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
exports.getAvailableMessageConferences = getAvailableMessageConferences; exports.getAvailableMessageConferences = getAvailableMessageConferences;
exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences; exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences;
exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag; exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag;
exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag; exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag;
exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag; exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag;
exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag; exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag;
exports.getMessageConferenceByTag = getMessageConferenceByTag; exports.getMessageConferenceByTag = getMessageConferenceByTag;
exports.getMessageAreaByTag = getMessageAreaByTag; exports.getMessageAreaByTag = getMessageAreaByTag;
exports.changeMessageConference = changeMessageConference; exports.changeMessageConference = changeMessageConference;
exports.changeMessageArea = changeMessageArea; exports.changeMessageArea = changeMessageArea;
exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea; exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea;
exports.getMessageListForArea = getMessageListForArea; exports.getMessageListForArea = getMessageListForArea;
exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser; exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser;
exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser; exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser;
exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea; exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea;
exports.getMessageAreaLastReadId = getMessageAreaLastReadId; exports.getMessageAreaLastReadId = getMessageAreaLastReadId;
exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId; exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId;
exports.persistMessage = persistMessage; exports.persistMessage = persistMessage;
exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent; exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent;
function getAvailableMessageConferences(client, options) { function getAvailableMessageConferences(client, options) {
options = options || { includeSystemInternal : false }; options = options || { includeSystemInternal : false };
assert(client || true === options.noClient); assert(client || true === options.noClient);
// perform ACS check per conf & omit system_internal if desired // perform ACS check per conf & omit system_internal if desired
return _.omitBy(Config().messageConferences, (conf, confTag) => { return _.omitBy(Config().messageConferences, (conf, confTag) => {
if(!options.includeSystemInternal && 'system_internal' === confTag) { if(!options.includeSystemInternal && 'system_internal' === confTag) {
return true; return true;
@ -53,7 +53,7 @@ function getSortedAvailMessageConferences(client, options) {
const confs = _.map(getAvailableMessageConferences(client, options), (v, k) => { const confs = _.map(getAvailableMessageConferences(client, options), (v, k) => {
return { return {
confTag : k, confTag : k,
conf : v, conf : v,
}; };
}); });
@ -73,10 +73,10 @@ function getAvailableMessageAreasByConfTag(confTag, options) {
const areas = config.messageConferences[confTag].areas; const areas = config.messageConferences[confTag].areas;
if(!options.client || true === options.noAcsCheck) { if(!options.client || true === options.noAcsCheck) {
// everything - no ACS checks // everything - no ACS checks
return areas; return areas;
} else { } else {
// perform ACS check per area // perform ACS check per area
return _.omitBy(areas, area => { return _.omitBy(areas, area => {
return !options.client.acs.hasMessageAreaRead(area); return !options.client.acs.hasMessageAreaRead(area);
}); });
@ -87,8 +87,8 @@ function getAvailableMessageAreasByConfTag(confTag, options) {
function getSortedAvailMessageAreasByConfTag(confTag, options) { function getSortedAvailMessageAreasByConfTag(confTag, options) {
const areas = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => { const areas = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => {
return { return {
areaTag : k, areaTag : k,
area : v, area : v,
}; };
}); });
@ -99,16 +99,16 @@ function getSortedAvailMessageAreasByConfTag(confTag, options) {
function getDefaultMessageConferenceTag(client, disableAcsCheck) { function getDefaultMessageConferenceTag(client, disableAcsCheck) {
// //
// Find the first conference marked 'default'. If found, // Find the first conference marked 'default'. If found,
// inspect |client| against *read* ACS using defaults if not // inspect |client| against *read* ACS using defaults if not
// specified. // specified.
// //
// If the above fails, just go down the list until we get one // If the above fails, just go down the list until we get one
// that passes. // that passes.
// //
// It's possible that we end up with nothing here! // It's possible that we end up with nothing here!
// //
// Note that built in 'system_internal' is always ommited here // Note that built in 'system_internal' is always ommited here
// //
const config = Config(); const config = Config();
let defaultConf = _.findKey(config.messageConferences, o => o.default); let defaultConf = _.findKey(config.messageConferences, o => o.default);
@ -170,7 +170,7 @@ function getMessageConfTagByAreaTag(areaTag) {
function getMessageAreaByTag(areaTag, optionalConfTag) { function getMessageAreaByTag(areaTag, optionalConfTag) {
const confs = Config().messageConferences; const confs = Config().messageConferences;
// :TODO: this could be cached // :TODO: this could be cached
if(_.isString(optionalConfTag)) { if(_.isString(optionalConfTag)) {
if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) { if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) {
return confs[optionalConfTag].areas[areaTag]; return confs[optionalConfTag].areas[areaTag];
@ -204,8 +204,8 @@ function changeMessageConference(client, confTag, cb) {
} }
}, },
function getDefaultAreaInConf(conf, callback) { function getDefaultAreaInConf(conf, callback) {
const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag); const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
const area = getMessageAreaByTag(areaTag, confTag); const area = getMessageAreaByTag(areaTag, confTag);
if(area) { if(area) {
callback(null, conf, { areaTag : areaTag, area : area } ); callback(null, conf, { areaTag : areaTag, area : area } );
@ -222,8 +222,8 @@ function changeMessageConference(client, confTag, cb) {
}, },
function changeConferenceAndArea(conf, areaInfo, callback) { function changeConferenceAndArea(conf, areaInfo, callback) {
const newProps = { const newProps = {
message_conf_tag : confTag, message_conf_tag : confTag,
message_area_tag : areaInfo.areaTag, message_area_tag : areaInfo.areaTag,
}; };
client.user.persistProperties(newProps, err => { client.user.persistProperties(newProps, err => {
callback(err, conf, areaInfo); callback(err, conf, areaInfo);
@ -242,7 +242,7 @@ function changeMessageConference(client, confTag, cb) {
} }
function changeMessageAreaWithOptions(client, areaTag, options, cb) { function changeMessageAreaWithOptions(client, areaTag, options, cb) {
options = options || {}; // :TODO: this is currently pointless... cb is required... options = options || {}; // :TODO: this is currently pointless... cb is required...
async.waterfall( async.waterfall(
[ [
@ -284,14 +284,14 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) {
} }
// //
// Temporairly -- e.g. non-persisted -- change to an area and it's // Temporairly -- e.g. non-persisted -- change to an area and it's
// associated underlying conference. ACS is checked for both. // associated underlying conference. ACS is checked for both.
// //
// This is useful for example when doing a new scan // This is useful for example when doing a new scan
// //
function tempChangeMessageConfAndArea(client, areaTag) { function tempChangeMessageConfAndArea(client, areaTag) {
const area = getMessageAreaByTag(areaTag); const area = getMessageAreaByTag(areaTag);
const confTag = getMessageConfTagByAreaTag(areaTag); const confTag = getMessageConfTagByAreaTag(areaTag);
if(!area || !confTag) { if(!area || !confTag) {
return false; return false;
@ -303,7 +303,7 @@ function tempChangeMessageConfAndArea(client, areaTag) {
return false; return false;
} }
client.user.properties.message_conf_tag = confTag; client.user.properties.message_conf_tag = confTag;
client.user.properties.message_area_tag = areaTag; client.user.properties.message_area_tag = areaTag;
return true; return true;
@ -319,8 +319,8 @@ function getNewMessageCountInAreaForUser(userId, areaTag, cb) {
const filter = { const filter = {
areaTag, areaTag,
newerThanMessageId : lastMessageId, newerThanMessageId : lastMessageId,
resultType : 'count', resultType : 'count',
}; };
if(Message.isPrivateAreaTag(areaTag)) { if(Message.isPrivateAreaTag(areaTag)) {
@ -339,10 +339,10 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) {
const filter = { const filter = {
areaTag, areaTag,
resultType : 'messageList', resultType : 'messageList',
newerThanMessageId : lastMessageId, newerThanMessageId : lastMessageId,
sort : 'messageId', sort : 'messageId',
order : 'ascending', order : 'ascending',
}; };
if(Message.isPrivateAreaTag(areaTag)) { if(Message.isPrivateAreaTag(areaTag)) {
@ -356,9 +356,9 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) {
function getMessageListForArea(client, areaTag, cb) { function getMessageListForArea(client, areaTag, cb) {
const filter = { const filter = {
areaTag, areaTag,
resultType : 'messageList', resultType : 'messageList',
sort : 'messageId', sort : 'messageId',
order : 'ascending', order : 'ascending',
}; };
if(Message.isPrivateAreaTag(areaTag)) { if(Message.isPrivateAreaTag(areaTag)) {
@ -373,9 +373,9 @@ function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) {
{ {
areaTag, areaTag,
newerThanTimestamp, newerThanTimestamp,
sort : 'modTimestamp', sort : 'modTimestamp',
order : 'ascending', order : 'ascending',
limit : 1, limit : 1,
}, },
(err, id) => { (err, id) => {
if(err) { if(err) {
@ -388,9 +388,9 @@ function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) {
function getMessageAreaLastReadId(userId, areaTag, cb) { function getMessageAreaLastReadId(userId, areaTag, cb) {
msgDb.get( msgDb.get(
'SELECT message_id ' + 'SELECT message_id ' +
'FROM user_message_area_last_read ' + 'FROM user_message_area_last_read ' +
'WHERE user_id = ? AND area_tag = ?;', 'WHERE user_id = ? AND area_tag = ?;',
[ userId, areaTag.toLowerCase() ], [ userId, areaTag.toLowerCase() ],
function complete(err, row) { function complete(err, row) {
cb(err, row ? row.message_id : 0); cb(err, row ? row.message_id : 0);
@ -404,20 +404,20 @@ function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb)
allowOlder = false; allowOlder = false;
} }
// :TODO: likely a better way to do this... // :TODO: likely a better way to do this...
async.waterfall( async.waterfall(
[ [
function getCurrent(callback) { function getCurrent(callback) {
getMessageAreaLastReadId(userId, areaTag, function result(err, lastId) { getMessageAreaLastReadId(userId, areaTag, function result(err, lastId) {
lastId = lastId || 0; lastId = lastId || 0;
callback(null, lastId); // ignore errors as we default to 0 callback(null, lastId); // ignore errors as we default to 0
}); });
}, },
function update(lastId, callback) { function update(lastId, callback) {
if(allowOlder || messageId > lastId) { if(allowOlder || messageId > lastId) {
msgDb.run( msgDb.run(
'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' + 'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' +
'VALUES (?, ?, ?);', 'VALUES (?, ?, ?);',
[ userId, areaTag, messageId ], [ userId, areaTag, messageId ],
function written(err) { function written(err) {
callback(err, true); // true=didUpdate callback(err, true); // true=didUpdate
@ -459,7 +459,7 @@ function persistMessage(message, cb) {
); );
} }
// method exposed for event scheduler // method exposed for event scheduler
function trimMessageAreasScheduledEvent(args, cb) { function trimMessageAreasScheduledEvent(args, cb) {
function trimMessageAreaByMaxMessages(areaInfo, cb) { function trimMessageAreaByMaxMessages(areaInfo, cb) {
@ -469,15 +469,15 @@ function trimMessageAreasScheduledEvent(args, cb) {
msgDb.run( msgDb.run(
`DELETE FROM message `DELETE FROM message
WHERE message_id IN( WHERE message_id IN(
SELECT message_id SELECT message_id
FROM message FROM message
WHERE area_tag = ? WHERE area_tag = ?
ORDER BY message_id DESC ORDER BY message_id DESC
LIMIT -1 OFFSET ${areaInfo.maxMessages} LIMIT -1 OFFSET ${areaInfo.maxMessages}
);`, );`,
[ areaInfo.areaTag.toLowerCase() ], [ areaInfo.areaTag.toLowerCase() ],
function result(err) { // no arrow func; need this function result(err) { // no arrow func; need this
if(err) { if(err) {
Log.error( { areaInfo : areaInfo, error : err.message, type : 'maxMessages' }, 'Error trimming message area'); Log.error( { areaInfo : areaInfo, error : err.message, type : 'maxMessages' }, 'Error trimming message area');
} else { } else {
@ -495,9 +495,9 @@ function trimMessageAreasScheduledEvent(args, cb) {
msgDb.run( msgDb.run(
`DELETE FROM message `DELETE FROM message
WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`, WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`,
[ areaInfo.areaTag ], [ areaInfo.areaTag ],
function result(err) { // no arrow func; need this function result(err) { // no arrow func; need this
if(err) { if(err) {
Log.warn( { areaInfo : areaInfo, error : err.message, type : 'maxAgeDays' }, 'Error trimming message area'); Log.warn( { areaInfo : areaInfo, error : err.message, type : 'maxAgeDays' }, 'Error trimming message area');
} else { } else {
@ -514,17 +514,17 @@ function trimMessageAreasScheduledEvent(args, cb) {
const areaTags = []; const areaTags = [];
// //
// We use SQL here vs API such that no-longer-used tags are picked up // We use SQL here vs API such that no-longer-used tags are picked up
// //
msgDb.each( msgDb.each(
`SELECT DISTINCT area_tag `SELECT DISTINCT area_tag
FROM message;`, FROM message;`,
(err, row) => { (err, row) => {
if(err) { if(err) {
return callback(err); return callback(err);
} }
// We treat private mail special // We treat private mail special
if(!Message.isPrivateAreaTag(row.area_tag)) { if(!Message.isPrivateAreaTag(row.area_tag)) {
areaTags.push(row.area_tag); areaTags.push(row.area_tag);
} }
@ -537,23 +537,23 @@ function trimMessageAreasScheduledEvent(args, cb) {
function prepareAreaInfo(areaTags, callback) { function prepareAreaInfo(areaTags, callback) {
let areaInfos = []; let areaInfos = [];
// determine maxMessages & maxAgeDays per area // determine maxMessages & maxAgeDays per area
const config = Config(); const config = Config();
areaTags.forEach(areaTag => { areaTags.forEach(areaTag => {
let maxMessages = config.messageAreaDefaults.maxMessages; let maxMessages = config.messageAreaDefaults.maxMessages;
let maxAgeDays = config.messageAreaDefaults.maxAgeDays; let maxAgeDays = config.messageAreaDefaults.maxAgeDays;
const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here
if(area) { if(area) {
maxMessages = area.maxMessages || maxMessages; maxMessages = area.maxMessages || maxMessages;
maxAgeDays = area.maxAgeDays || maxAgeDays; maxAgeDays = area.maxAgeDays || maxAgeDays;
} }
areaInfos.push( { areaInfos.push( {
areaTag : areaTag, areaTag : areaTag,
maxMessages : maxMessages, maxMessages : maxMessages,
maxAgeDays : maxAgeDays, maxAgeDays : maxAgeDays,
} ); } );
}); });
@ -578,13 +578,13 @@ function trimMessageAreasScheduledEvent(args, cb) {
}, },
function trimExternalPrivateSentMail(callback) { function trimExternalPrivateSentMail(callback) {
// //
// *External* (FTN, email, ...) outgoing is cleaned up *after export* // *External* (FTN, email, ...) outgoing is cleaned up *after export*
// if it is older than the configured |maxExternalSentAgeDays| days // if it is older than the configured |maxExternalSentAgeDays| days
// //
// Outgoing externally exported private mail is: // Outgoing externally exported private mail is:
// - In the 'private_mail' area // - In the 'private_mail' area
// - Marked exported (state_flags0 exported bit set) // - Marked exported (state_flags0 exported bit set)
// - Marked with any external flavor (we don't mark local) // - Marked with any external flavor (we don't mark local)
// //
const maxExternalSentAgeDays = _.get( const maxExternalSentAgeDays = _.get(
Config, Config,
@ -594,18 +594,18 @@ function trimMessageAreasScheduledEvent(args, cb) {
msgDb.run( msgDb.run(
`DELETE FROM message `DELETE FROM message
WHERE message_id IN ( WHERE message_id IN (
SELECT m.message_id SELECT m.message_id
FROM message m FROM message m
JOIN message_meta mms JOIN message_meta mms
ON m.message_id = mms.message_id AND ON m.message_id = mms.message_id AND
(mms.meta_category='System' AND mms.meta_name='${Message.SystemMetaNames.StateFlags0}' AND (mms.meta_value & ${Message.StateFlags0.Exported} = ${Message.StateFlags0.Exported})) (mms.meta_category='System' AND mms.meta_name='${Message.SystemMetaNames.StateFlags0}' AND (mms.meta_value & ${Message.StateFlags0.Exported} = ${Message.StateFlags0.Exported}))
JOIN message_meta mmf JOIN message_meta mmf
ON m.message_id = mmf.message_id AND ON m.message_id = mmf.message_id AND
(mmf.meta_category='System' AND mmf.meta_name='${Message.SystemMetaNames.ExternalFlavor}') (mmf.meta_category='System' AND mmf.meta_name='${Message.SystemMetaNames.ExternalFlavor}')
WHERE m.area_tag='${Message.WellKnownAreaTags.Private}' AND DATETIME('now') > DATETIME(m.modified_timestamp, '+${maxExternalSentAgeDays} days') WHERE m.area_tag='${Message.WellKnownAreaTags.Private}' AND DATETIME('now') > DATETIME(m.modified_timestamp, '+${maxExternalSentAgeDays} days')
);`, );`,
function results(err) { // no arrow func; need this function results(err) { // no arrow func; need this
if(err) { if(err) {
Log.warn( { error : err.message }, 'Error trimming private externally sent messages'); Log.warn( { error : err.message }, 'Error trimming private externally sent messages');
} else { } else {

View File

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

View File

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

View File

@ -1,17 +1,17 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const paths = require('path'); const paths = require('path');
const os = require('os'); const os = require('os');
const packageJson = require('../package.json'); const packageJson = require('../package.json');
exports.isProduction = isProduction; exports.isProduction = isProduction;
exports.isDevelopment = isDevelopment; exports.isDevelopment = isDevelopment;
exports.valueWithDefault = valueWithDefault; exports.valueWithDefault = valueWithDefault;
exports.resolvePath = resolvePath; exports.resolvePath = resolvePath;
exports.getCleanEnigmaVersion = getCleanEnigmaVersion; exports.getCleanEnigmaVersion = getCleanEnigmaVersion;
exports.getEnigmaUserAgent = getEnigmaUserAgent; exports.getEnigmaUserAgent = getEnigmaUserAgent;
function isProduction() { function isProduction() {
var env = process.env.NODE_ENV || 'dev'; var env = process.env.NODE_ENV || 'dev';
@ -42,11 +42,11 @@ function getCleanEnigmaVersion() {
; ;
} }
// See also ftn_util.js getTearLine() & getProductIdentifier() // See also ftn_util.js getTearLine() & getProductIdentifier()
function getEnigmaUserAgent() { function getEnigmaUserAgent() {
// can't have 1/2 or ½ in User-Agent according to RFC 1945 :( // can't have 1/2 or ½ in User-Agent according to RFC 1945 :(
const version = getCleanEnigmaVersion(); const version = getCleanEnigmaVersion();
const nodeVer = process.version.substr(1); // remove 'v' prefix const nodeVer = process.version.substr(1); // remove 'v' prefix
return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
} }

View File

@ -1,8 +1,8 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const messageArea = require('../core/message_area.js'); const messageArea = require('../core/message_area.js');
const { get } = require('lodash'); const { get } = require('lodash');
exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { exports.MessageAreaConfTempSwitcher = Sup => class extends Sup {
@ -10,13 +10,13 @@ exports.MessageAreaConfTempSwitcher = Sup => class extends Sup {
tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) { tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) {
messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag); messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag);
if(!messageAreaTag) { if(!messageAreaTag) {
return; // nothing to do! return; // nothing to do!
} }
if(recordPrevious) { if(recordPrevious) {
this.prevMessageConfAndArea = { this.prevMessageConfAndArea = {
confTag : this.client.user.properties.message_conf_tag, confTag : this.client.user.properties.message_conf_tag,
areaTag : this.client.user.properties.message_area_tag, areaTag : this.client.user.properties.message_area_tag,
}; };
} }

View File

@ -1,21 +1,21 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
// deps // deps
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
const async = require('async'); const async = require('async');
// exports // exports
exports.loadModuleEx = loadModuleEx; exports.loadModuleEx = loadModuleEx;
exports.loadModule = loadModule; exports.loadModule = loadModule;
exports.loadModulesForCategory = loadModulesForCategory; exports.loadModulesForCategory = loadModulesForCategory;
exports.getModulePaths = getModulePaths; exports.getModulePaths = getModulePaths;
function loadModuleEx(options, cb) { function loadModuleEx(options, cb) {
assert(_.isObject(options)); assert(_.isObject(options));
@ -25,18 +25,18 @@ function loadModuleEx(options, cb) {
const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null; const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null;
if(_.isObject(modConfig) && false === modConfig.enabled) { if(_.isObject(modConfig) && false === modConfig.enabled) {
const err = new Error(`Module "${options.name}" is disabled`); const err = new Error(`Module "${options.name}" is disabled`);
err.code = 'EENIGMODDISABLED'; err.code = 'EENIGMODDISABLED';
return cb(err); return cb(err);
} }
// //
// Modules are allowed to live in /path/to/<moduleName>/<moduleName>.js or // Modules are allowed to live in /path/to/<moduleName>/<moduleName>.js or
// simply in /path/to/<moduleName>.js. This allows for more advanced modules // simply in /path/to/<moduleName>.js. This allows for more advanced modules
// to have their own containing folder, package.json & dependencies, etc. // to have their own containing folder, package.json & dependencies, etc.
// //
let mod; let mod;
let modPath = paths.join(options.path, `${options.name}.js`); // general case first let modPath = paths.join(options.path, `${options.name}.js`); // general case first
try { try {
mod = require(modPath); mod = require(modPath);
} catch(e) { } catch(e) {

View File

@ -1,30 +1,30 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
const displayThemeArt = require('./theme.js').displayThemeArt; const displayThemeArt = require('./theme.js').displayThemeArt;
const resetScreen = require('./ansi_term.js').resetScreen; const resetScreen = require('./ansi_term.js').resetScreen;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Area List', name : 'Message Area List',
desc : 'Module for listing / choosing message areas', desc : 'Module for listing / choosing message areas',
author : 'NuSkooler', author : 'NuSkooler',
}; };
/* /*
:TODO: :TODO:
Obv/2 has the following: Obv/2 has the following:
CHANGE .ANS - Message base changing ansi CHANGE .ANS - Message base changing ansi
|SN Current base name |SN Current base name
|SS Current base sponsor |SS Current base sponsor
|NM Number of messages in current base |NM Number of messages in current base
@ -35,9 +35,9 @@ exports.moduleInfo = {
*/ */
const MciViewIds = { const MciViewIds = {
AreaList : 1, AreaList : 1,
SelAreaInfo1 : 2, SelAreaInfo1 : 2,
SelAreaInfo2 : 3, SelAreaInfo2 : 3,
}; };
exports.getModule = class MessageAreaListModule extends MenuModule { exports.getModule = class MessageAreaListModule extends MenuModule {
@ -53,9 +53,9 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
this.menuMethods = { this.menuMethods = {
changeArea : function(formData, extraArgs, cb) { changeArea : function(formData, extraArgs, cb) {
if(1 === formData.submitId) { if(1 === formData.submitId) {
let area = self.messageAreas[formData.value.area]; let area = self.messageAreas[formData.value.area];
const areaTag = area.areaTag; const areaTag = area.areaTag;
area = area.area; // what we want is actually embedded area = area.area; // what we want is actually embedded
messageArea.changeMessageArea(self.client, areaTag, err => { messageArea.changeMessageArea(self.client, areaTag, err => {
if(err) { if(err) {
@ -65,14 +65,14 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
} else { } else {
if(_.isString(area.art)) { if(_.isString(area.art)) {
const dispOptions = { const dispOptions = {
client : self.client, client : self.client,
name : area.art, name : area.art,
}; };
self.client.term.rawWrite(resetScreen()); self.client.term.rawWrite(resetScreen());
displayThemeArt(dispOptions, () => { displayThemeArt(dispOptions, () => {
// pause by default, unless explicitly told not to // pause by default, unless explicitly told not to
if(_.has(area, 'options.pause') && false === area.options.pause) { if(_.has(area, 'options.pause') && false === area.options.pause) {
return self.prevMenuOnTimeout(1000, cb); return self.prevMenuOnTimeout(1000, cb);
} else { } else {
@ -99,18 +99,18 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
}, timeout); }, timeout);
} }
// :TODO: these concepts have been replaced with the {someKey} style formatting - update me! // :TODO: these concepts have been replaced with the {someKey} style formatting - update me!
updateGeneralAreaInfoViews(areaIndex) { updateGeneralAreaInfoViews(areaIndex) {
/* /*
const areaInfo = self.messageAreas[areaIndex]; const areaInfo = self.messageAreas[areaIndex];
[ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => { [ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => {
const v = self.viewControllers.areaList.getView(mciId); const v = self.viewControllers.areaList.getView(mciId);
if(v) { if(v) {
v.setFormatObject(areaInfo.area); v.setFormatObject(areaInfo.area);
} }
}); });
*/ */
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
@ -119,16 +119,16 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu, mciMap : mciData.menu,
formId : 0, formId : 0,
}; };
vc.loadFromMenuConfig(loadOpts, function startingViewReady(err) { vc.loadFromMenuConfig(loadOpts, function startingViewReady(err) {
@ -136,8 +136,8 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
}); });
}, },
function populateAreaListView(callback) { function populateAreaListView(callback) {
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
const areaListView = vc.getView(MciViewIds.AreaList); const areaListView = vc.getView(MciViewIds.AreaList);
if(!areaListView) { if(!areaListView) {

View File

@ -1,16 +1,16 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
const persistMessage = require('./message_area.js').persistMessage; const persistMessage = require('./message_area.js').persistMessage;
const _ = require('lodash'); const _ = require('lodash');
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Area Post', name : 'Message Area Post',
desc : 'Module for posting a new message to an area', desc : 'Module for posting a new message to an area',
author : 'NuSkooler', author : 'NuSkooler',
}; };
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
@ -19,7 +19,7 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
const self = this; const self = this;
// we're posting, so always start with 'edit' mode // we're posting, so always start with 'edit' mode
this.editorMode = 'edit'; this.editorMode = 'edit';
this.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) { this.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) {
@ -42,9 +42,9 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
], ],
function complete(err) { function complete(err) {
if(err) { if(err) {
// :TODO:... sooooo now what? // :TODO:... sooooo now what?
} else { } else {
// note: not logging 'from' here as it's part of client.log.xxxx() // note: not logging 'from' here as it's part of client.log.xxxx()
self.client.log.info( self.client.log.info(
{ to : msg.toUserName, subject : msg.subject, uuid : msg.uuid }, { to : msg.toUserName, subject : msg.subject, uuid : msg.uuid },
'Message persisted' 'Message persisted'

View File

@ -1,14 +1,14 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
exports.getModule = AreaReplyFSEModule; exports.getModule = AreaReplyFSEModule;
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Area Reply', name : 'Message Area Reply',
desc : 'Module for replying to an area message', desc : 'Module for replying to an area message',
author : 'NuSkooler', author : 'NuSkooler',
}; };
function AreaReplyFSEModule(options) { function AreaReplyFSEModule(options) {

View File

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

View File

@ -1,29 +1,29 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
const displayThemeArt = require('./theme.js').displayThemeArt; const displayThemeArt = require('./theme.js').displayThemeArt;
const resetScreen = require('./ansi_term.js').resetScreen; const resetScreen = require('./ansi_term.js').resetScreen;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Conference List', name : 'Message Conference List',
desc : 'Module for listing / choosing message conferences', desc : 'Module for listing / choosing message conferences',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
ConfList : 1, ConfList : 1,
// :TODO: // :TODO:
// # areas in conf .... see Obv/2, iNiQ, ... // # areas in conf .... see Obv/2, iNiQ, ...
// //
}; };
@ -37,9 +37,9 @@ exports.getModule = class MessageConfListModule extends MenuModule {
this.menuMethods = { this.menuMethods = {
changeConference : function(formData, extraArgs, cb) { changeConference : function(formData, extraArgs, cb) {
if(1 === formData.submitId) { if(1 === formData.submitId) {
let conf = self.messageConfs[formData.value.conf]; let conf = self.messageConfs[formData.value.conf];
const confTag = conf.confTag; const confTag = conf.confTag;
conf = conf.conf; // what we want is embedded conf = conf.conf; // what we want is embedded
messageArea.changeMessageConference(self.client, confTag, err => { messageArea.changeMessageConference(self.client, confTag, err => {
if(err) { if(err) {
@ -51,14 +51,14 @@ exports.getModule = class MessageConfListModule extends MenuModule {
} else { } else {
if(_.isString(conf.art)) { if(_.isString(conf.art)) {
const dispOptions = { const dispOptions = {
client : self.client, client : self.client,
name : conf.art, name : conf.art,
}; };
self.client.term.rawWrite(resetScreen()); self.client.term.rawWrite(resetScreen());
displayThemeArt(dispOptions, () => { displayThemeArt(dispOptions, () => {
// pause by default, unless explicitly told not to // pause by default, unless explicitly told not to
if(_.has(conf, 'options.pause') && false === conf.options.pause) { if(_.has(conf, 'options.pause') && false === conf.options.pause) {
return self.prevMenuOnTimeout(1000, cb); return self.prevMenuOnTimeout(1000, cb);
} else { } else {
@ -91,23 +91,23 @@ exports.getModule = class MessageConfListModule extends MenuModule {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
let loadOpts = { let loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu, mciMap : mciData.menu,
formId : 0, formId : 0,
}; };
vc.loadFromMenuConfig(loadOpts, callback); vc.loadFromMenuConfig(loadOpts, callback);
}, },
function populateConfListView(callback) { function populateConfListView(callback) {
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
const confListView = vc.getView(MciViewIds.ConfList); const confListView = vc.getView(MciViewIds.ConfList);
let i = 1; let i = 1;
@ -135,7 +135,7 @@ exports.getModule = class MessageConfListModule extends MenuModule {
callback(null); callback(null);
}, },
function populateTextViews(callback) { function populateTextViews(callback) {
// :TODO: populate other avail MCI, e.g. current conf name // :TODO: populate other avail MCI, e.g. current conf name
callback(null); callback(null);
} }
], ],

View File

@ -1,51 +1,51 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
/* /*
Available listFormat/focusListFormat members (VM1): Available listFormat/focusListFormat members (VM1):
msgNum : Message number msgNum : Message number
to : To username/handle to : To username/handle
from : From username/handle from : From username/handle
subj : Subject subj : Subject
ts : Message mod timestamp (format with config.dateTimeFormat) ts : Message mod timestamp (format with config.dateTimeFormat)
newIndicator : New mark/indicator (config.newIndicator) newIndicator : New mark/indicator (config.newIndicator)
MCI codes: MCI codes:
VM1 : Message list VM1 : Message list
TL2 : Message info 1: { msgNumSelected, msgNumTotal } TL2 : Message info 1: { msgNumSelected, msgNumTotal }
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message List', name : 'Message List',
desc : 'Module for listing/browsing available messages', desc : 'Module for listing/browsing available messages',
author : 'NuSkooler', author : 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
msgList : 1, // VM1 msgList : 1, // VM1
msgInfo1 : 2, // TL2 msgInfo1 : 2, // TL2
}; };
exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(MenuModule) { exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(MenuModule) {
constructor(options) { constructor(options) {
super(options); super(options);
// :TODO: consider this pattern in base MenuModule - clean up code all over // :TODO: consider this pattern in base MenuModule - clean up code all over
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs);
this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false); this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false);
@ -55,11 +55,11 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
this.initialFocusIndex = formData.value.message; this.initialFocusIndex = formData.value.message;
const modOpts = { const modOpts = {
extraArgs : { extraArgs : {
messageAreaTag : this.getSelectedAreaTag(formData.value.message),// this.config.messageAreaTag, messageAreaTag : this.getSelectedAreaTag(formData.value.message),// this.config.messageAreaTag,
messageList : this.config.messageList, messageList : this.config.messageList,
messageIndex : formData.value.message, messageIndex : formData.value.message,
lastMessageNextExit : true, lastMessageNextExit : true,
} }
}; };
@ -68,8 +68,8 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
} }
// //
// Provide a serializer so we don't dump *huge* bits of information to the log // Provide a serializer so we don't dump *huge* bits of information to the log
// due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189 // due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189
// //
const self = this; const self = this;
modOpts.extraArgs.toJSON = function() { modOpts.extraArgs.toJSON = function() {
@ -78,11 +78,11 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
self.config.messageList.slice(0, 2).concat(self.config.messageList.slice(-2)); self.config.messageList.slice(0, 2).concat(self.config.messageList.slice(-2));
return { return {
// note |this| is scope of toJSON()! // note |this| is scope of toJSON()!
messageAreaTag : this.messageAreaTag, messageAreaTag : this.messageAreaTag,
apprevMessageList : logMsgList, apprevMessageList : logMsgList,
messageCount : this.messageList.length, messageCount : this.messageList.length,
messageIndex : this.messageIndex, messageIndex : this.messageIndex,
}; };
}; };
@ -111,10 +111,10 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
super.enter(); super.enter();
// //
// Config can specify |messageAreaTag| else it comes from // Config can specify |messageAreaTag| else it comes from
// the user's current area. If |messageList| is supplied, // the user's current area. If |messageList| is supplied,
// each item is expected to contain |areaTag|, so we use that // each item is expected to contain |areaTag|, so we use that
// instead in those cases. // instead in those cases.
// //
if(!Array.isArray(this.config.messageList)) { if(!Array.isArray(this.config.messageList)) {
if(this.config.messageAreaTag) { if(this.config.messageAreaTag) {
@ -136,23 +136,23 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
let configProvidedMessageList = false; let configProvidedMessageList = false;
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu mciMap : mciData.menu
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
}, },
function fetchMessagesInArea(callback) { function fetchMessagesInArea(callback) {
// //
// Config can supply messages else we'll need to populate the list now // Config can supply messages else we'll need to populate the list now
// //
if(_.isArray(self.config.messageList)) { if(_.isArray(self.config.messageList)) {
configProvidedMessageList = true; configProvidedMessageList = true;
@ -169,7 +169,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
}); });
}, },
function getLastReadMesageId(callback) { function getLastReadMesageId(callback) {
// messageList entries can contain |isNew| if they want to be considered new // messageList entries can contain |isNew| if they want to be considered new
if(configProvidedMessageList) { if(configProvidedMessageList) {
self.lastReadId = 0; self.lastReadId = 0;
return callback(null); return callback(null);
@ -177,33 +177,33 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
messageArea.getMessageAreaLastReadId(self.client.user.userId, self.config.messageAreaTag, function lastRead(err, lastReadId) { messageArea.getMessageAreaLastReadId(self.client.user.userId, self.config.messageAreaTag, function lastRead(err, lastReadId) {
self.lastReadId = lastReadId || 0; self.lastReadId = lastReadId || 0;
return callback(null); // ignore any errors, e.g. missing value return callback(null); // ignore any errors, e.g. missing value
}); });
}, },
function updateMessageListObjects(callback) { function updateMessageListObjects(callback) {
const dateTimeFormat = self.menuConfig.config.dateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat(); const dateTimeFormat = self.menuConfig.config.dateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat();
const newIndicator = self.menuConfig.config.newIndicator || '*'; const newIndicator = self.menuConfig.config.newIndicator || '*';
const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues
let msgNum = 1; let msgNum = 1;
self.config.messageList.forEach( (listItem, index) => { self.config.messageList.forEach( (listItem, index) => {
listItem.msgNum = msgNum++; listItem.msgNum = msgNum++;
listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat); listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat);
const isNew = _.isBoolean(listItem.isNew) ? listItem.isNew : listItem.messageId > self.lastReadId; const isNew = _.isBoolean(listItem.isNew) ? listItem.isNew : listItem.messageId > self.lastReadId;
listItem.newIndicator = isNew ? newIndicator : regIndicator; listItem.newIndicator = isNew ? newIndicator : regIndicator;
if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) { if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) {
self.initialFocusIndex = index; self.initialFocusIndex = index;
} }
listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text
}); });
return callback(null); return callback(null);
}, },
function populateList(callback) { function populateList(callback) {
const msgListView = vc.getView(MciViewIds.msgList); const msgListView = vc.getView(MciViewIds.msgList);
// :TODO: replace with standard custom info MCI - msgNumSelected, msgNumTotal, areaName, areaDesc, confName, confDesc, ... // :TODO: replace with standard custom info MCI - msgNumSelected, msgNumTotal, areaName, areaDesc, confName, confDesc, ...
const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}';
msgListView.setItems(self.config.messageList); msgListView.setItems(self.config.messageList);
@ -215,7 +215,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
}); });
if(self.initialFocusIndex > 0) { if(self.initialFocusIndex > 0) {
// note: causes redraw() // note: causes redraw()
msgListView.setFocusItemIndex(self.initialFocusIndex); msgListView.setFocusItemIndex(self.initialFocusIndex);
} else { } else {
msgListView.redraw(); msgListView.redraw();

View File

@ -1,15 +1,15 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
let loadModulesForCategory = require('./module_util.js').loadModulesForCategory; let loadModulesForCategory = require('./module_util.js').loadModulesForCategory;
// standard/deps // standard/deps
let async = require('async'); let async = require('async');
exports.startup = startup; exports.startup = startup;
exports.shutdown = shutdown; exports.shutdown = shutdown;
exports.recordMessage = recordMessage; exports.recordMessage = recordMessage;
let msgNetworkModules = []; let msgNetworkModules = [];
@ -53,9 +53,9 @@ function shutdown(cb) {
function recordMessage(message, cb) { function recordMessage(message, cb) {
// //
// Give all message network modules (scanner/tossers) // Give all message network modules (scanner/tossers)
// a chance to do something with |message|. Any or all can // a chance to do something with |message|. Any or all can
// choose to ignore it. // choose to ignore it.
// //
async.each(msgNetworkModules, (modInst, next) => { async.each(msgNetworkModules, (modInst, next) => {
modInst.record(message); modInst.record(message);

View File

@ -1,10 +1,10 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
var PluginModule = require('./plugin_module.js').PluginModule; var PluginModule = require('./plugin_module.js').PluginModule;
exports.MessageScanTossModule = MessageScanTossModule; exports.MessageScanTossModule = MessageScanTossModule;
function MessageScanTossModule() { function MessageScanTossModule() {
PluginModule.call(this); PluginModule.call(this);

View File

@ -1,22 +1,22 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const View = require('./view.js').View; const View = require('./view.js').View;
const strUtil = require('./string_util.js'); const strUtil = require('./string_util.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const wordWrapText = require('./word_wrap.js').wordWrapText; const wordWrapText = require('./word_wrap.js').wordWrapText;
const ansiPrep = require('./ansi_prep.js'); const ansiPrep = require('./ansi_prep.js');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
// :TODO: Determine CTRL-* keys for various things // :TODO: Determine CTRL-* keys for various things
// See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt // See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt
// http://wiki.synchro.net/howto:editor:slyedit#edit_mode // http://wiki.synchro.net/howto:editor:slyedit#edit_mode
// http://sublime-text-unofficial-documentation.readthedocs.org/en/latest/reference/keyboard_shortcuts_win.html // http://sublime-text-unofficial-documentation.readthedocs.org/en/latest/reference/keyboard_shortcuts_win.html
/* Mystic /* Mystic
[^B] Reformat Paragraph [^O] Show this help file [^B] Reformat Paragraph [^O] Show this help file
[^I] Insert tab space [^Q] Enter quote mode [^I] Insert tab space [^Q] Enter quote mode
[^K] Cut current line of text [^V] Toggle insert/overwrite [^K] Cut current line of text [^V] Toggle insert/overwrite
[^U] Paste previously cut text [^Y] Delete current line [^U] Paste previously cut text [^Y] Delete current line
@ -29,58 +29,58 @@ const _ = require('lodash');
*/ */
// //
// Some other interesting implementations, resources, etc. // Some other interesting implementations, resources, etc.
// //
// Editors - BBS // Editors - BBS
// * https://github.com/M-griffin/Enthral/blob/master/src/msg_fse.cpp // * https://github.com/M-griffin/Enthral/blob/master/src/msg_fse.cpp
// //
// //
// Editors - Other // Editors - Other
// * http://joe-editor.sourceforge.net/ // * http://joe-editor.sourceforge.net/
// * http://www.jbox.dk/downloads/edit.c // * http://www.jbox.dk/downloads/edit.c
// * https://github.com/dominictarr/hipster // * https://github.com/dominictarr/hipster
// //
// Implementations - Word Wrap // Implementations - Word Wrap
// * https://github.com/protomouse/synchronet/blob/93b01c55b3102ebc3c4f4793c3a45b8c13d0dc2a/src/sbbs3/wordwrap.c // * https://github.com/protomouse/synchronet/blob/93b01c55b3102ebc3c4f4793c3a45b8c13d0dc2a/src/sbbs3/wordwrap.c
// //
// Misc notes // Misc notes
// * https://github.com/dominictarr/hipster/issues/15 (Deleting lines/etc.) // * https://github.com/dominictarr/hipster/issues/15 (Deleting lines/etc.)
// //
// Blessed // Blessed
// insertLine: CSR(top, bottom) + CUP(y, 0) + IL(1) + CSR(0, height) // insertLine: CSR(top, bottom) + CUP(y, 0) + IL(1) + CSR(0, height)
// deleteLine: CSR(top, bottom) + CUP(y, 0) + DL(1) + CSR(0, height) // deleteLine: CSR(top, bottom) + CUP(y, 0) + DL(1) + CSR(0, height)
// Quick Ansi -- update only what was changed: // Quick Ansi -- update only what was changed:
// https://github.com/dominictarr/quickansi // https://github.com/dominictarr/quickansi
// //
// To-Do // To-Do
// //
// * Index pos % for emit scroll events // * Index pos % for emit scroll events
// * Some of this should be async'd where there is lots of processing (e.g. word wrap) // * Some of this should be async'd where there is lots of processing (e.g. word wrap)
// * Fix backspace when col=0 (e.g. bs to prev line) // * Fix backspace when col=0 (e.g. bs to prev line)
// * Add word delete (CTRL+????) // * Add word delete (CTRL+????)
// * // *
const SPECIAL_KEY_MAP_DEFAULT = { const SPECIAL_KEY_MAP_DEFAULT = {
'line feed' : [ 'return' ], 'line feed' : [ 'return' ],
exit : [ 'esc' ], exit : [ 'esc' ],
backspace : [ 'backspace' ], backspace : [ 'backspace' ],
delete : [ 'delete' ], delete : [ 'delete' ],
tab : [ 'tab' ], tab : [ 'tab' ],
up : [ 'up arrow' ], up : [ 'up arrow' ],
down : [ 'down arrow' ], down : [ 'down arrow' ],
end : [ 'end' ], end : [ 'end' ],
home : [ 'home' ], home : [ 'home' ],
left : [ 'left arrow' ], left : [ 'left arrow' ],
right : [ 'right arrow' ], right : [ 'right arrow' ],
'delete line' : [ 'ctrl + y' ], 'delete line' : [ 'ctrl + y' ],
'page up' : [ 'page up' ], 'page up' : [ 'page up' ],
'page down' : [ 'page down' ], 'page down' : [ 'page down' ],
insert : [ 'insert', 'ctrl + v' ], insert : [ 'insert', 'ctrl + v' ],
}; };
exports.MultiLineEditTextView = MultiLineEditTextView; exports.MultiLineEditTextView = MultiLineEditTextView;
function MultiLineEditTextView(options) { function MultiLineEditTextView(options) {
if(!_.isBoolean(options.acceptsFocus)) { if(!_.isBoolean(options.acceptsFocus)) {
@ -100,18 +100,18 @@ function MultiLineEditTextView(options) {
var self = this; var self = this;
// //
// ANSI seems to want tabs to default to 8 characters. See the following: // ANSI seems to want tabs to default to 8 characters. See the following:
// * http://www.ansi-bbs.org/ansi-bbs2/control_chars/ // * http://www.ansi-bbs.org/ansi-bbs2/control_chars/
// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt // * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt
// //
// This seems overkill though, so let's default to 4 :) // This seems overkill though, so let's default to 4 :)
// :TODO: what shoudl this really be? Maybe 8 is OK // :TODO: what shoudl this really be? Maybe 8 is OK
// //
this.tabWidth = _.isNumber(options.tabWidth) ? options.tabWidth : 4; this.tabWidth = _.isNumber(options.tabWidth) ? options.tabWidth : 4;
this.textLines = [ ]; this.textLines = [ ];
this.topVisibleIndex = 0; this.topVisibleIndex = 0;
this.mode = options.mode || 'edit'; // edit | preview | read-only this.mode = options.mode || 'edit'; // edit | preview | read-only
if ('preview' === this.mode) { if ('preview' === this.mode) {
this.autoScroll = options.autoScroll || true; this.autoScroll = options.autoScroll || true;
@ -121,10 +121,10 @@ function MultiLineEditTextView(options) {
this.tabSwitchesView = options.tabSwitchesView || false; this.tabSwitchesView = options.tabSwitchesView || false;
} }
// //
// cursorPos represents zero-based row, col positions // cursorPos represents zero-based row, col positions
// within the editor itself // within the editor itself
// //
this.cursorPos = { col : 0, row : 0 }; this.cursorPos = { col : 0, row : 0 };
this.getSGRFor = function(sgrFor) { this.getSGRFor = function(sgrFor) {
return { return {
@ -140,7 +140,7 @@ function MultiLineEditTextView(options) {
return 'preview' === self.mode; return 'preview' === self.mode;
}; };
// :TODO: Most of the calls to this could be avoided via incrementRow(), decrementRow() that keeps track or such // :TODO: Most of the calls to this could be avoided via incrementRow(), decrementRow() that keeps track or such
this.getTextLinesIndex = function(row) { this.getTextLinesIndex = function(row) {
if(!_.isNumber(row)) { if(!_.isNumber(row)) {
row = self.cursorPos.row; row = self.cursorPos.row;
@ -172,34 +172,34 @@ function MultiLineEditTextView(options) {
this.redrawRows = function(startRow, endRow) { this.redrawRows = function(startRow, endRow) {
self.toggleTextCursor('hide'); self.toggleTextCursor('hide');
const startIndex = self.getTextLinesIndex(startRow); const startIndex = self.getTextLinesIndex(startRow);
const endIndex = Math.min(self.getTextLinesIndex(endRow), self.textLines.length); const endIndex = Math.min(self.getTextLinesIndex(endRow), self.textLines.length);
const absPos = self.getAbsolutePosition(startRow, 0); const absPos = self.getAbsolutePosition(startRow, 0);
for(let i = startIndex; i < endIndex; ++i) { for(let i = startIndex; i < endIndex; ++i) {
//${self.getSGRFor('text')} //${self.getSGRFor('text')}
self.client.term.write( self.client.term.write(
`${ansi.goto(absPos.row++, absPos.col)}${self.getRenderText(i)}`, `${ansi.goto(absPos.row++, absPos.col)}${self.getRenderText(i)}`,
false // convertLineFeeds false // convertLineFeeds
); );
} }
self.toggleTextCursor('show'); self.toggleTextCursor('show');
return absPos.row - self.position.row; // row we ended on return absPos.row - self.position.row; // row we ended on
}; };
this.eraseRows = function(startRow, endRow) { this.eraseRows = function(startRow, endRow) {
self.toggleTextCursor('hide'); self.toggleTextCursor('hide');
const absPos = self.getAbsolutePosition(startRow, 0); const absPos = self.getAbsolutePosition(startRow, 0);
const absPosEnd = self.getAbsolutePosition(endRow, 0); const absPosEnd = self.getAbsolutePosition(endRow, 0);
const eraseFiller = ' '.repeat(self.dimens.width);//new Array(self.dimens.width).join(' '); const eraseFiller = ' '.repeat(self.dimens.width);//new Array(self.dimens.width).join(' ');
while(absPos.row < absPosEnd.row) { while(absPos.row < absPosEnd.row) {
self.client.term.write( self.client.term.write(
`${ansi.goto(absPos.row++, absPos.col)}${eraseFiller}`, `${ansi.goto(absPos.row++, absPos.col)}${eraseFiller}`,
false // convertLineFeeds false // convertLineFeeds
); );
} }
@ -213,16 +213,16 @@ function MultiLineEditTextView(options) {
self.eraseRows(lastRow, self.dimens.height); self.eraseRows(lastRow, self.dimens.height);
/* /*
// :TOOD: create eraseRows(startRow, endRow) // :TOOD: create eraseRows(startRow, endRow)
if(lastRow < self.dimens.height) { if(lastRow < self.dimens.height) {
var absPos = self.getAbsolutePosition(lastRow, 0); var absPos = self.getAbsolutePosition(lastRow, 0);
var empty = new Array(self.dimens.width).join(' '); var empty = new Array(self.dimens.width).join(' ');
while(lastRow++ < self.dimens.height) { while(lastRow++ < self.dimens.height) {
self.client.term.write(ansi.goto(absPos.row++, absPos.col)); self.client.term.write(ansi.goto(absPos.row++, absPos.col));
self.client.term.write(empty); self.client.term.write(empty);
} }
} }
*/ */
}; };
this.getVisibleText = function(index) { this.getVisibleText = function(index) {
@ -262,12 +262,12 @@ function MultiLineEditTextView(options) {
}; };
this.getRenderText = function(index) { this.getRenderText = function(index) {
let text = self.getVisibleText(index); let text = self.getVisibleText(index);
const remain = self.dimens.width - text.length; const remain = self.dimens.width - text.length;
if(remain > 0) { if(remain > 0) {
text += ' '.repeat(remain + 1); text += ' '.repeat(remain + 1);
// text += new Array(remain + 1).join(' '); // text += new Array(remain + 1).join(' ');
} }
return text; return text;
@ -278,15 +278,15 @@ function MultiLineEditTextView(options) {
if(startIndex === endIndex) { if(startIndex === endIndex) {
lines = [ self.textLines[startIndex] ]; lines = [ self.textLines[startIndex] ];
} else { } else {
lines = self.textLines.slice(startIndex, endIndex + 1); // "slice extracts up to but not including end." lines = self.textLines.slice(startIndex, endIndex + 1); // "slice extracts up to but not including end."
} }
return lines; return lines;
}; };
this.getOutputText = function(startIndex, endIndex, eolMarker, options) { this.getOutputText = function(startIndex, endIndex, eolMarker, options) {
const lines = self.getTextLines(startIndex, endIndex); const lines = self.getTextLines(startIndex, endIndex);
let text = ''; let text = '';
const re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g'); const re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g');
lines.forEach(line => { lines.forEach(line => {
text += line.text.replace(re, '\t'); text += line.text.replace(re, '\t');
@ -317,28 +317,28 @@ function MultiLineEditTextView(options) {
}; };
/* /*
this.editTextAtPosition = function(editAction, text, index, col) { this.editTextAtPosition = function(editAction, text, index, col) {
switch(editAction) { switch(editAction) {
case 'insert' : case 'insert' :
self.insertCharactersInText(text, index, col); self.insertCharactersInText(text, index, col);
break; break;
case 'deleteForward' : case 'deleteForward' :
break; break;
case 'deleteBack' : case 'deleteBack' :
break; break;
case 'replace' : case 'replace' :
break; break;
} }
}; };
*/ */
this.updateTextWordWrap = function(index) { this.updateTextWordWrap = function(index) {
const nextEolIndex = self.getNextEndOfLineIndex(index); const nextEolIndex = self.getNextEndOfLineIndex(index);
const wrapped = self.wordWrapSingleLine(self.getContiguousText(index, nextEolIndex), 'tabsIntact'); const wrapped = self.wordWrapSingleLine(self.getContiguousText(index, nextEolIndex), 'tabsIntact');
const newLines = wrapped.wrapped.map(l => { return { text : l }; } ); const newLines = wrapped.wrapped.map(l => { return { text : l }; } );
newLines[newLines.length - 1].eol = true; newLines[newLines.length - 1].eol = true;
@ -352,17 +352,17 @@ function MultiLineEditTextView(options) {
this.removeCharactersFromText = function(index, col, operation, count) { this.removeCharactersFromText = function(index, col, operation, count) {
if('delete' === operation) { if('delete' === operation) {
self.textLines[index].text = self.textLines[index].text =
self.textLines[index].text.slice(0, col) + self.textLines[index].text.slice(0, col) +
self.textLines[index].text.slice(col + count); self.textLines[index].text.slice(col + count);
self.updateTextWordWrap(index); self.updateTextWordWrap(index);
self.redrawRows(self.cursorPos.row, self.dimens.height); self.redrawRows(self.cursorPos.row, self.dimens.height);
self.moveClientCursorToCursorPos(); self.moveClientCursorToCursorPos();
} else if ('backspace' === operation) { } else if ('backspace' === operation) {
// :TODO: method for splicing text // :TODO: method for splicing text
self.textLines[index].text = self.textLines[index].text =
self.textLines[index].text.slice(0, col - (count - 1)) + self.textLines[index].text.slice(0, col - (count - 1)) +
self.textLines[index].text.slice(col + 1); self.textLines[index].text.slice(col + 1);
self.cursorPos.col -= (count - 1); self.cursorPos.col -= (count - 1);
@ -372,14 +372,14 @@ function MultiLineEditTextView(options) {
self.moveClientCursorToCursorPos(); self.moveClientCursorToCursorPos();
} else if('delete line' === operation) { } else if('delete line' === operation) {
// //
// Delete a visible line. Note that this is *not* the "physical" line, or // Delete a visible line. Note that this is *not* the "physical" line, or
// 1:n entries up to eol! This is to keep consistency with home/end, and // 1:n entries up to eol! This is to keep consistency with home/end, and
// some other text editors such as nano. Sublime for example want to // some other text editors such as nano. Sublime for example want to
// treat all of these things using the physical approach, but this seems // treat all of these things using the physical approach, but this seems
// a bit odd in this context. // a bit odd in this context.
// //
var isLastLine = (index === self.textLines.length - 1); var isLastLine = (index === self.textLines.length - 1);
var hadEol = self.textLines[index].eol; var hadEol = self.textLines[index].eol;
self.textLines.splice(index, 1); self.textLines.splice(index, 1);
if(hadEol && self.textLines.length > index && !self.textLines[index].eol) { if(hadEol && self.textLines.length > index && !self.textLines[index].eol) {
@ -387,11 +387,11 @@ function MultiLineEditTextView(options) {
} }
// //
// Create a empty edit buffer if necessary // Create a empty edit buffer if necessary
// :TODO: Make this a method // :TODO: Make this a method
if(self.textLines.length < 1) { if(self.textLines.length < 1) {
self.textLines = [ { text : '', eol : true } ]; self.textLines = [ { text : '', eol : true } ];
isLastLine = false; // resetting isLastLine = false; // resetting
} }
self.cursorPos.col = 0; self.cursorPos.col = 0;
@ -400,7 +400,7 @@ function MultiLineEditTextView(options) {
self.eraseRows(lastRow, self.dimens.height); self.eraseRows(lastRow, self.dimens.height);
// //
// If we just deleted the last line in the buffer, move up // If we just deleted the last line in the buffer, move up
// //
if(isLastLine) { if(isLastLine) {
self.cursorEndOfPreviousLine(); self.cursorEndOfPreviousLine();
@ -411,8 +411,8 @@ function MultiLineEditTextView(options) {
}; };
this.insertCharactersInText = function(c, index, col) { this.insertCharactersInText = function(c, index, col) {
const prevTextLength = self.getTextLength(index); const prevTextLength = self.getTextLength(index);
let editingEol = self.cursorPos.col === prevTextLength; let editingEol = self.cursorPos.col === prevTextLength;
self.textLines[index].text = [ self.textLines[index].text = [
self.textLines[index].text.slice(0, col), self.textLines[index].text.slice(0, col),
@ -424,43 +424,43 @@ function MultiLineEditTextView(options) {
if(self.getTextLength(index) > self.dimens.width) { if(self.getTextLength(index) > self.dimens.width) {
// //
// Update word wrapping and |cursorOffset| if the cursor // Update word wrapping and |cursorOffset| if the cursor
// was within the bounds of the wrapped text // was within the bounds of the wrapped text
// //
let cursorOffset; let cursorOffset;
const lastCol = self.cursorPos.col - c.length; const lastCol = self.cursorPos.col - c.length;
const firstWrapRange = self.updateTextWordWrap(index); const firstWrapRange = self.updateTextWordWrap(index);
if(lastCol >= firstWrapRange.start && lastCol <= firstWrapRange.end) { if(lastCol >= firstWrapRange.start && lastCol <= firstWrapRange.end) {
cursorOffset = self.cursorPos.col - firstWrapRange.start; cursorOffset = self.cursorPos.col - firstWrapRange.start;
editingEol = true; //override editingEol = true; //override
} else { } else {
cursorOffset = firstWrapRange.end; cursorOffset = firstWrapRange.end;
} }
// redraw from current row to end of visible area // redraw from current row to end of visible area
self.redrawRows(self.cursorPos.row, self.dimens.height); self.redrawRows(self.cursorPos.row, self.dimens.height);
// If we're editing mid, we're done here. Else, we need to // If we're editing mid, we're done here. Else, we need to
// move the cursor to the new editing position after a wrap // move the cursor to the new editing position after a wrap
if(editingEol) { if(editingEol) {
self.cursorBeginOfNextLine(); self.cursorBeginOfNextLine();
self.cursorPos.col += cursorOffset; self.cursorPos.col += cursorOffset;
self.client.term.rawWrite(ansi.right(cursorOffset)); self.client.term.rawWrite(ansi.right(cursorOffset));
} else { } else {
// adjust cursor after drawing new rows // adjust cursor after drawing new rows
const absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); const absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col);
self.client.term.rawWrite(ansi.goto(absPos.row, absPos.col)); self.client.term.rawWrite(ansi.goto(absPos.row, absPos.col));
} }
} else { } else {
// //
// We must only redraw from col -> end of current visible line // We must only redraw from col -> end of current visible line
// //
const absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); const absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col);
const renderText = self.getRenderText(index).slice(self.cursorPos.col - c.length); const renderText = self.getRenderText(index).slice(self.cursorPos.col - c.length);
self.client.term.write( self.client.term.write(
`${ansi.hideCursor()}${self.getSGRFor('text')}${renderText}${ansi.goto(absPos.row, absPos.col)}${ansi.showCursor()}`, `${ansi.hideCursor()}${self.getSGRFor('text')}${renderText}${ansi.goto(absPos.row, absPos.col)}${ansi.showCursor()}`,
false // convertLineFeeds false // convertLineFeeds
); );
} }
}; };
@ -502,24 +502,24 @@ function MultiLineEditTextView(options) {
return wordWrapText( return wordWrapText(
line, line,
{ {
width : self.dimens.width, width : self.dimens.width,
tabHandling : tabHandling, tabHandling : tabHandling,
tabWidth : self.tabWidth, tabWidth : self.tabWidth,
tabChar : '\t', tabChar : '\t',
} }
); );
}; };
this.setTextLines = function(lines, index, termWithEol) { this.setTextLines = function(lines, index, termWithEol) {
if(0 === index && (0 === self.textLines.length || (self.textLines.length === 1 && '' === self.textLines[0].text) )) { if(0 === index && (0 === self.textLines.length || (self.textLines.length === 1 && '' === self.textLines[0].text) )) {
// quick path: just set the things // quick path: just set the things
self.textLines = lines.slice(0, -1).map(l => { self.textLines = lines.slice(0, -1).map(l => {
return { text : l }; return { text : l };
}).concat( { text : lines[lines.length - 1], eol : termWithEol } ); }).concat( { text : lines[lines.length - 1], eol : termWithEol } );
} else { } else {
// insert somewhere in textLines... // insert somewhere in textLines...
if(index > self.textLines.length) { if(index > self.textLines.length) {
// fill with empty // fill with empty
self.textLines.splice( self.textLines.splice(
self.textLines.length, self.textLines.length,
0, 0,
@ -547,7 +547,7 @@ function MultiLineEditTextView(options) {
let index = 0; let index = 0;
text.forEach(line => { text.forEach(line => {
self.setTextLines( [ line ], index, true); // true=termWithEol self.setTextLines( [ line ], index, true); // true=termWithEol
index += 1; index += 1;
}); });
@ -565,12 +565,12 @@ function MultiLineEditTextView(options) {
ansiPrep( ansiPrep(
ansi, ansi,
{ {
termWidth : this.client.term.termWidth, termWidth : this.client.term.termWidth,
termHeight : this.client.term.termHeight, termHeight : this.client.term.termHeight,
cols : this.dimens.width, cols : this.dimens.width,
rows : 'auto', rows : 'auto',
startCol : this.position.col, startCol : this.position.col,
forceLineTerm : options.forceLineTerm, forceLineTerm : options.forceLineTerm,
}, },
(err, preppedAnsi) => { (err, preppedAnsi) => {
return setLines(err ? ansi : preppedAnsi); return setLines(err ? ansi : preppedAnsi);
@ -580,31 +580,31 @@ function MultiLineEditTextView(options) {
this.insertRawText = function(text, index, col) { this.insertRawText = function(text, index, col) {
// //
// Perform the following on |text|: // Perform the following on |text|:
// * Normalize various line feed formats -> \n // * Normalize various line feed formats -> \n
// * Remove some control characters (e.g. \b) // * Remove some control characters (e.g. \b)
// * Word wrap lines such that they fit in the visible workspace. // * Word wrap lines such that they fit in the visible workspace.
// Each actual line will then take 1:n elements in textLines[]. // Each actual line will then take 1:n elements in textLines[].
// * Each tab will be appropriately expanded and take 1:n \t // * Each tab will be appropriately expanded and take 1:n \t
// characters. This allows us to know when we're in tab space // characters. This allows us to know when we're in tab space
// when doing cursor movement/etc. // when doing cursor movement/etc.
// //
// //
// Try to handle any possible newline that can be fed to us. // Try to handle any possible newline that can be fed to us.
// See http://stackoverflow.com/questions/5034781/js-regex-to-split-by-line // See http://stackoverflow.com/questions/5034781/js-regex-to-split-by-line
// //
// :TODO: support index/col insertion point // :TODO: support index/col insertion point
if(_.isNumber(index)) { if(_.isNumber(index)) {
if(_.isNumber(col)) { if(_.isNumber(col)) {
// //
// Modify text to have information from index // Modify text to have information from index
// before and and after column // before and and after column
// //
// :TODO: Need to clean this string (e.g. collapse tabs) // :TODO: Need to clean this string (e.g. collapse tabs)
text = self.textLines; text = self.textLines;
// :TODO: Remove original line @ index // :TODO: Remove original line @ index
} }
} else { } else {
index = self.textLines.length; index = self.textLines.length;
@ -616,7 +616,7 @@ function MultiLineEditTextView(options) {
text.forEach(line => { text.forEach(line => {
wrapped = self.wordWrapSingleLine(line, 'expand').wrapped; wrapped = self.wordWrapSingleLine(line, 'expand').wrapped;
self.setTextLines(wrapped, index, true); // true=termWithEol self.setTextLines(wrapped, index, true); // true=termWithEol
index += wrapped.length; index += wrapped.length;
}); });
}; };
@ -638,16 +638,16 @@ function MultiLineEditTextView(options) {
var index = self.getTextLinesIndex(); var index = self.getTextLinesIndex();
// //
// :TODO: stuff that needs to happen // :TODO: stuff that needs to happen
// * Break up into smaller methods // * Break up into smaller methods
// * Even in overtype mode, word wrapping must apply if past bounds // * Even in overtype mode, word wrapping must apply if past bounds
// * A lot of this can be used for backspacing also // * A lot of this can be used for backspacing also
// * See how Sublime treats tabs in *non* overtype mode... just overwrite them? // * See how Sublime treats tabs in *non* overtype mode... just overwrite them?
// //
// //
if(self.overtypeMode) { if(self.overtypeMode) {
// :TODO: special handing for insert over eol mark? // :TODO: special handing for insert over eol mark?
self.replaceCharacterInText(c, index, self.cursorPos.col); self.replaceCharacterInText(c, index, self.cursorPos.col);
self.cursorPos.col++; self.cursorPos.col++;
self.client.term.write(c); self.client.term.write(c);
@ -754,7 +754,7 @@ function MultiLineEditTextView(options) {
self.adjustCursorIfPastEndOfLine(true); self.adjustCursorIfPastEndOfLine(true);
} else { } else {
self.cursorPos.row = 0; self.cursorPos.row = 0;
self.moveClientCursorToCursorPos(); // :TODO: ajust if eol, etc. self.moveClientCursorToCursorPos(); // :TODO: ajust if eol, etc.
} }
self.emitEditPosition(); self.emitEditPosition();
@ -773,13 +773,13 @@ function MultiLineEditTextView(options) {
this.keyPressLineFeed = function() { this.keyPressLineFeed = function() {
// //
// Break up text from cursor position, redraw, and update cursor // Break up text from cursor position, redraw, and update cursor
// position to start of next line // position to start of next line
// //
var index = self.getTextLinesIndex(); var index = self.getTextLinesIndex();
var nextEolIndex = self.getNextEndOfLineIndex(index); var nextEolIndex = self.getNextEndOfLineIndex(index);
var text = self.getContiguousText(index, nextEolIndex); var text = self.getContiguousText(index, nextEolIndex);
const newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped; const newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped;
newLines.unshift( { text : text.slice(0, self.cursorPos.col), eol : true } ); newLines.unshift( { text : text.slice(0, self.cursorPos.col), eol : true } );
for(var i = 1; i < newLines.length; ++i) { for(var i = 1; i < newLines.length; ++i) {
@ -791,7 +791,7 @@ function MultiLineEditTextView(options) {
self.textLines, self.textLines,
[ index, (nextEolIndex - index) + 1 ].concat(newLines)); [ index, (nextEolIndex - index) + 1 ].concat(newLines));
// redraw from current row to end of visible area // redraw from current row to end of visible area
self.redrawRows(self.cursorPos.row, self.dimens.height); self.redrawRows(self.cursorPos.row, self.dimens.height);
self.cursorBeginOfNextLine(); self.cursorBeginOfNextLine();
@ -812,8 +812,8 @@ function MultiLineEditTextView(options) {
this.keyPressBackspace = function() { this.keyPressBackspace = function() {
if(self.cursorPos.col >= 1) { if(self.cursorPos.col >= 1) {
// //
// Don't want to delete character at cursor, but rather the character // Don't want to delete character at cursor, but rather the character
// to the left of the cursor! // to the left of the cursor!
// //
self.cursorPos.col -= 1; self.cursorPos.col -= 1;
@ -842,12 +842,12 @@ function MultiLineEditTextView(options) {
count); count);
} else { } else {
// //
// Delete character at end of line previous. // Delete character at end of line previous.
// * This may be a eol marker // * This may be a eol marker
// * Word wrapping will need re-applied // * Word wrapping will need re-applied
// //
// :TODO: apply word wrapping such that text can be re-adjusted if it can now fit on prev // :TODO: apply word wrapping such that text can be re-adjusted if it can now fit on prev
self.keyPressLeft(); // same as hitting left - jump to previous line self.keyPressLeft(); // same as hitting left - jump to previous line
//self.keyPressBackspace(); //self.keyPressBackspace();
} }
@ -859,7 +859,7 @@ function MultiLineEditTextView(options) {
if(0 === self.cursorPos.col && 0 === self.textLines[lineIndex].text.length && self.textLines.length > 0) { if(0 === self.cursorPos.col && 0 === self.textLines[lineIndex].text.length && self.textLines.length > 0) {
// //
// Start of line and nothing left. Just delete the line // Start of line and nothing left. Just delete the line
// //
self.removeCharactersFromText( self.removeCharactersFromText(
lineIndex, lineIndex,
@ -906,7 +906,7 @@ function MultiLineEditTextView(options) {
var move; var move;
switch(direction) { switch(direction) {
// //
// Next tabstop to the right // Next tabstop to the right
// //
case 'right' : case 'right' :
move = self.getNextTabStop(self.cursorPos.col) - self.cursorPos.col; move = self.getNextTabStop(self.cursorPos.col) - self.cursorPos.col;
@ -915,7 +915,7 @@ function MultiLineEditTextView(options) {
break; break;
// //
// Next tabstop to the left // Next tabstop to the left
// //
case 'left' : case 'left' :
move = self.cursorPos.col - self.getPrevTabStop(self.cursorPos.col); move = self.cursorPos.col - self.getPrevTabStop(self.cursorPos.col);
@ -926,7 +926,7 @@ function MultiLineEditTextView(options) {
case 'up' : case 'up' :
case 'down' : case 'down' :
// //
// Jump to the tabstop nearest the cursor // Jump to the tabstop nearest the cursor
// //
var newCol = self.tabStops.reduce(function r(prev, curr) { var newCol = self.tabStops.reduce(function r(prev, curr) {
return (Math.abs(curr - self.cursorPos.col) < Math.abs(prev - self.cursorPos.col) ? curr : prev); return (Math.abs(curr - self.cursorPos.col) < Math.abs(prev - self.cursorPos.col) ? curr : prev);
@ -946,43 +946,43 @@ function MultiLineEditTextView(options) {
return true; return true;
} }
return false; // did not fall on a tab return false; // did not fall on a tab
}; };
this.cursorStartOfDocument = function() { this.cursorStartOfDocument = function() {
self.topVisibleIndex = 0; self.topVisibleIndex = 0;
self.cursorPos = { row : 0, col : 0 }; self.cursorPos = { row : 0, col : 0 };
self.redraw(); self.redraw();
self.moveClientCursorToCursorPos(); self.moveClientCursorToCursorPos();
}; };
this.cursorEndOfDocument = function() { this.cursorEndOfDocument = function() {
self.topVisibleIndex = Math.max(self.textLines.length - self.dimens.height, 0); self.topVisibleIndex = Math.max(self.textLines.length - self.dimens.height, 0);
self.cursorPos.row = (self.textLines.length - self.topVisibleIndex) - 1; self.cursorPos.row = (self.textLines.length - self.topVisibleIndex) - 1;
self.cursorPos.col = self.getTextEndOfLineColumn(); self.cursorPos.col = self.getTextEndOfLineColumn();
self.redraw(); self.redraw();
self.moveClientCursorToCursorPos(); self.moveClientCursorToCursorPos();
}; };
this.cursorBeginOfNextLine = function() { this.cursorBeginOfNextLine = function() {
// e.g. when scrolling right past eol // e.g. when scrolling right past eol
var linesBelow = self.getRemainingLinesBelowRow(); var linesBelow = self.getRemainingLinesBelowRow();
if(linesBelow > 0) { if(linesBelow > 0) {
var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1; var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1;
if(self.cursorPos.row < lastVisibleRow) { if(self.cursorPos.row < lastVisibleRow) {
self.cursorPos.row++; self.cursorPos.row++;
} else { } else {
self.scrollDocumentUp(); self.scrollDocumentUp();
} }
self.keyPressHome(); // same as pressing 'home' self.keyPressHome(); // same as pressing 'home'
} }
}; };
this.cursorEndOfPreviousLine = function() { this.cursorEndOfPreviousLine = function() {
// e.g. when scrolling left past start of line // e.g. when scrolling left past start of line
var moveToEnd; var moveToEnd;
if(self.cursorPos.row > 0) { if(self.cursorPos.row > 0) {
self.cursorPos.row--; self.cursorPos.row--;
@ -993,30 +993,30 @@ function MultiLineEditTextView(options) {
} }
if(moveToEnd) { if(moveToEnd) {
self.keyPressEnd(); // same as pressing 'end' self.keyPressEnd(); // same as pressing 'end'
} }
}; };
/* /*
this.cusorEndOfNextLine = function() { this.cusorEndOfNextLine = function() {
var linesBelow = self.getRemainingLinesBelowRow(); var linesBelow = self.getRemainingLinesBelowRow();
if(linesBelow > 0) { if(linesBelow > 0) {
var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1; var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1;
if(self.cursorPos.row < lastVisibleRow) { if(self.cursorPos.row < lastVisibleRow) {
self.cursorPos.row++; self.cursorPos.row++;
} else { } else {
self.scrollDocumentUp(); self.scrollDocumentUp();
} }
self.keyPressEnd(); // same as pressing 'end' self.keyPressEnd(); // same as pressing 'end'
} }
}; };
*/ */
this.scrollDocumentUp = function() { this.scrollDocumentUp = function() {
// //
// Note: We scroll *up* when the cursor goes *down* beyond // Note: We scroll *up* when the cursor goes *down* beyond
// the visible area! // the visible area!
// //
var linesBelow = self.getRemainingLinesBelowRow(); var linesBelow = self.getRemainingLinesBelowRow();
if(linesBelow > 0) { if(linesBelow > 0) {
@ -1027,8 +1027,8 @@ function MultiLineEditTextView(options) {
this.scrollDocumentDown = function() { this.scrollDocumentDown = function() {
// //
// Note: We scroll *down* when the cursor goes *up* beyond // Note: We scroll *down* when the cursor goes *up* beyond
// the visible area! // the visible area!
// //
if(self.topVisibleIndex > 0) { if(self.topVisibleIndex > 0) {
self.topVisibleIndex--; self.topVisibleIndex--;
@ -1037,7 +1037,7 @@ function MultiLineEditTextView(options) {
}; };
this.emitEditPosition = function() { this.emitEditPosition = function() {
self.emit('edit position', self.getEditPosition()); self.emit('edit position', self.getEditPosition());
}; };
this.toggleTextEditMode = function() { this.toggleTextEditMode = function() {
@ -1045,7 +1045,7 @@ function MultiLineEditTextView(options) {
self.emit('text edit mode', self.getTextEditMode()); self.emit('text edit mode', self.getTextEditMode());
}; };
this.insertRawText(''); // init to blank/empty this.insertRawText(''); // init to blank/empty
} }
require('util').inherits(MultiLineEditTextView, View); require('util').inherits(MultiLineEditTextView, View);
@ -1074,11 +1074,11 @@ MultiLineEditTextView.prototype.setText = function(text, options = { scrollMode
this.addText(text, options); this.addText(text, options);
/*this.insertRawText(text); /*this.insertRawText(text);
if(this.isEditMode()) { if(this.isEditMode()) {
this.cursorEndOfDocument(); this.cursorEndOfDocument();
} else if(this.isPreviewMode()) { } else if(this.isPreviewMode()) {
this.cursorStartOfDocument(); this.cursorStartOfDocument();
}*/ }*/
}; };
MultiLineEditTextView.prototype.setAnsi = function(ansi, options = { prepped : false }, cb) { MultiLineEditTextView.prototype.setAnsi = function(ansi, options = { prepped : false }, cb) {
@ -1116,14 +1116,14 @@ MultiLineEditTextView.prototype.getData = function(options = { forceLineTerms :
MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) {
switch(propName) { switch(propName) {
case 'mode' : case 'mode' :
this.mode = value; this.mode = value;
if('preview' === value && !this.specialKeyMap.next) { if('preview' === value && !this.specialKeyMap.next) {
this.specialKeyMap.next = [ 'tab' ]; this.specialKeyMap.next = [ 'tab' ];
} }
break; break;
case 'autoScroll' : case 'autoScroll' :
this.autoScroll = value; this.autoScroll = value;
break; break;
@ -1205,9 +1205,9 @@ MultiLineEditTextView.prototype.getEditPosition = function() {
var currentIndex = this.getTextLinesIndex() + 1; var currentIndex = this.getTextLinesIndex() + 1;
return { return {
row : this.getTextLinesIndex(this.cursorPos.row), row : this.getTextLinesIndex(this.cursorPos.row),
col : this.cursorPos.col, col : this.cursorPos.col,
percent : Math.floor(((currentIndex / this.textLines.length) * 100)), percent : Math.floor(((currentIndex / this.textLines.length) * 100)),
below : this.getRemainingLinesBelowRow(), below : this.getRemainingLinesBelowRow(),
}; };
}; };

View File

@ -1,24 +1,24 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const msgArea = require('./message_area.js'); const msgArea = require('./message_area.js');
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const FileBaseFilters = require('./file_base_filter.js'); const FileBaseFilters = require('./file_base_filter.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const { getAvailableFileAreaTags } = require('./file_base_area.js'); const { getAvailableFileAreaTags } = require('./file_base_area.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'New Scan', name : 'New Scan',
desc : 'Performs a new scan against various areas of the system', desc : 'Performs a new scan against various areas of the system',
author : 'NuSkooler', author : 'NuSkooler',
}; };
/* /*
@ -30,15 +30,15 @@ exports.moduleInfo = {
*/ */
const MciCodeIds = { const MciCodeIds = {
ScanStatusLabel : 1, // TL1 ScanStatusLabel : 1, // TL1
ScanStatusList : 2, // VM2 (appends) ScanStatusList : 2, // VM2 (appends)
}; };
const Steps = { const Steps = {
MessageConfs : 'messageConferences', MessageConfs : 'messageConferences',
FileBase : 'fileBase', FileBase : 'fileBase',
Finished : 'finished', Finished : 'finished',
}; };
exports.getModule = class NewScanModule extends MenuModule { exports.getModule = class NewScanModule extends MenuModule {
@ -46,17 +46,17 @@ exports.getModule = class NewScanModule extends MenuModule {
super(options); super(options);
this.newScanFullExit = _.get(options, 'lastMenuResult.fullExit', false); this.newScanFullExit = _.get(options, 'lastMenuResult.fullExit', false);
this.currentStep = Steps.MessageConfs; this.currentStep = Steps.MessageConfs;
this.currentScanAux = {}; this.currentScanAux = {};
// :TODO: Make this conf/area specific: // :TODO: Make this conf/area specific:
const config = this.menuConfig.config; const config = this.menuConfig.config;
this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...';
this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new'; this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new';
this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found'; this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found';
this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan'; this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan';
} }
updateScanStatus(statusText) { updateScanStatus(statusText) {
@ -76,9 +76,9 @@ exports.getModule = class NewScanModule extends MenuModule {
}); });
// //
// Sort conferences by name, other than 'system_internal' which should // Sort conferences by name, other than 'system_internal' which should
// always come first such that we display private mails/etc. before // always come first such that we display private mails/etc. before
// other conferences & areas // other conferences & areas
// //
this.sortedMessageConfs.sort((a, b) => { this.sortedMessageConfs.sort((a, b) => {
if('system_internal' === a.confTag) { if('system_internal' === a.confTag) {
@ -110,23 +110,23 @@ exports.getModule = class NewScanModule extends MenuModule {
newScanMessageArea(conf, cb) { newScanMessageArea(conf, cb) {
// :TODO: it would be nice to cache this - must be done by conf! // :TODO: it would be nice to cache this - must be done by conf!
const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ); const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } );
const currentArea = sortedAreas[this.currentScanAux.area]; const currentArea = sortedAreas[this.currentScanAux.area];
// //
// Scan and update index until we find something. If results are found, // Scan and update index until we find something. If results are found,
// we'll goto the list module & show them. // we'll goto the list module & show them.
// //
const self = this; const self = this;
async.waterfall( async.waterfall(
[ [
function checkAndUpdateIndex(callback) { function checkAndUpdateIndex(callback) {
// Advance to next area if possible // Advance to next area if possible
if(sortedAreas.length >= self.currentScanAux.area + 1) { if(sortedAreas.length >= self.currentScanAux.area + 1) {
self.currentScanAux.area += 1; self.currentScanAux.area += 1;
return callback(null); return callback(null);
} else { } else {
self.updateScanStatus(self.scanCompleteMsg); self.updateScanStatus(self.scanCompleteMsg);
return callback(Errors.DoesNotExist('No more areas')); // this will stop our scan return callback(Errors.DoesNotExist('No more areas')); // this will stop our scan
} }
}, },
function updateStatusScanStarted(callback) { function updateStatusScanStarted(callback) {
@ -147,7 +147,7 @@ exports.getModule = class NewScanModule extends MenuModule {
}, },
function displayMessageList(newMessageCount) { function displayMessageList(newMessageCount) {
if(newMessageCount <= 0) { if(newMessageCount <= 0) {
return self.newScanMessageArea(conf, cb); // next area, if any return self.newScanMessageArea(conf, cb); // next area, if any
} }
const nextModuleOpts = { const nextModuleOpts = {
@ -166,11 +166,11 @@ exports.getModule = class NewScanModule extends MenuModule {
} }
newScanFileBase(cb) { newScanFileBase(cb) {
// :TODO: add in steps // :TODO: add in steps
const filterCriteria = { const filterCriteria = {
newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user), newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user),
areaTag : getAvailableFileAreaTags(this.client), areaTag : getAvailableFileAreaTags(this.client),
order : 'ascending', // oldest first order : 'ascending', // oldest first
}; };
FileEntry.findFiles( FileEntry.findFiles(
@ -195,14 +195,14 @@ exports.getModule = class NewScanModule extends MenuModule {
getSaveState() { getSaveState() {
return { return {
currentStep : this.currentStep, currentStep : this.currentStep,
currentScanAux : this.currentScanAux, currentScanAux : this.currentScanAux,
}; };
} }
restoreSavedState(savedState) { restoreSavedState(savedState) {
this.currentStep = savedState.currentStep; this.currentStep = savedState.currentStep;
this.currentScanAux = savedState.currentScanAux; this.currentScanAux = savedState.currentScanAux;
} }
performScanCurrentStep(cb) { performScanCurrentStep(cb) {
@ -227,7 +227,7 @@ exports.getModule = class NewScanModule extends MenuModule {
mciReady(mciData, cb) { mciReady(mciData, cb) {
if(this.newScanFullExit) { if(this.newScanFullExit) {
// user has canceled the entire scan @ message list view // user has canceled the entire scan @ message list view
return cb(null); return cb(null);
} }
@ -236,18 +236,18 @@ exports.getModule = class NewScanModule extends MenuModule {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
// :TODO: display scan step/etc. // :TODO: display scan step/etc.
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu, mciMap : mciData.menu,
noInput : true, noInput : true,
}; };
vc.loadFromMenuConfig(loadOpts, callback); vc.loadFromMenuConfig(loadOpts, callback);

View File

@ -1,24 +1,24 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const User = require('./user.js'); const User = require('./user.js');
const theme = require('./theme.js'); const theme = require('./theme.js');
const login = require('./system_menu_method.js').login; const login = require('./system_menu_method.js').login;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
exports.moduleInfo = { exports.moduleInfo = {
name : 'NUA', name : 'NUA',
desc : 'New User Application', desc : 'New User Application',
}; };
const MciViewIds = { const MciViewIds = {
userName : 1, userName : 1,
password : 9, password : 9,
confirm : 10, confirm : 10,
errMsg : 11, errMsg : 11,
}; };
exports.getModule = class NewUserAppModule extends MenuModule { exports.getModule = class NewUserAppModule extends MenuModule {
@ -30,7 +30,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
this.menuMethods = { this.menuMethods = {
// //
// Validation stuff // Validation stuff
// //
validatePassConfirmMatch : function(data, cb) { validatePassConfirmMatch : function(data, cb) {
const passwordView = self.viewControllers.menu.getView(MciViewIds.password); const passwordView = self.viewControllers.menu.getView(MciViewIds.password);
@ -58,7 +58,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
// //
// Submit handlers // Submit handlers
// //
submitApplication : function(formData, extraArgs, cb) { submitApplication : function(formData, extraArgs, cb) {
const newUser = new User(); const newUser = new User();
@ -67,33 +67,33 @@ exports.getModule = class NewUserAppModule extends MenuModule {
newUser.username = formData.value.username; newUser.username = formData.value.username;
// //
// We have to disable ACS checks for initial default areas as the user is not yet ready // We have to disable ACS checks for initial default areas as the user is not yet ready
// //
let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck
let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck
// can't store undefined! // can't store undefined!
confTag = confTag || ''; confTag = confTag || '';
areaTag = areaTag || ''; areaTag = areaTag || '';
newUser.properties = { newUser.properties = {
real_name : formData.value.realName, real_name : formData.value.realName,
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format
sex : formData.value.sex, sex : formData.value.sex,
location : formData.value.location, location : formData.value.location,
affiliation : formData.value.affils, affiliation : formData.value.affils,
email_address : formData.value.email, email_address : formData.value.email,
web_address : formData.value.web, web_address : formData.value.web,
account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format
message_conf_tag : confTag, message_conf_tag : confTag,
message_area_tag : areaTag, message_area_tag : areaTag,
term_height : self.client.term.termHeight, term_height : self.client.term.termHeight,
term_width : self.client.term.termWidth, term_width : self.client.term.termWidth,
// :TODO: Other defaults // :TODO: Other defaults
// :TODO: should probably have a place to create defaults/etc. // :TODO: should probably have a place to create defaults/etc.
}; };
if('*' === config.defaults.theme) { if('*' === config.defaults.theme) {
@ -102,7 +102,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
newUser.properties.theme_id = config.defaults.theme; newUser.properties.theme_id = config.defaults.theme;
} }
// :TODO: User.create() should validate email uniqueness! // :TODO: User.create() should validate email uniqueness!
newUser.create(formData.value.password, err => { newUser.create(formData.value.password, err => {
if(err) { if(err) {
self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed'); self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed');
@ -116,12 +116,12 @@ exports.getModule = class NewUserAppModule extends MenuModule {
} else { } else {
self.client.log.info( { username : formData.value.username, userId : newUser.userId }, 'New user created'); self.client.log.info( { username : formData.value.username, userId : newUser.userId }, 'New user created');
// Cache SysOp information now // Cache SysOp information now
// :TODO: Similar to bbs.js. DRY // :TODO: Similar to bbs.js. DRY
if(newUser.isSysOp()) { if(newUser.isSysOp()) {
config.general.sysOp = { config.general.sysOp = {
username : formData.value.username, username : formData.value.username,
properties : newUser.properties, properties : newUser.properties,
}; };
} }
@ -129,7 +129,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
return self.gotoMenu(extraArgs.inactive, cb); return self.gotoMenu(extraArgs.inactive, cb);
} else { } else {
// //
// If active now, we need to call login() to authenticate // If active now, we need to call login() to authenticate
// //
return login(self, formData, extraArgs, cb); return login(self, formData, extraArgs, cb);
} }

View File

@ -1,56 +1,56 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const { const {
getModDatabasePath, getModDatabasePath,
getTransactionDatabase getTransactionDatabase
} = require('./database.js'); } = require('./database.js');
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const theme = require('./theme.js'); const theme = require('./theme.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
// deps // deps
const sqlite3 = require('sqlite3'); const sqlite3 = require('sqlite3');
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
/* /*
Module :TODO: Module :TODO:
* Add pipe code support * Add pipe code support
- override max length & monitor *display* len as user types in order to allow for actual display len with color - override max length & monitor *display* len as user types in order to allow for actual display len with color
* Add preview control: Shows preview with pipe codes resolved * Add preview control: Shows preview with pipe codes resolved
* Add ability to at least alternate formatStrings -- every other * Add ability to at least alternate formatStrings -- every other
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'Onelinerz', name : 'Onelinerz',
desc : 'Standard local onelinerz', desc : 'Standard local onelinerz',
author : 'NuSkooler', author : 'NuSkooler',
packageName : 'codes.l33t.enigma.onelinerz', packageName : 'codes.l33t.enigma.onelinerz',
}; };
const MciViewIds = { const MciViewIds = {
ViewForm : { ViewForm : {
Entries : 1, Entries : 1,
AddPrompt : 2, AddPrompt : 2,
}, },
AddForm : { AddForm : {
NewEntry : 1, NewEntry : 1,
EntryPreview : 2, EntryPreview : 2,
AddPrompt : 3, AddPrompt : 3,
} }
}; };
const FormIds = { const FormIds = {
View : 0, View : 0,
Add : 1, Add : 1,
}; };
exports.getModule = class OnelinerzModule extends MenuModule { exports.getModule = class OnelinerzModule extends MenuModule {
@ -66,7 +66,7 @@ exports.getModule = class OnelinerzModule extends MenuModule {
addEntry : function(formData, extraArgs, cb) { addEntry : function(formData, extraArgs, cb) {
if(_.isString(formData.value.oneliner) && formData.value.oneliner.length > 0) { if(_.isString(formData.value.oneliner) && formData.value.oneliner.length > 0) {
const oneliner = formData.value.oneliner.trim(); // remove any trailing ws const oneliner = formData.value.oneliner.trim(); // remove any trailing ws
self.storeNewOneliner(oneliner, err => { self.storeNewOneliner(oneliner, err => {
if(err) { if(err) {
@ -74,18 +74,18 @@ exports.getModule = class OnelinerzModule extends MenuModule {
} }
self.clearAddForm(); self.clearAddForm();
return self.displayViewScreen(true, cb); // true=cls return self.displayViewScreen(true, cb); // true=cls
}); });
} else { } else {
// empty message - treat as if cancel was hit // empty message - treat as if cancel was hit
return self.displayViewScreen(true, cb); // true=cls return self.displayViewScreen(true, cb); // true=cls
} }
}, },
cancelAdd : function(formData, extraArgs, cb) { cancelAdd : function(formData, extraArgs, cb) {
self.clearAddForm(); self.clearAddForm();
return self.displayViewScreen(true, cb); // true=cls return self.displayViewScreen(true, cb); // true=cls
} }
}; };
} }
@ -103,7 +103,7 @@ exports.getModule = class OnelinerzModule extends MenuModule {
], ],
err => { err => {
if(err) { if(err) {
// :TODO: Handle me -- initSequence() should really take a completion callback // :TODO: Handle me -- initSequence() should really take a completion callback
} }
self.finishedLoading(); self.finishedLoading();
} }
@ -141,9 +141,9 @@ exports.getModule = class OnelinerzModule extends MenuModule {
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds.View, formId : FormIds.View,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -160,16 +160,16 @@ exports.getModule = class OnelinerzModule extends MenuModule {
self.db.each( self.db.each(
`SELECT * `SELECT *
FROM ( FROM (
SELECT * SELECT *
FROM onelinerz FROM onelinerz
ORDER BY timestamp DESC ORDER BY timestamp DESC
LIMIT ${limit} LIMIT ${limit}
) )
ORDER BY timestamp ASC;`, ORDER BY timestamp ASC;`,
(err, row) => { (err, row) => {
if(!err) { if(!err) {
row.timestamp = moment(row.timestamp); // convert -> moment row.timestamp = moment(row.timestamp); // convert -> moment
entries.push(row); entries.push(row);
} }
}, },
@ -179,15 +179,15 @@ exports.getModule = class OnelinerzModule extends MenuModule {
); );
}, },
function populateEntries(entriesView, entries, callback) { function populateEntries(entriesView, entries, callback) {
const listFormat = self.menuConfig.config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent const listFormat = self.menuConfig.config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent
const tsFormat = self.menuConfig.config.timestampFormat || 'ddd h:mma'; const tsFormat = self.menuConfig.config.timestampFormat || 'ddd h:mma';
entriesView.setItems(entries.map( e => { entriesView.setItems(entries.map( e => {
return stringFormat(listFormat, { return stringFormat(listFormat, {
userId : e.user_id, userId : e.user_id,
username : e.user_name, username : e.user_name,
oneliner : e.oneliner, oneliner : e.oneliner,
ts : e.timestamp.format(tsFormat), ts : e.timestamp.format(tsFormat),
} ); } );
})); }));
@ -197,7 +197,7 @@ exports.getModule = class OnelinerzModule extends MenuModule {
}, },
function finalPrep(callback) { function finalPrep(callback) {
const promptView = self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt); const promptView = self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt);
promptView.setFocusItemIndex(1); // default to NO promptView.setFocusItemIndex(1); // default to NO
return callback(null); return callback(null);
} }
], ],
@ -235,9 +235,9 @@ exports.getModule = class OnelinerzModule extends MenuModule {
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds.Add, formId : FormIds.Add,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -278,12 +278,12 @@ exports.getModule = class OnelinerzModule extends MenuModule {
function createTables(callback) { function createTables(callback) {
self.db.run( self.db.run(
`CREATE TABLE IF NOT EXISTS onelinerz ( `CREATE TABLE IF NOT EXISTS onelinerz (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
user_id INTEGER_NOT NULL, user_id INTEGER_NOT NULL,
user_name VARCHAR NOT NULL, user_name VARCHAR NOT NULL,
oneliner VARCHAR NOT NULL, oneliner VARCHAR NOT NULL,
timestamp DATETIME NOT NULL timestamp DATETIME NOT NULL
);` );`
, ,
err => { err => {
return callback(err); return callback(err);
@ -297,29 +297,29 @@ exports.getModule = class OnelinerzModule extends MenuModule {
} }
storeNewOneliner(oneliner, cb) { storeNewOneliner(oneliner, cb) {
const self = this; const self = this;
const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ');
async.series( async.series(
[ [
function addRec(callback) { function addRec(callback) {
self.db.run( self.db.run(
`INSERT INTO onelinerz (user_id, user_name, oneliner, timestamp) `INSERT INTO onelinerz (user_id, user_name, oneliner, timestamp)
VALUES (?, ?, ?, ?);`, VALUES (?, ?, ?, ?);`,
[ self.client.user.userId, self.client.user.username, oneliner, ts ], [ self.client.user.userId, self.client.user.username, oneliner, ts ],
callback callback
); );
}, },
function removeOld(callback) { function removeOld(callback) {
// keep 25 max most recent items - remove the older ones // keep 25 max most recent items - remove the older ones
self.db.run( self.db.run(
`DELETE FROM onelinerz `DELETE FROM onelinerz
WHERE id IN ( WHERE id IN (
SELECT id SELECT id
FROM onelinerz FROM onelinerz
ORDER BY id DESC ORDER BY id DESC
LIMIT -1 OFFSET 25 LIMIT -1 OFFSET 25
);`, );`,
callback callback
); );
} }

View File

@ -1,7 +1,7 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
exports.PluginModule = PluginModule; exports.PluginModule = PluginModule;
function PluginModule(/*options*/) { function PluginModule(/*options*/) {
} }

View File

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

View File

@ -1,42 +1,42 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const theme = require('./theme.js'); const theme = require('./theme.js');
const resetScreen = require('./ansi_term.js').resetScreen; const resetScreen = require('./ansi_term.js').resetScreen;
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const renderStringLength = require('./string_util.js').renderStringLength; const renderStringLength = require('./string_util.js').renderStringLength;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Rumorz', name : 'Rumorz',
desc : 'Standard local rumorz', desc : 'Standard local rumorz',
author : 'NuSkooler', author : 'NuSkooler',
packageName : 'codes.l33t.enigma.rumorz', packageName : 'codes.l33t.enigma.rumorz',
}; };
const STATLOG_KEY_RUMORZ = 'system_rumorz'; const STATLOG_KEY_RUMORZ = 'system_rumorz';
const FormIds = { const FormIds = {
View : 0, View : 0,
Add : 1, Add : 1,
}; };
const MciCodeIds = { const MciCodeIds = {
ViewForm : { ViewForm : {
Entries : 1, Entries : 1,
AddPrompt : 2, AddPrompt : 2,
}, },
AddForm : { AddForm : {
NewEntry : 1, NewEntry : 1,
EntryPreview : 2, EntryPreview : 2,
AddPrompt : 3, AddPrompt : 3,
} }
}; };
@ -51,21 +51,21 @@ exports.getModule = class RumorzModule extends MenuModule {
addEntry : (formData, extraArgs, cb) => { addEntry : (formData, extraArgs, cb) => {
if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) { if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) {
const rumor = formData.value.rumor.trim(); // remove any trailing ws const rumor = formData.value.rumor.trim(); // remove any trailing ws
StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => { StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => {
this.clearAddForm(); this.clearAddForm();
return this.displayViewScreen(true, cb); // true=cls return this.displayViewScreen(true, cb); // true=cls
}); });
} else { } else {
// empty message - treat as if cancel was hit // empty message - treat as if cancel was hit
return this.displayViewScreen(true, cb); // true=cls return this.displayViewScreen(true, cb); // true=cls
} }
}, },
cancelAdd : (formData, extraArgs, cb) => { cancelAdd : (formData, extraArgs, cb) => {
this.clearAddForm(); this.clearAddForm();
return this.displayViewScreen(true, cb); // true=cls return this.displayViewScreen(true, cb); // true=cls
} }
}; };
} }
@ -73,12 +73,12 @@ exports.getModule = class RumorzModule extends MenuModule {
get config() { return this.menuConfig.config; } get config() { return this.menuConfig.config; }
clearAddForm() { clearAddForm() {
const newEntryView = this.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); const newEntryView = this.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
newEntryView.setText(''); newEntryView.setText('');
// preview is optional // preview is optional
if(previewView) { if(previewView) {
previewView.setText(''); previewView.setText('');
} }
@ -98,7 +98,7 @@ exports.getModule = class RumorzModule extends MenuModule {
], ],
err => { err => {
if(err) { if(err) {
// :TODO: Handle me -- initSequence() should really take a completion callback // :TODO: Handle me -- initSequence() should really take a completion callback
} }
self.finishedLoading(); self.finishedLoading();
} }
@ -135,9 +135,9 @@ exports.getModule = class RumorzModule extends MenuModule {
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds.View, formId : FormIds.View,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -155,9 +155,9 @@ exports.getModule = class RumorzModule extends MenuModule {
}); });
}, },
function populateEntries(entriesView, entries, callback) { function populateEntries(entriesView, entries, callback) {
const config = self.config; const config = self.config;
const listFormat = config.listFormat || '{rumor}'; const listFormat = config.listFormat || '{rumor}';
const focusListFormat = config.focusListFormat || listFormat; const focusListFormat = config.focusListFormat || listFormat;
entriesView.setItems(entries.map( e => stringFormat(listFormat, { rumor : e.log_value } ) ) ); entriesView.setItems(entries.map( e => stringFormat(listFormat, { rumor : e.log_value } ) ) );
entriesView.setFocusItems(entries.map(e => stringFormat(focusListFormat, { rumor : e.log_value } ) ) ); entriesView.setFocusItems(entries.map(e => stringFormat(focusListFormat, { rumor : e.log_value } ) ) );
@ -167,7 +167,7 @@ exports.getModule = class RumorzModule extends MenuModule {
}, },
function finalPrep(callback) { function finalPrep(callback) {
const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt); const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt);
promptView.setFocusItemIndex(1); // default to NO promptView.setFocusItemIndex(1); // default to NO
return callback(null); return callback(null);
} }
], ],
@ -205,9 +205,9 @@ exports.getModule = class RumorzModule extends MenuModule {
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : artData.mciMap, mciMap : artData.mciMap,
formId : FormIds.Add, formId : FormIds.Add,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -219,8 +219,8 @@ exports.getModule = class RumorzModule extends MenuModule {
} }
}, },
function initPreviewUpdates(callback) { function initPreviewUpdates(callback) {
const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
if(previewView) { if(previewView) {
let timerId; let timerId;
entryView.on('key press', () => { entryView.on('key press', () => {

View File

@ -1,28 +1,28 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
// deps // deps
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const { Parser } = require('binary-parser'); const { Parser } = require('binary-parser');
exports.readSAUCE = readSAUCE; exports.readSAUCE = readSAUCE;
const SAUCE_SIZE = 128; const SAUCE_SIZE = 128;
const SAUCE_ID = Buffer.from([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE' const SAUCE_ID = Buffer.from([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE'
// :TODO read comments // :TODO read comments
//const COMNT_ID = Buffer.from([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' //const COMNT_ID = Buffer.from([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT'
exports.SAUCE_SIZE = SAUCE_SIZE; exports.SAUCE_SIZE = SAUCE_SIZE;
// :TODO: SAUCE should be a class // :TODO: SAUCE should be a class
// - with getFontName() // - with getFontName()
// - ...other methods // - ...other methods
// //
// See // See
// http://www.acid.org/info/sauce/sauce.htm // http://www.acid.org/info/sauce/sauce.htm
// //
const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ]; const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ];
@ -49,8 +49,8 @@ function readSAUCE(data, cb) {
.uint16le('tinfo4') .uint16le('tinfo4')
.int8('numComments') .int8('numComments')
.int8('flags') .int8('flags')
// :TODO: does this need to be optional? // :TODO: does this need to be optional?
.buffer('tinfos', { length: 22 } ) // SAUCE 00.5 .buffer('tinfos', { length: 22 } ) // SAUCE 00.5
.parse(data.slice(data.length - SAUCE_SIZE)); .parse(data.slice(data.length - SAUCE_SIZE));
} catch(e) { } catch(e) {
return cb(Errors.Invalid('Invalid SAUCE record')); return cb(Errors.Invalid('Invalid SAUCE record'));
@ -72,22 +72,22 @@ function readSAUCE(data, cb) {
} }
const sauce = { const sauce = {
id : iconv.decode(sauceRec.id, 'cp437'), id : iconv.decode(sauceRec.id, 'cp437'),
version : iconv.decode(sauceRec.version, 'cp437').trim(), version : iconv.decode(sauceRec.version, 'cp437').trim(),
title : iconv.decode(sauceRec.title, 'cp437').trim(), title : iconv.decode(sauceRec.title, 'cp437').trim(),
author : iconv.decode(sauceRec.author, 'cp437').trim(), author : iconv.decode(sauceRec.author, 'cp437').trim(),
group : iconv.decode(sauceRec.group, 'cp437').trim(), group : iconv.decode(sauceRec.group, 'cp437').trim(),
date : iconv.decode(sauceRec.date, 'cp437').trim(), date : iconv.decode(sauceRec.date, 'cp437').trim(),
fileSize : sauceRec.fileSize, fileSize : sauceRec.fileSize,
dataType : sauceRec.dataType, dataType : sauceRec.dataType,
fileType : sauceRec.fileType, fileType : sauceRec.fileType,
tinfo1 : sauceRec.tinfo1, tinfo1 : sauceRec.tinfo1,
tinfo2 : sauceRec.tinfo2, tinfo2 : sauceRec.tinfo2,
tinfo3 : sauceRec.tinfo3, tinfo3 : sauceRec.tinfo3,
tinfo4 : sauceRec.tinfo4, tinfo4 : sauceRec.tinfo4,
numComments : sauceRec.numComments, numComments : sauceRec.numComments,
flags : sauceRec.flags, flags : sauceRec.flags,
tinfos : sauceRec.tinfos, tinfos : sauceRec.tinfos,
}; };
const dt = SAUCE_DATA_TYPES[sauce.dataType]; const dt = SAUCE_DATA_TYPES[sauce.dataType];
@ -98,51 +98,51 @@ function readSAUCE(data, cb) {
return cb(null, sauce); return cb(null, sauce);
} }
// :TODO: These need completed: // :TODO: These need completed:
const SAUCE_DATA_TYPES = { const SAUCE_DATA_TYPES = {
0 : { name : 'None' }, 0 : { name : 'None' },
1 : { name : 'Character', parser : parseCharacterSAUCE }, 1 : { name : 'Character', parser : parseCharacterSAUCE },
2 : 'Bitmap', 2 : 'Bitmap',
3 : 'Vector', 3 : 'Vector',
4 : 'Audio', 4 : 'Audio',
5 : 'BinaryText', 5 : 'BinaryText',
6 : 'XBin', 6 : 'XBin',
7 : 'Archive', 7 : 'Archive',
8 : 'Executable', 8 : 'Executable',
}; };
const SAUCE_CHARACTER_FILE_TYPES = { const SAUCE_CHARACTER_FILE_TYPES = {
0 : 'ASCII', 0 : 'ASCII',
1 : 'ANSi', 1 : 'ANSi',
2 : 'ANSiMation', 2 : 'ANSiMation',
3 : 'RIP script', 3 : 'RIP script',
4 : 'PCBoard', 4 : 'PCBoard',
5 : 'Avatar', 5 : 'Avatar',
6 : 'HTML', 6 : 'HTML',
7 : 'Source', 7 : 'Source',
8 : 'TundraDraw', 8 : 'TundraDraw',
}; };
// //
// Map of SAUCE font -> encoding hint // Map of SAUCE font -> encoding hint
// //
// Note that this is the same mapping that x84 uses. Be compatible! // Note that this is the same mapping that x84 uses. Be compatible!
// //
const SAUCE_FONT_TO_ENCODING_HINT = { const SAUCE_FONT_TO_ENCODING_HINT = {
'Amiga MicroKnight' : 'amiga', 'Amiga MicroKnight' : 'amiga',
'Amiga MicroKnight+' : 'amiga', 'Amiga MicroKnight+' : 'amiga',
'Amiga mOsOul' : 'amiga', 'Amiga mOsOul' : 'amiga',
'Amiga P0T-NOoDLE' : 'amiga', 'Amiga P0T-NOoDLE' : 'amiga',
'Amiga Topaz 1' : 'amiga', 'Amiga Topaz 1' : 'amiga',
'Amiga Topaz 1+' : 'amiga', 'Amiga Topaz 1+' : 'amiga',
'Amiga Topaz 2' : 'amiga', 'Amiga Topaz 2' : 'amiga',
'Amiga Topaz 2+' : 'amiga', 'Amiga Topaz 2+' : 'amiga',
'Atari ATASCII' : 'atari', 'Atari ATASCII' : 'atari',
'IBM EGA43' : 'cp437', 'IBM EGA43' : 'cp437',
'IBM EGA' : 'cp437', 'IBM EGA' : 'cp437',
'IBM VGA25G' : 'cp437', 'IBM VGA25G' : 'cp437',
'IBM VGA50' : 'cp437', 'IBM VGA50' : 'cp437',
'IBM VGA' : 'cp437', 'IBM VGA' : 'cp437',
}; };
[ [
@ -150,20 +150,20 @@ const SAUCE_FONT_TO_ENCODING_HINT = {
'860', '861', '862', '863', '864', '865', '866', '869', '872' '860', '861', '862', '863', '864', '865', '866', '869', '872'
].forEach( page => { ].forEach( page => {
const codec = 'cp' + page; const codec = 'cp' + page;
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec;
}); });
function parseCharacterSAUCE(sauce) { function parseCharacterSAUCE(sauce) {
const result = {}; const result = {};
result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown'; result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown';
if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) { if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) {
// convience: create ansiFlags // convience: create ansiFlags
sauce.ansiFlags = sauce.flags; sauce.ansiFlags = sauce.flags;
let i = 0; let i = 0;

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var PluginModule = require('./plugin_module.js').PluginModule; var PluginModule = require('./plugin_module.js').PluginModule;
exports.ServerModule = ServerModule; exports.ServerModule = ServerModule;
function ServerModule() { function ServerModule() {
PluginModule.call(this); PluginModule.call(this);

View File

@ -1,62 +1,62 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Log = require('../../logger.js').log; const Log = require('../../logger.js').log;
const { ServerModule } = require('../../server_module.js'); const { ServerModule } = require('../../server_module.js');
const Config = require('../../config.js').get; const Config = require('../../config.js').get;
const { const {
splitTextAtTerms, splitTextAtTerms,
isAnsi, isAnsi,
cleanControlCodes cleanControlCodes
} = require('../../string_util.js'); } = require('../../string_util.js');
const { const {
getMessageConferenceByTag, getMessageConferenceByTag,
getMessageAreaByTag, getMessageAreaByTag,
getMessageListForArea, getMessageListForArea,
} = require('../../message_area.js'); } = require('../../message_area.js');
const { sortAreasOrConfs } = require('../../conf_area_util.js'); const { sortAreasOrConfs } = require('../../conf_area_util.js');
const AnsiPrep = require('../../ansi_prep.js'); const AnsiPrep = require('../../ansi_prep.js');
// deps // deps
const net = require('net'); const net = require('net');
const _ = require('lodash'); const _ = require('lodash');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
const moment = require('moment'); const moment = require('moment');
const ModuleInfo = exports.moduleInfo = { const ModuleInfo = exports.moduleInfo = {
name : 'Gopher', name : 'Gopher',
desc : 'Gopher Server', desc : 'Gopher Server',
author : 'NuSkooler', author : 'NuSkooler',
packageName : 'codes.l33t.enigma.gopher.server', packageName : 'codes.l33t.enigma.gopher.server',
}; };
const Message = require('../../message.js'); const Message = require('../../message.js');
const ItemTypes = { const ItemTypes = {
Invalid : '', // not really a type, of course! Invalid : '', // not really a type, of course!
// Canonical, RFC-1436 // Canonical, RFC-1436
TextFile : '0', TextFile : '0',
SubMenu : '1', SubMenu : '1',
CCSONameserver : '2', CCSONameserver : '2',
Error : '3', Error : '3',
BinHexFile : '4', BinHexFile : '4',
DOSFile : '5', DOSFile : '5',
UuEncodedFile : '6', UuEncodedFile : '6',
FullTextSearch : '7', FullTextSearch : '7',
Telnet : '8', Telnet : '8',
BinaryFile : '9', BinaryFile : '9',
AltServer : '+', AltServer : '+',
GIFFile : 'g', GIFFile : 'g',
ImageFile : 'I', ImageFile : 'I',
Telnet3270 : 'T', Telnet3270 : 'T',
// Non-canonical // Non-canonical
HtmlFile : 'h', HtmlFile : 'h',
InfoMessage : 'i', InfoMessage : 'i',
SoundFile : 's', SoundFile : 's',
}; };
exports.getModule = class GopherModule extends ServerModule { exports.getModule = class GopherModule extends ServerModule {
@ -64,7 +64,7 @@ exports.getModule = class GopherModule extends ServerModule {
constructor() { constructor() {
super(); super();
this.routes = new Map(); // selector->generator => gopher item this.routes = new Map(); // selector->generator => gopher item
this.log = Log.child( { server : 'Gopher' } ); this.log = Log.child( { server : 'Gopher' } );
} }
@ -75,7 +75,7 @@ exports.getModule = class GopherModule extends ServerModule {
const config = Config(); const config = Config();
this.publicHostname = config.contentServers.gopher.publicHostname; this.publicHostname = config.contentServers.gopher.publicHostname;
this.publicPort = config.contentServers.gopher.publicPort; this.publicPort = config.contentServers.gopher.publicPort;
this.addRoute(/^\/?\r\n$/, this.defaultGenerator); this.addRoute(/^\/?\r\n$/, this.defaultGenerator);
this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator); this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator);
@ -88,7 +88,7 @@ exports.getModule = class GopherModule extends ServerModule {
}); });
socket.on('error', err => { socket.on('error', err => {
if('ECONNRESET' !== err.code) { // normal if('ECONNRESET' !== err.code) { // normal
this.log.trace( { error : err.message }, 'Socket error'); this.log.trace( { error : err.message }, 'Socket error');
} }
}); });
@ -97,7 +97,7 @@ exports.getModule = class GopherModule extends ServerModule {
listen() { listen() {
if(!this.enabled) { if(!this.enabled) {
return true; // nothing to do, but not an error return true; // nothing to do, but not an error
} }
const config = Config(); const config = Config();
@ -115,10 +115,10 @@ exports.getModule = class GopherModule extends ServerModule {
} }
isConfigured() { isConfigured() {
// public hostname & port must be set; responses contain them! // public hostname & port must be set; responses contain them!
const config = Config(); const config = Config();
return _.isString(_.get(config, 'contentServers.gopher.publicHostname')) && return _.isString(_.get(config, 'contentServers.gopher.publicHostname')) &&
_.isNumber(_.get(config, 'contentServers.gopher.publicPort')); _.isNumber(_.get(config, 'contentServers.gopher.publicPort'));
} }
addRoute(selectorRegExp, generatorHandler) { addRoute(selectorRegExp, generatorHandler) {
@ -149,7 +149,7 @@ exports.getModule = class GopherModule extends ServerModule {
} }
makeItem(itemType, text, selector, hostname, port) { makeItem(itemType, text, selector, hostname, port) {
selector = selector || ''; // e.g. for info selector = selector || ''; // e.g. for info
hostname = hostname || this.publicHostname; hostname = hostname || this.publicHostname;
port = port || this.publicPort; port = port || this.publicPort;
return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`; return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`;
@ -186,10 +186,10 @@ exports.getModule = class GopherModule extends ServerModule {
AnsiPrep( AnsiPrep(
body, body,
{ {
cols : 79, // Gopher std. wants 70, but we'll have to deal with it. cols : 79, // Gopher std. wants 70, but we'll have to deal with it.
forceLineTerm : true, // ensure each line is term'd forceLineTerm : true, // ensure each line is term'd
asciiMode : true, // export to ASCII asciiMode : true, // export to ASCII
fillLines : false, // don't fill up to |cols| fillLines : false, // don't fill up to |cols|
}, },
(err, prepped) => { (err, prepped) => {
return cb(prepped || body); return cb(prepped || body);
@ -207,21 +207,21 @@ exports.getModule = class GopherModule extends ServerModule {
messageAreaGenerator(selectorMatch, cb) { messageAreaGenerator(selectorMatch, cb) {
this.log.trace( { selector : selectorMatch[0] }, 'Serving message area content'); this.log.trace( { selector : selectorMatch[0] }, 'Serving message area content');
// //
// Selector should be: // Selector should be:
// /msgarea - list confs // /msgarea - list confs
// /msgarea/conftag - list areas in conf // /msgarea/conftag - list areas in conf
// /msgarea/conftag/areatag - list messages in area // /msgarea/conftag/areatag - list messages in area
// /msgarea/conftag/areatag/<UUID> - message as text // /msgarea/conftag/areatag/<UUID> - message as text
// /msgarea/conftag/areatag/<UUID>_raw - full message as text + headers // /msgarea/conftag/areatag/<UUID>_raw - full message as text + headers
// //
if(selectorMatch[3] || selectorMatch[4]) { if(selectorMatch[3] || selectorMatch[4]) {
// message // message
//const raw = selectorMatch[4] ? true : false; //const raw = selectorMatch[4] ? true : false;
// :TODO: support 'raw' // :TODO: support 'raw'
const msgUuid = selectorMatch[3].replace(/\r\n|\//g, ''); const msgUuid = selectorMatch[3].replace(/\r\n|\//g, '');
const confTag = selectorMatch[1].substr(1).split('/')[0]; const confTag = selectorMatch[1].substr(1).split('/')[0];
const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
const message = new Message(); const message = new Message();
return message.load( { uuid : msgUuid }, err => { return message.load( { uuid : msgUuid }, err => {
if(err) { if(err) {
@ -248,15 +248,15 @@ Subject: ${message.subject}
ID : ${message.messageUuid} (${message.messageId}) ID : ${message.messageUuid} (${message.messageId})
${'-'.repeat(70)} ${'-'.repeat(70)}
${msgBody} ${msgBody}
`; `;
return cb(response); return cb(response);
}); });
}); });
} else if(selectorMatch[2]) { } else if(selectorMatch[2]) {
// list messages in area // list messages in area
const confTag = selectorMatch[1].substr(1).split('/')[0]; const confTag = selectorMatch[1].substr(1).split('/')[0];
const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
const area = getMessageAreaByTag(areaTag); const area = getMessageAreaByTag(areaTag);
if(Message.isPrivateAreaTag(areaTag)) { if(Message.isPrivateAreaTag(areaTag)) {
this.log.warn( { areaTag }, 'Attempted access to private area!'); this.log.warn( { areaTag }, 'Attempted access to private area!');
@ -283,10 +283,10 @@ ${msgBody}
return cb(response); return cb(response);
}); });
} else if(selectorMatch[1]) { } else if(selectorMatch[1]) {
// list areas in conf // list areas in conf
const sysConfig = Config(); const sysConfig = Config();
const confTag = selectorMatch[1].replace(/\r\n|\//g, ''); const confTag = selectorMatch[1].replace(/\r\n|\//g, '');
const conf = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ]) && getMessageConferenceByTag(confTag); const conf = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ]) && getMessageConferenceByTag(confTag);
if(!conf) { if(!conf) {
return this.notFoundGenerator(selectorMatch, cb); return this.notFoundGenerator(selectorMatch, cb);
} }
@ -310,10 +310,10 @@ ${msgBody}
return cb(response); return cb(response);
} else { } else {
// message area base (list confs) // message area base (list confs)
const confs = Object.keys(_.get(Config(), 'contentServers.gopher.messageConferences', {})) const confs = Object.keys(_.get(Config(), 'contentServers.gopher.messageConferences', {}))
.map(confTag => Object.assign( { confTag }, getMessageConferenceByTag(confTag))) .map(confTag => Object.assign( { confTag }, getMessageConferenceByTag(confTag)))
.filter(conf => conf); // remove any baddies .filter(conf => conf); // remove any baddies
if(0 === confs.length) { if(0 === confs.length) {
return cb(this.makeItem(ItemTypes.InfoMessage, 'No message conferences available')); return cb(this.makeItem(ItemTypes.InfoMessage, 'No message conferences available'));

View File

@ -1,24 +1,24 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Log = require('../../logger.js').log; const Log = require('../../logger.js').log;
const ServerModule = require('../../server_module.js').ServerModule; const ServerModule = require('../../server_module.js').ServerModule;
const Config = require('../../config.js').get; const Config = require('../../config.js').get;
// deps // deps
const http = require('http'); const http = require('http');
const https = require('https'); const https = require('https');
const _ = require('lodash'); const _ = require('lodash');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
const mimeTypes = require('mime-types'); const mimeTypes = require('mime-types');
const ModuleInfo = exports.moduleInfo = { const ModuleInfo = exports.moduleInfo = {
name : 'Web', name : 'Web',
desc : 'Web Server', desc : 'Web Server',
author : 'NuSkooler', author : 'NuSkooler',
packageName : 'codes.l33t.enigma.web.server', packageName : 'codes.l33t.enigma.web.server',
}; };
class Route { class Route {
@ -39,8 +39,8 @@ class Route {
isValid() { isValid() {
return ( return (
this.pathRegExp instanceof RegExp && this.pathRegExp instanceof RegExp &&
( -1 !== [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', ].indexOf(this.method) ) || ( -1 !== [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', ].indexOf(this.method) ) ||
!_.isFunction(this.handler) !_.isFunction(this.handler)
); );
} }
@ -55,28 +55,28 @@ exports.getModule = class WebServerModule extends ServerModule {
constructor() { constructor() {
super(); super();
const config = Config(); const config = Config();
this.enableHttp = config.contentServers.web.http.enabled || false; this.enableHttp = config.contentServers.web.http.enabled || false;
this.enableHttps = config.contentServers.web.https.enabled || false; this.enableHttps = config.contentServers.web.https.enabled || false;
this.routes = {}; this.routes = {};
if(this.isEnabled() && config.contentServers.web.staticRoot) { if(this.isEnabled() && config.contentServers.web.staticRoot) {
this.addRoute({ this.addRoute({
method : 'GET', method : 'GET',
path : '/static/.*$', path : '/static/.*$',
handler : this.routeStaticFile.bind(this), handler : this.routeStaticFile.bind(this),
}); });
} }
} }
buildUrl(pathAndQuery) { buildUrl(pathAndQuery) {
// //
// Create a URL such as // Create a URL such as
// https://l33t.codes:44512/ + |pathAndQuery| // https://l33t.codes:44512/ + |pathAndQuery|
// //
// Prefer HTTPS over HTTP. Be explicit about the port // Prefer HTTPS over HTTP. Be explicit about the port
// only if non-standard. Allow users to override full prefix in config. // only if non-standard. Allow users to override full prefix in config.
// //
const config = Config(); const config = Config();
if(_.isString(config.contentServers.web.overrideUrlPrefix)) { if(_.isString(config.contentServers.web.overrideUrlPrefix)) {
@ -86,13 +86,13 @@ exports.getModule = class WebServerModule extends ServerModule {
let schema; let schema;
let port; let port;
if(config.contentServers.web.https.enabled) { if(config.contentServers.web.https.enabled) {
schema = 'https://'; schema = 'https://';
port = (443 === config.contentServers.web.https.port) ? port = (443 === config.contentServers.web.https.port) ?
'' : '' :
`:${config.contentServers.web.https.port}`; `:${config.contentServers.web.https.port}`;
} else { } else {
schema = 'http://'; schema = 'http://';
port = (80 === config.contentServers.web.http.port) ? port = (80 === config.contentServers.web.http.port) ?
'' : '' :
`:${config.contentServers.web.http.port}`; `:${config.contentServers.web.http.port}`;
} }
@ -112,11 +112,11 @@ exports.getModule = class WebServerModule extends ServerModule {
const config = Config(); const config = Config();
if(this.enableHttps) { if(this.enableHttps) {
const options = { const options = {
cert : fs.readFileSync(config.contentServers.web.https.certPem), cert : fs.readFileSync(config.contentServers.web.https.certPem),
key : fs.readFileSync(config.contentServers.web.https.keyPem), key : fs.readFileSync(config.contentServers.web.https.keyPem),
}; };
// additional options // additional options
Object.assign(options, config.contentServers.web.https.options || {} ); Object.assign(options, config.contentServers.web.https.options || {} );
this.httpsServer = https.createServer(options, (req, resp) => this.routeRequest(req, resp) ); this.httpsServer = https.createServer(options, (req, resp) => this.routeRequest(req, resp) );
@ -178,18 +178,18 @@ exports.getModule = class WebServerModule extends ServerModule {
if(err) { if(err) {
return resp.end(`<!doctype html> return resp.end(`<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>${title}</title> <title>${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
</head> </head>
<body> <body>
<article> <article>
<h2>${bodyText}</h2> <h2>${bodyText}</h2>
</article> </article>
</body> </body>
</html>` </html>`
); );
} }
@ -227,8 +227,8 @@ exports.getModule = class WebServerModule extends ServerModule {
} }
const headers = { const headers = {
'Content-Type' : mimeTypes.contentType(paths.basename(filePath)) || mimeTypes.contentType('.bin'), 'Content-Type' : mimeTypes.contentType(paths.basename(filePath)) || mimeTypes.contentType('.bin'),
'Content-Length' : stats.size, 'Content-Length' : stats.size,
}; };
const readStream = fs.createReadStream(filePath); const readStream = fs.createReadStream(filePath);
@ -251,8 +251,8 @@ exports.getModule = class WebServerModule extends ServerModule {
} }
const headers = { const headers = {
'Content-Type' : contentType || mimeTypes.contentType('.html'), 'Content-Type' : contentType || mimeTypes.contentType('.html'),
'Content-Length' : finalPage.length, 'Content-Length' : finalPage.length,
}; };
resp.writeHead(200, headers); resp.writeHead(200, headers);

View File

@ -1,37 +1,37 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('../../config.js').get; const Config = require('../../config.js').get;
const baseClient = require('../../client.js'); const baseClient = require('../../client.js');
const Log = require('../../logger.js').log; const Log = require('../../logger.js').log;
const LoginServerModule = require('../../login_server_module.js'); const LoginServerModule = require('../../login_server_module.js');
const userLogin = require('../../user_login.js').userLogin; const userLogin = require('../../user_login.js').userLogin;
const enigVersion = require('../../../package.json').version; const enigVersion = require('../../../package.json').version;
const theme = require('../../theme.js'); const theme = require('../../theme.js');
const stringFormat = require('../../string_format.js'); const stringFormat = require('../../string_format.js');
// deps // deps
const ssh2 = require('ssh2'); const ssh2 = require('ssh2');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const util = require('util'); const util = require('util');
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
const ModuleInfo = exports.moduleInfo = { const ModuleInfo = exports.moduleInfo = {
name : 'SSH', name : 'SSH',
desc : 'SSH Server', desc : 'SSH Server',
author : 'NuSkooler', author : 'NuSkooler',
isSecure : true, isSecure : true,
packageName : 'codes.l33t.enigma.ssh.server', packageName : 'codes.l33t.enigma.ssh.server',
}; };
function SSHClient(clientConn) { function SSHClient(clientConn) {
baseClient.Client.apply(this, arguments); baseClient.Client.apply(this, arguments);
// //
// WARNING: Until we have emit 'ready', self.input, and self.output and // WARNING: Until we have emit 'ready', self.input, and self.output and
// not yet defined! // not yet defined!
// //
const self = this; const self = this;
@ -39,11 +39,11 @@ function SSHClient(clientConn) {
let loginAttempts = 0; let loginAttempts = 0;
clientConn.on('authentication', function authAttempt(ctx) { clientConn.on('authentication', function authAttempt(ctx) {
const username = ctx.username || ''; const username = ctx.username || '';
const password = ctx.password || ''; const password = ctx.password || '';
const config = Config(); const config = Config();
self.isNewUser = (config.users.newUserNames || []).indexOf(username) > -1; self.isNewUser = (config.users.newUserNames || []).indexOf(username) > -1;
self.log.trace( { method : ctx.method, username : username, newUser : self.isNewUser }, 'SSH authentication attempt'); self.log.trace( { method : ctx.method, username : username, newUser : self.isNewUser }, 'SSH authentication attempt');
@ -58,8 +58,8 @@ function SSHClient(clientConn) {
} }
// //
// If the system is open and |isNewUser| is true, the login // If the system is open and |isNewUser| is true, the login
// sequence is hijacked in order to start the applicaiton process. // sequence is hijacked in order to start the applicaiton process.
// //
if(false === config.general.closedSystem && self.isNewUser) { if(false === config.general.closedSystem && self.isNewUser) {
return ctx.accept(); return ctx.accept();
@ -85,7 +85,7 @@ function SSHClient(clientConn) {
} }
if(0 === username.length) { if(0 === username.length) {
// :TODO: can we display something here? // :TODO: can we display something here?
return ctx.reject(); return ctx.reject();
} }
@ -105,9 +105,9 @@ function SSHClient(clientConn) {
} }
const artOpts = { const artOpts = {
client : self, client : self,
name : 'SSHPMPT.ASC', name : 'SSHPMPT.ASC',
readSauce : false, readSauce : false,
}; };
theme.getThemeArt(artOpts, (err, artInfo) => { theme.getThemeArt(artOpts, (err, artInfo) => {
@ -136,31 +136,31 @@ function SSHClient(clientConn) {
this.updateTermInfo = function(info) { this.updateTermInfo = function(info) {
// //
// From ssh2 docs: // From ssh2 docs:
// "rows and cols override width and height when rows and cols are non-zero." // "rows and cols override width and height when rows and cols are non-zero."
// //
let termHeight; let termHeight;
let termWidth; let termWidth;
if(info.rows > 0 && info.cols > 0) { if(info.rows > 0 && info.cols > 0) {
termHeight = info.rows; termHeight = info.rows;
termWidth = info.cols; termWidth = info.cols;
} else if(info.width > 0 && info.height > 0) { } else if(info.width > 0 && info.height > 0) {
termHeight = info.height; termHeight = info.height;
termWidth = info.width; termWidth = info.width;
} }
assert(_.isObject(self.term)); assert(_.isObject(self.term));
// //
// Note that if we fail here, connect.js attempts some non-standard // Note that if we fail here, connect.js attempts some non-standard
// queries/etc., and ultimately will default to 80x24 if all else fails // queries/etc., and ultimately will default to 80x24 if all else fails
// //
if(termHeight > 0 && termWidth > 0) { if(termHeight > 0 && termWidth > 0) {
self.term.termHeight = termHeight; self.term.termHeight = termHeight;
self.term.termWidth = termWidth; self.term.termWidth = termWidth;
self.clearMciCache(); // term size changes = invalidate cache self.clearMciCache(); // term size changes = invalidate cache
} }
if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) { if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) {
@ -182,7 +182,7 @@ function SSHClient(clientConn) {
accept(); accept();
} }
if(self.input) { // do we have I/O? if(self.input) { // do we have I/O?
self.updateTermInfo(info); self.updateTermInfo(info);
} else { } else {
self.cachedTermInfo = info; self.cachedTermInfo = info;
@ -203,7 +203,7 @@ function SSHClient(clientConn) {
delete self.cachedTermInfo; delete self.cachedTermInfo;
} }
// we're ready! // we're ready!
const firstMenu = self.isNewUser ? Config().loginServers.ssh.firstMenuNewUser : Config().loginServers.ssh.firstMenu; const firstMenu = self.isNewUser ? Config().loginServers.ssh.firstMenuNewUser : Config().loginServers.ssh.firstMenu;
self.emit('ready', { firstMenu : firstMenu } ); self.emit('ready', { firstMenu : firstMenu } );
}); });
@ -222,7 +222,7 @@ function SSHClient(clientConn) {
}); });
clientConn.on('end', () => { clientConn.on('end', () => {
self.emit('end'); // remove client connection/tracking self.emit('end'); // remove client connection/tracking
}); });
clientConn.on('error', err => { clientConn.on('error', err => {
@ -244,13 +244,13 @@ exports.getModule = class SSHServerModule extends LoginServerModule {
const serverConf = { const serverConf = {
hostKeys : [ hostKeys : [
{ {
key : fs.readFileSync(config.loginServers.ssh.privateKeyPem), key : fs.readFileSync(config.loginServers.ssh.privateKeyPem),
passphrase : config.loginServers.ssh.privateKeyPass, passphrase : config.loginServers.ssh.privateKeyPass,
} }
], ],
ident : 'enigma-bbs-' + enigVersion + '-srv', ident : 'enigma-bbs-' + enigVersion + '-srv',
// Note that sending 'banner' breaks at least EtherTerm! // Note that sending 'banner' breaks at least EtherTerm!
debug : (sshDebugLine) => { debug : (sshDebugLine) => {
if(true === config.loginServers.ssh.traceConnections) { if(true === config.loginServers.ssh.traceConnections) {
Log.trace(`SSH: ${sshDebugLine}`); Log.trace(`SSH: ${sshDebugLine}`);

View File

@ -1,166 +1,166 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const baseClient = require('../../client.js'); const baseClient = require('../../client.js');
const Log = require('../../logger.js').log; const Log = require('../../logger.js').log;
const LoginServerModule = require('../../login_server_module.js'); const LoginServerModule = require('../../login_server_module.js');
const Config = require('../../config.js').get; const Config = require('../../config.js').get;
const EnigAssert = require('../../enigma_assert.js'); const EnigAssert = require('../../enigma_assert.js');
const { stringFromNullTermBuffer } = require('../../string_util.js'); const { stringFromNullTermBuffer } = require('../../string_util.js');
// deps // deps
const net = require('net'); const net = require('net');
const buffers = require('buffers'); const buffers = require('buffers');
const { Parser } = require('binary-parser'); const { Parser } = require('binary-parser');
const util = require('util'); const util = require('util');
//var debug = require('debug')('telnet'); //var debug = require('debug')('telnet');
const ModuleInfo = exports.moduleInfo = { const ModuleInfo = exports.moduleInfo = {
name : 'Telnet', name : 'Telnet',
desc : 'Telnet Server', desc : 'Telnet Server',
author : 'NuSkooler', author : 'NuSkooler',
isSecure : false, isSecure : false,
packageName : 'codes.l33t.enigma.telnet.server', packageName : 'codes.l33t.enigma.telnet.server',
}; };
exports.TelnetClient = TelnetClient; exports.TelnetClient = TelnetClient;
// //
// Telnet Protocol Resources // Telnet Protocol Resources
// * http://pcmicro.com/netfoss/telnet.html // * http://pcmicro.com/netfoss/telnet.html
// * http://mud-dev.wikidot.com/telnet:negotiation // * http://mud-dev.wikidot.com/telnet:negotiation
// //
/* /*
TODO: TODO:
* Document COMMANDS -- add any missing * Document COMMANDS -- add any missing
* Document OPTIONS -- add any missing * Document OPTIONS -- add any missing
* Internally handle OPTIONS: * Internally handle OPTIONS:
* Some should be emitted generically * Some should be emitted generically
* Some should be handled internally -- denied, handled, etc. * Some should be handled internally -- denied, handled, etc.
* *
* Allow term (ttype) to be set by environ sub negotiation * Allow term (ttype) to be set by environ sub negotiation
* Process terms in loop.... research needed * Process terms in loop.... research needed
* Handle will/won't * Handle will/won't
* Handle do's, .. * Handle do's, ..
* Some won't should close connection * Some won't should close connection
* Options/Commands we don't understand shouldn't crash the server!! * Options/Commands we don't understand shouldn't crash the server!!
*/ */
const COMMANDS = { const COMMANDS = {
SE : 240, // End of Sub-Negotation Parameters SE : 240, // End of Sub-Negotation Parameters
NOP : 241, // No Operation NOP : 241, // No Operation
DM : 242, // Data Mark DM : 242, // Data Mark
BRK : 243, // Break BRK : 243, // Break
IP : 244, // Interrupt Process IP : 244, // Interrupt Process
AO : 245, // Abort Output AO : 245, // Abort Output
AYT : 246, // Are You There? AYT : 246, // Are You There?
EC : 247, // Erase Character EC : 247, // Erase Character
EL : 248, // Erase Line EL : 248, // Erase Line
GA : 249, // Go Ahead GA : 249, // Go Ahead
SB : 250, // Start Sub-Negotiation Parameters SB : 250, // Start Sub-Negotiation Parameters
WILL : 251, // WILL : 251, //
WONT : 252, WONT : 252,
DO : 253, DO : 253,
DONT : 254, DONT : 254,
IAC : 255, // (Data Byte) IAC : 255, // (Data Byte)
}; };
// //
// Resources: // Resources:
// * http://www.faqs.org/rfcs/rfc1572.html // * http://www.faqs.org/rfcs/rfc1572.html
// //
const SB_COMMANDS = { const SB_COMMANDS = {
IS : 0, IS : 0,
SEND : 1, SEND : 1,
INFO : 2, INFO : 2,
}; };
// //
// Telnet Options // Telnet Options
// //
// Resources // Resources
// * http://mars.netanya.ac.il/~unesco/cdrom/booklet/HTML/NETWORKING/node300.html // * http://mars.netanya.ac.il/~unesco/cdrom/booklet/HTML/NETWORKING/node300.html
// * http://www.networksorcery.com/enp/protocol/telnet.htm // * http://www.networksorcery.com/enp/protocol/telnet.htm
// //
const OPTIONS = { const OPTIONS = {
TRANSMIT_BINARY : 0, // http://tools.ietf.org/html/rfc856 TRANSMIT_BINARY : 0, // http://tools.ietf.org/html/rfc856
ECHO : 1, // http://tools.ietf.org/html/rfc857 ECHO : 1, // http://tools.ietf.org/html/rfc857
// RECONNECTION : 2 // RECONNECTION : 2
SUPPRESS_GO_AHEAD : 3, // aka 'SGA': RFC 858 @ http://tools.ietf.org/html/rfc858 SUPPRESS_GO_AHEAD : 3, // aka 'SGA': RFC 858 @ http://tools.ietf.org/html/rfc858
//APPROX_MESSAGE_SIZE : 4 //APPROX_MESSAGE_SIZE : 4
STATUS : 5, // http://tools.ietf.org/html/rfc859 STATUS : 5, // http://tools.ietf.org/html/rfc859
TIMING_MARK : 6, // http://tools.ietf.org/html/rfc860 TIMING_MARK : 6, // http://tools.ietf.org/html/rfc860
//RC_TRANS_AND_ECHO : 7, // aka 'RCTE' @ http://www.rfc-base.org/txt/rfc-726.txt //RC_TRANS_AND_ECHO : 7, // aka 'RCTE' @ http://www.rfc-base.org/txt/rfc-726.txt
//OUPUT_LINE_WIDTH : 8, //OUPUT_LINE_WIDTH : 8,
//OUTPUT_PAGE_SIZE : 9, // //OUTPUT_PAGE_SIZE : 9, //
//OUTPUT_CARRIAGE_RETURN_DISP : 10, // RFC 652 //OUTPUT_CARRIAGE_RETURN_DISP : 10, // RFC 652
//OUTPUT_HORIZ_TABSTOPS : 11, // RFC 653 //OUTPUT_HORIZ_TABSTOPS : 11, // RFC 653
//OUTPUT_HORIZ_TAB_DISP : 12, // RFC 654 //OUTPUT_HORIZ_TAB_DISP : 12, // RFC 654
//OUTPUT_FORMFEED_DISP : 13, // RFC 655 //OUTPUT_FORMFEED_DISP : 13, // RFC 655
//OUTPUT_VERT_TABSTOPS : 14, // RFC 656 //OUTPUT_VERT_TABSTOPS : 14, // RFC 656
//OUTPUT_VERT_TAB_DISP : 15, // RFC 657 //OUTPUT_VERT_TAB_DISP : 15, // RFC 657
//OUTPUT_LF_DISP : 16, // RFC 658 //OUTPUT_LF_DISP : 16, // RFC 658
//EXTENDED_ASCII : 17, // RFC 659 //EXTENDED_ASCII : 17, // RFC 659
//LOGOUT : 18, // RFC 727 //LOGOUT : 18, // RFC 727
//BYTE_MACRO : 19, // RFC 753 //BYTE_MACRO : 19, // RFC 753
//DATA_ENTRY_TERMINAL : 20, // RFC 1043 //DATA_ENTRY_TERMINAL : 20, // RFC 1043
//SUPDUP : 21, // RFC 736 //SUPDUP : 21, // RFC 736
//SUPDUP_OUTPUT : 22, // RFC 749 //SUPDUP_OUTPUT : 22, // RFC 749
SEND_LOCATION : 23, // RFC 779 SEND_LOCATION : 23, // RFC 779
TERMINAL_TYPE : 24, // aka 'TTYPE': RFC 1091 @ http://tools.ietf.org/html/rfc1091 TERMINAL_TYPE : 24, // aka 'TTYPE': RFC 1091 @ http://tools.ietf.org/html/rfc1091
//END_OF_RECORD : 25, // RFC 885 //END_OF_RECORD : 25, // RFC 885
//TACACS_USER_ID : 26, // RFC 927 //TACACS_USER_ID : 26, // RFC 927
//OUTPUT_MARKING : 27, // RFC 933 //OUTPUT_MARKING : 27, // RFC 933
//TERMINCAL_LOCATION_NUMBER : 28, // RFC 946 //TERMINCAL_LOCATION_NUMBER : 28, // RFC 946
//TELNET_3270_REGIME : 29, // RFC 1041 //TELNET_3270_REGIME : 29, // RFC 1041
WINDOW_SIZE : 31, // aka 'NAWS': RFC 1073 @ http://tools.ietf.org/html/rfc1073 WINDOW_SIZE : 31, // aka 'NAWS': RFC 1073 @ http://tools.ietf.org/html/rfc1073
TERMINAL_SPEED : 32, // RFC 1079 @ http://tools.ietf.org/html/rfc1079 TERMINAL_SPEED : 32, // RFC 1079 @ http://tools.ietf.org/html/rfc1079
REMOTE_FLOW_CONTROL : 33, // RFC 1072 @ http://tools.ietf.org/html/rfc1372 REMOTE_FLOW_CONTROL : 33, // RFC 1072 @ http://tools.ietf.org/html/rfc1372
LINEMODE : 34, // RFC 1184 @ http://tools.ietf.org/html/rfc1184 LINEMODE : 34, // RFC 1184 @ http://tools.ietf.org/html/rfc1184
X_DISPLAY_LOCATION : 35, // aka 'XDISPLOC': RFC 1096 @ http://tools.ietf.org/html/rfc1096 X_DISPLAY_LOCATION : 35, // aka 'XDISPLOC': RFC 1096 @ http://tools.ietf.org/html/rfc1096
NEW_ENVIRONMENT_DEP : 36, // aka 'NEW-ENVIRON': RFC 1408 @ http://tools.ietf.org/html/rfc1408 (note: RFC 1572 is an update to this) NEW_ENVIRONMENT_DEP : 36, // aka 'NEW-ENVIRON': RFC 1408 @ http://tools.ietf.org/html/rfc1408 (note: RFC 1572 is an update to this)
AUTHENTICATION : 37, // RFC 2941 @ http://tools.ietf.org/html/rfc2941 AUTHENTICATION : 37, // RFC 2941 @ http://tools.ietf.org/html/rfc2941
ENCRYPT : 38, // RFC 2946 @ http://tools.ietf.org/html/rfc2946 ENCRYPT : 38, // RFC 2946 @ http://tools.ietf.org/html/rfc2946
NEW_ENVIRONMENT : 39, // aka 'NEW-ENVIRON': RFC 1572 @ http://tools.ietf.org/html/rfc1572 (note: update to RFC 1408) NEW_ENVIRONMENT : 39, // aka 'NEW-ENVIRON': RFC 1572 @ http://tools.ietf.org/html/rfc1572 (note: update to RFC 1408)
//TN3270E : 40, // RFC 2355 //TN3270E : 40, // RFC 2355
//XAUTH : 41, //XAUTH : 41,
//CHARSET : 42, // RFC 2066 //CHARSET : 42, // RFC 2066
//REMOTE_SERIAL_PORT : 43, //REMOTE_SERIAL_PORT : 43,
//COM_PORT_CONTROL : 44, // RFC 2217 //COM_PORT_CONTROL : 44, // RFC 2217
//SUPRESS_LOCAL_ECHO : 45, //SUPRESS_LOCAL_ECHO : 45,
//START_TLS : 46, //START_TLS : 46,
//KERMIT : 47, // RFC 2840 //KERMIT : 47, // RFC 2840
//SEND_URL : 48, //SEND_URL : 48,
//FORWARD_X : 49, //FORWARD_X : 49,
//PRAGMA_LOGON : 138, //PRAGMA_LOGON : 138,
//SSPI_LOGON : 139, //SSPI_LOGON : 139,
//PRAGMA_HEARTBEAT : 140 //PRAGMA_HEARTBEAT : 140
ARE_YOU_THERE : 246, // aka 'AYT' RFC 854 @ https://tools.ietf.org/html/rfc854 ARE_YOU_THERE : 246, // aka 'AYT' RFC 854 @ https://tools.ietf.org/html/rfc854
EXTENDED_OPTIONS_LIST : 255, // RFC 861 (STD 32) EXTENDED_OPTIONS_LIST : 255, // RFC 861 (STD 32)
}; };
// Commands used within NEW_ENVIRONMENT[_DEP] // Commands used within NEW_ENVIRONMENT[_DEP]
const NEW_ENVIRONMENT_COMMANDS = { const NEW_ENVIRONMENT_COMMANDS = {
VAR : 0, VAR : 0,
VALUE : 1, VALUE : 1,
ESC : 2, ESC : 2,
USERVAR : 3, USERVAR : 3,
}; };
const IAC_BUF = Buffer.from([ COMMANDS.IAC ]); const IAC_BUF = Buffer.from([ COMMANDS.IAC ]);
const IAC_SE_BUF = Buffer.from([ COMMANDS.IAC, COMMANDS.SE ]); const IAC_SE_BUF = Buffer.from([ COMMANDS.IAC, COMMANDS.SE ]);
const COMMAND_NAMES = Object.keys(COMMANDS).reduce(function(names, name) { const COMMAND_NAMES = Object.keys(COMMANDS).reduce(function(names, name) {
names[COMMANDS[name]] = name.toLowerCase(); names[COMMANDS[name]] = name.toLowerCase();
@ -178,9 +178,9 @@ const COMMAND_IMPLS = {};
}; };
}); });
// :TODO: See TooTallNate's telnet.js: Handle COMMAND_IMPL for IAC in binary mode // :TODO: See TooTallNate's telnet.js: Handle COMMAND_IMPL for IAC in binary mode
// Create option names such as 'transmit binary' -> OPTIONS.TRANSMIT_BINARY // Create option names such as 'transmit binary' -> OPTIONS.TRANSMIT_BINARY
const OPTION_NAMES = Object.keys(OPTIONS).reduce(function(names, name) { const OPTION_NAMES = Object.keys(OPTIONS).reduce(function(names, name) {
names[OPTIONS[name]] = name.toLowerCase().replace(/_/g, ' '); names[OPTIONS[name]] = name.toLowerCase().replace(/_/g, ' ');
return names; return names;
@ -193,19 +193,19 @@ function unknownOption(bufs, i, event) {
} }
const OPTION_IMPLS = {}; const OPTION_IMPLS = {};
// :TODO: fill in the rest... // :TODO: fill in the rest...
OPTION_IMPLS.NO_ARGS = OPTION_IMPLS.NO_ARGS =
OPTION_IMPLS[OPTIONS.ECHO] = OPTION_IMPLS[OPTIONS.ECHO] =
OPTION_IMPLS[OPTIONS.STATUS] = OPTION_IMPLS[OPTIONS.STATUS] =
OPTION_IMPLS[OPTIONS.LINEMODE] = OPTION_IMPLS[OPTIONS.LINEMODE] =
OPTION_IMPLS[OPTIONS.TRANSMIT_BINARY] = OPTION_IMPLS[OPTIONS.TRANSMIT_BINARY] =
OPTION_IMPLS[OPTIONS.AUTHENTICATION] = OPTION_IMPLS[OPTIONS.AUTHENTICATION] =
OPTION_IMPLS[OPTIONS.TERMINAL_SPEED] = OPTION_IMPLS[OPTIONS.TERMINAL_SPEED] =
OPTION_IMPLS[OPTIONS.REMOTE_FLOW_CONTROL] = OPTION_IMPLS[OPTIONS.REMOTE_FLOW_CONTROL] =
OPTION_IMPLS[OPTIONS.X_DISPLAY_LOCATION] = OPTION_IMPLS[OPTIONS.X_DISPLAY_LOCATION] =
OPTION_IMPLS[OPTIONS.SEND_LOCATION] = OPTION_IMPLS[OPTIONS.SEND_LOCATION] =
OPTION_IMPLS[OPTIONS.ARE_YOU_THERE] = OPTION_IMPLS[OPTIONS.ARE_YOU_THERE] =
OPTION_IMPLS[OPTIONS.SUPPRESS_GO_AHEAD] = function(bufs, i, event) { OPTION_IMPLS[OPTIONS.SUPPRESS_GO_AHEAD] = function(bufs, i, event) {
event.buf = bufs.splice(0, i).toBuffer(); event.buf = bufs.splice(0, i).toBuffer();
return event; return event;
}; };
@ -214,12 +214,12 @@ OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) {
if(event.commandCode !== COMMANDS.SB) { if(event.commandCode !== COMMANDS.SB) {
OPTION_IMPLS.NO_ARGS(bufs, i, event); OPTION_IMPLS.NO_ARGS(bufs, i, event);
} else { } else {
// We need 4 bytes header + data + IAC SE // We need 4 bytes header + data + IAC SE
if(bufs.length < 7) { if(bufs.length < 7) {
return MORE_DATA_REQUIRED; return MORE_DATA_REQUIRED;
} }
const end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes const end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes
if(-1 === end) { if(-1 === end) {
return MORE_DATA_REQUIRED; return MORE_DATA_REQUIRED;
} }
@ -232,10 +232,10 @@ OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) {
.uint8('opt') .uint8('opt')
.uint8('is') .uint8('is')
.array('ttype', { .array('ttype', {
type : 'uint8', type : 'uint8',
readUntil : b => 255 === b, // 255=COMMANDS.IAC readUntil : b => 255 === b, // 255=COMMANDS.IAC
}) })
// note we read iac2 above // note we read iac2 above
.uint8('se') .uint8('se')
.parse(bufs.toBuffer()); .parse(bufs.toBuffer());
} catch(e) { } catch(e) {
@ -248,10 +248,10 @@ OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) {
EnigAssert(OPTIONS.TERMINAL_TYPE === ttypeCmd.opt); EnigAssert(OPTIONS.TERMINAL_TYPE === ttypeCmd.opt);
EnigAssert(SB_COMMANDS.IS === ttypeCmd.is); EnigAssert(SB_COMMANDS.IS === ttypeCmd.is);
EnigAssert(ttypeCmd.ttype.length > 0); EnigAssert(ttypeCmd.ttype.length > 0);
// note we found IAC_SE above // note we found IAC_SE above
// some terminals such as NetRunner provide a NULL-terminated buffer // some terminals such as NetRunner provide a NULL-terminated buffer
// slice to remove IAC // slice to remove IAC
event.ttype = stringFromNullTermBuffer(ttypeCmd.ttype.slice(0, -1), 'ascii'); event.ttype = stringFromNullTermBuffer(ttypeCmd.ttype.slice(0, -1), 'ascii');
bufs.splice(0, end); bufs.splice(0, end);
@ -264,7 +264,7 @@ OPTION_IMPLS[OPTIONS.WINDOW_SIZE] = function(bufs, i, event) {
if(event.commandCode !== COMMANDS.SB) { if(event.commandCode !== COMMANDS.SB) {
OPTION_IMPLS.NO_ARGS(bufs, i, event); OPTION_IMPLS.NO_ARGS(bufs, i, event);
} else { } else {
// we need 9 bytes // we need 9 bytes
if(bufs.length < 9) { if(bufs.length < 9) {
return MORE_DATA_REQUIRED; return MORE_DATA_REQUIRED;
} }
@ -291,39 +291,39 @@ OPTION_IMPLS[OPTIONS.WINDOW_SIZE] = function(bufs, i, event) {
EnigAssert(COMMANDS.IAC === nawsCmd.iac2); EnigAssert(COMMANDS.IAC === nawsCmd.iac2);
EnigAssert(COMMANDS.SE === nawsCmd.se); EnigAssert(COMMANDS.SE === nawsCmd.se);
event.cols = event.columns = event.width = nawsCmd.width; event.cols = event.columns = event.width = nawsCmd.width;
event.rows = event.height = nawsCmd.height; event.rows = event.height = nawsCmd.height;
} }
return event; return event;
}; };
// Build an array of delimiters for parsing NEW_ENVIRONMENT[_DEP] // Build an array of delimiters for parsing NEW_ENVIRONMENT[_DEP]
const NEW_ENVIRONMENT_DELIMITERS = []; const NEW_ENVIRONMENT_DELIMITERS = [];
Object.keys(NEW_ENVIRONMENT_COMMANDS).forEach(function onKey(k) { Object.keys(NEW_ENVIRONMENT_COMMANDS).forEach(function onKey(k) {
NEW_ENVIRONMENT_DELIMITERS.push(NEW_ENVIRONMENT_COMMANDS[k]); NEW_ENVIRONMENT_DELIMITERS.push(NEW_ENVIRONMENT_COMMANDS[k]);
}); });
// Handle the deprecated RFC 1408 & the updated RFC 1572: // Handle the deprecated RFC 1408 & the updated RFC 1572:
OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT_DEP] = OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT_DEP] =
OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) {
if(event.commandCode !== COMMANDS.SB) { if(event.commandCode !== COMMANDS.SB) {
OPTION_IMPLS.NO_ARGS(bufs, i, event); OPTION_IMPLS.NO_ARGS(bufs, i, event);
} else { } else {
// //
// We need 4 bytes header + <optional payload> + IAC SE // We need 4 bytes header + <optional payload> + IAC SE
// Many terminals send a empty list: // Many terminals send a empty list:
// IAC SB NEW-ENVIRON IS IAC SE // IAC SB NEW-ENVIRON IS IAC SE
// //
if(bufs.length < 6) { if(bufs.length < 6) {
return MORE_DATA_REQUIRED; return MORE_DATA_REQUIRED;
} }
let end = bufs.indexOf(IAC_SE_BUF, 4); // look past header bytes let end = bufs.indexOf(IAC_SE_BUF, 4); // look past header bytes
if(-1 === end) { if(-1 === end) {
return MORE_DATA_REQUIRED; return MORE_DATA_REQUIRED;
} }
// :TODO: It's likely that we could do all the env name/value parsing directly in Parser. // :TODO: It's likely that we could do all the env name/value parsing directly in Parser.
let envCmd; let envCmd;
try { try {
@ -331,12 +331,12 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) {
.uint8('iac1') .uint8('iac1')
.uint8('sb') .uint8('sb')
.uint8('opt') .uint8('opt')
.uint8('isOrInfo') // IS=initial, INFO=updates .uint8('isOrInfo') // IS=initial, INFO=updates
.array('envBlock', { .array('envBlock', {
type : 'uint8', type : 'uint8',
readUntil : b => 255 === b, // 255=COMMANDS.IAC readUntil : b => 255 === b, // 255=COMMANDS.IAC
}) })
// note we consume IAC above // note we consume IAC above
.uint8('se') .uint8('se')
.parse(bufs.splice(0, bufs.length).toBuffer()); .parse(bufs.splice(0, bufs.length).toBuffer());
} catch(e) { } catch(e) {
@ -350,34 +350,34 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) {
EnigAssert(SB_COMMANDS.IS === envCmd.isOrInfo || SB_COMMANDS.INFO === envCmd.isOrInfo); EnigAssert(SB_COMMANDS.IS === envCmd.isOrInfo || SB_COMMANDS.INFO === envCmd.isOrInfo);
if(OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt) { if(OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt) {
// :TODO: we should probably support this for legacy clients? // :TODO: we should probably support this for legacy clients?
Log.warn('Handling deprecated RFC 1408 NEW-ENVIRON'); Log.warn('Handling deprecated RFC 1408 NEW-ENVIRON');
} }
const envBuf = envCmd.envBlock.slice(0, -1); // remove IAC const envBuf = envCmd.envBlock.slice(0, -1); // remove IAC
if(envBuf.length < 4) { // TYPE + single char name + sep + single char value if(envBuf.length < 4) { // TYPE + single char name + sep + single char value
// empty env block // empty env block
return event; return event;
} }
const States = { const States = {
Name : 1, Name : 1,
Value : 2, Value : 2,
}; };
let state = States.Name; let state = States.Name;
const setVars = {}; const setVars = {};
const delVars = []; const delVars = [];
let varName; let varName;
// :TODO: handle ESC type!!! // :TODO: handle ESC type!!!
while(envBuf.length) { while(envBuf.length) {
switch(state) { switch(state) {
case States.Name : case States.Name :
{ {
const type = parseInt(envBuf.splice(0, 1)); const type = parseInt(envBuf.splice(0, 1));
if(![ NEW_ENVIRONMENT_COMMANDS.VAR, NEW_ENVIRONMENT_COMMANDS.USERVAR, NEW_ENVIRONMENT_COMMANDS.ESC ].includes(type)) { if(![ NEW_ENVIRONMENT_COMMANDS.VAR, NEW_ENVIRONMENT_COMMANDS.USERVAR, NEW_ENVIRONMENT_COMMANDS.ESC ].includes(type)) {
return event; // fail :( return event; // fail :(
} }
let nameEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VALUE); let nameEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VALUE);
@ -387,7 +387,7 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) {
varName = envBuf.splice(0, nameEnd); varName = envBuf.splice(0, nameEnd);
if(!varName) { if(!varName) {
return event; // something is wrong. return event; // something is wrong.
} }
varName = Buffer.from(varName).toString('ascii'); varName = Buffer.from(varName).toString('ascii');
@ -397,7 +397,7 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) {
state = States.Value; state = States.Value;
} else { } else {
state = States.Name; state = States.Name;
delVars.push(varName); // no value; del this var delVars.push(varName); // no value; del this var
} }
} }
break; break;
@ -423,15 +423,15 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) {
} }
} }
// :TODO: Handle deleting previously set vars via delVars // :TODO: Handle deleting previously set vars via delVars
event.type = envCmd.isOrInfo; event.type = envCmd.isOrInfo;
event.envVars = setVars; event.envVars = setVars;
} }
return event; return event;
}; };
const MORE_DATA_REQUIRED = 0xfeedface; const MORE_DATA_REQUIRED = 0xfeedface;
function parseBufs(bufs) { function parseBufs(bufs) {
EnigAssert(bufs.length >= 2); EnigAssert(bufs.length >= 2);
@ -440,16 +440,16 @@ function parseBufs(bufs) {
} }
function parseCommand(bufs, i, event) { function parseCommand(bufs, i, event) {
const command = bufs.get(i); // :TODO: fix deprecation... [i] is not the same const command = bufs.get(i); // :TODO: fix deprecation... [i] is not the same
event.commandCode = command; event.commandCode = command;
event.command = COMMAND_NAMES[command]; event.command = COMMAND_NAMES[command];
const handler = COMMAND_IMPLS[command]; const handler = COMMAND_IMPLS[command];
if(handler) { if(handler) {
return handler(bufs, i + 1, event); return handler(bufs, i + 1, event);
} else { } else {
if(2 !== bufs.length) { if(2 !== bufs.length) {
Log.warn( { bufsLength : bufs.length }, 'Expected bufs length of 2'); // expected: IAC + COMMAND Log.warn( { bufsLength : bufs.length }, 'Expected bufs length of 2'); // expected: IAC + COMMAND
} }
event.buf = bufs.splice(0, 2).toBuffer(); event.buf = bufs.splice(0, 2).toBuffer();
@ -458,9 +458,9 @@ function parseCommand(bufs, i, event) {
} }
function parseOption(bufs, i, event) { function parseOption(bufs, i, event) {
const option = bufs.get(i); // :TODO: fix deprecation... [i] is not the same const option = bufs.get(i); // :TODO: fix deprecation... [i] is not the same
event.optionCode = option; event.optionCode = option;
event.option = OPTION_NAMES[option]; event.option = OPTION_NAMES[option];
const handler = OPTION_IMPLS[option]; const handler = OPTION_IMPLS[option];
return handler ? handler(bufs, i + 1, event) : unknownOption(bufs, i + 1, event); return handler ? handler(bufs, i + 1, event) : unknownOption(bufs, i + 1, event);
@ -470,20 +470,20 @@ function parseOption(bufs, i, event) {
function TelnetClient(input, output) { function TelnetClient(input, output) {
baseClient.Client.apply(this, arguments); baseClient.Client.apply(this, arguments);
const self = this; const self = this;
let bufs = buffers(); let bufs = buffers();
this.bufs = bufs; this.bufs = bufs;
this.sentDont = {}; // DON'T's we've already sent this.sentDont = {}; // DON'T's we've already sent
this.setInputOutput(input, output); this.setInputOutput(input, output);
this.negotiationsComplete = false; // are we in the 'negotiation' phase? this.negotiationsComplete = false; // are we in the 'negotiation' phase?
this.didReady = false; // have we emit the 'ready' event? this.didReady = false; // have we emit the 'ready' event?
this.subNegotiationState = { this.subNegotiationState = {
newEnvironRequested : false, newEnvironRequested : false,
}; };
this.dataHandler = function(b) { this.dataHandler = function(b) {
@ -498,7 +498,7 @@ function TelnetClient(input, output) {
while((i = bufs.indexOf(IAC_BUF)) >= 0) { while((i = bufs.indexOf(IAC_BUF)) >= 0) {
// //
// Some clients will send even IAC separate from data // Some clients will send even IAC separate from data
// //
if(bufs.length <= (i + 1)) { if(bufs.length <= (i + 1)) {
i = MORE_DATA_REQUIRED; i = MORE_DATA_REQUIRED;
@ -517,7 +517,7 @@ function TelnetClient(input, output) {
break; break;
} else if(i) { } else if(i) {
if(i.option) { if(i.option) {
self.emit(i.option, i); // "transmit binary", "echo", ... self.emit(i.option, i); // "transmit binary", "echo", ...
} }
self.handleTelnetEvent(i); self.handleTelnetEvent(i);
@ -530,8 +530,8 @@ function TelnetClient(input, output) {
if(MORE_DATA_REQUIRED !== i && bufs.length > 0) { if(MORE_DATA_REQUIRED !== i && bufs.length > 0) {
// //
// Standard data payload. This can still be "non-user" data // Standard data payload. This can still be "non-user" data
// such as ANSI control, but we don't handle that here. // such as ANSI control, but we don't handle that here.
// //
self.emit('data', bufs.splice(0).toBuffer()); self.emit('data', bufs.splice(0).toBuffer());
} }
@ -576,7 +576,7 @@ function TelnetClient(input, output) {
util.inherits(TelnetClient, baseClient.Client); util.inherits(TelnetClient, baseClient.Client);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Telnet Command/Option handling // Telnet Command/Option handling
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
TelnetClient.prototype.handleTelnetEvent = function(evt) { TelnetClient.prototype.handleTelnetEvent = function(evt) {
@ -584,14 +584,14 @@ TelnetClient.prototype.handleTelnetEvent = function(evt) {
return this.connectionWarn( { evt : evt }, 'No command for event'); return this.connectionWarn( { evt : evt }, 'No command for event');
} }
// handler name e.g. 'handleWontCommand' // handler name e.g. 'handleWontCommand'
const handlerName = `handle${evt.command.charAt(0).toUpperCase()}${evt.command.substr(1)}Command`; const handlerName = `handle${evt.command.charAt(0).toUpperCase()}${evt.command.substr(1)}Command`;
if(this[handlerName]) { if(this[handlerName]) {
// specialized // specialized
this[handlerName](evt); this[handlerName](evt);
} else { } else {
// generic-ish // generic-ish
this.handleMiscCommand(evt); this.handleMiscCommand(evt);
} }
}; };
@ -599,16 +599,16 @@ TelnetClient.prototype.handleTelnetEvent = function(evt) {
TelnetClient.prototype.handleWillCommand = function(evt) { TelnetClient.prototype.handleWillCommand = function(evt) {
if('terminal type' === evt.option) { if('terminal type' === evt.option) {
// //
// See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html
// //
this.requestTerminalType(); this.requestTerminalType();
} else if('new environment' === evt.option) { } else if('new environment' === evt.option) {
// //
// See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html
// //
this.requestNewEnvironment(); this.requestNewEnvironment();
} else { } else {
// :TODO: temporary: // :TODO: temporary:
this.connectionTrace(evt, 'WILL'); this.connectionTrace(evt, 'WILL');
} }
}; };
@ -628,20 +628,20 @@ TelnetClient.prototype.handleWontCommand = function(evt) {
}; };
TelnetClient.prototype.handleDoCommand = function(evt) { TelnetClient.prototype.handleDoCommand = function(evt) {
// :TODO: handle the rest, e.g. echo nd the like // :TODO: handle the rest, e.g. echo nd the like
if('linemode' === evt.option) { if('linemode' === evt.option) {
// //
// Client wants to enable linemode editing. Denied. // Client wants to enable linemode editing. Denied.
// //
this.wont.linemode(); this.wont.linemode();
} else if('encrypt' === evt.option) { } else if('encrypt' === evt.option) {
// //
// Client wants to enable encryption. Denied. // Client wants to enable encryption. Denied.
// //
this.wont.encrypt(); this.wont.encrypt();
} else { } else {
// :TODO: temporary: // :TODO: temporary:
this.connectionTrace(evt, 'DO'); this.connectionTrace(evt, 'DO');
} }
}; };
@ -655,33 +655,33 @@ TelnetClient.prototype.handleSbCommand = function(evt) {
if('terminal type' === evt.option) { if('terminal type' === evt.option) {
// //
// See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html
// //
// :TODO: According to RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html // :TODO: According to RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html
// We should keep asking until we see a repeat. From there, determine the best type/etc. // We should keep asking until we see a repeat. From there, determine the best type/etc.
self.setTermType(evt.ttype); self.setTermType(evt.ttype);
self.negotiationsComplete = true; // :TODO: throw in a array of what we've taken care. Complete = array satisified or timeout self.negotiationsComplete = true; // :TODO: throw in a array of what we've taken care. Complete = array satisified or timeout
self.readyNow(); self.readyNow();
} else if('new environment' === evt.option) { } else if('new environment' === evt.option) {
// //
// Handling is as follows: // Handling is as follows:
// * Map 'TERM' -> 'termType' and only update if ours is 'unknown' // * Map 'TERM' -> 'termType' and only update if ours is 'unknown'
// * Map COLUMNS -> 'termWidth' and only update if ours is 0 // * Map COLUMNS -> 'termWidth' and only update if ours is 0
// * Map ROWS -> 'termHeight' and only update if ours is 0 // * Map ROWS -> 'termHeight' and only update if ours is 0
// * Add any new variables, ignore any existing // * Add any new variables, ignore any existing
// //
Object.keys(evt.envVars || {} ).forEach(function onEnv(name) { Object.keys(evt.envVars || {} ).forEach(function onEnv(name) {
if('TERM' === name && 'unknown' === self.term.termType) { if('TERM' === name && 'unknown' === self.term.termType) {
self.setTermType(evt.envVars[name]); self.setTermType(evt.envVars[name]);
} else if('COLUMNS' === name && 0 === self.term.termWidth) { } else if('COLUMNS' === name && 0 === self.term.termWidth) {
self.term.termWidth = parseInt(evt.envVars[name]); self.term.termWidth = parseInt(evt.envVars[name]);
self.clearMciCache(); // term size changes = invalidate cache self.clearMciCache(); // term size changes = invalidate cache
self.connectionDebug({ termWidth : self.term.termWidth, source : 'NEW-ENVIRON'}, 'Window width updated'); self.connectionDebug({ termWidth : self.term.termWidth, source : 'NEW-ENVIRON'}, 'Window width updated');
} else if('ROWS' === name && 0 === self.term.termHeight) { } else if('ROWS' === name && 0 === self.term.termHeight) {
self.term.termHeight = parseInt(evt.envVars[name]); self.term.termHeight = parseInt(evt.envVars[name]);
self.clearMciCache(); // term size changes = invalidate cache self.clearMciCache(); // term size changes = invalidate cache
self.connectionDebug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated'); self.connectionDebug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated');
} else { } else {
if(name in self.term.env) { if(name in self.term.env) {
@ -704,11 +704,11 @@ TelnetClient.prototype.handleSbCommand = function(evt) {
} else if('window size' === evt.option) { } else if('window size' === evt.option) {
// //
// Update termWidth & termHeight. // Update termWidth & termHeight.
// Set LINES and COLUMNS environment variables as well. // Set LINES and COLUMNS environment variables as well.
// //
self.term.termWidth = evt.width; self.term.termWidth = evt.width;
self.term.termHeight = evt.height; self.term.termHeight = evt.height;
if(evt.width > 0) { if(evt.width > 0) {
self.term.env.COLUMNS = evt.height; self.term.env.COLUMNS = evt.height;
@ -718,7 +718,7 @@ TelnetClient.prototype.handleSbCommand = function(evt) {
self.term.env.ROWS = evt.height; self.term.env.ROWS = evt.height;
} }
self.clearMciCache(); // term size changes = invalidate cache self.clearMciCache(); // term size changes = invalidate cache
self.connectionDebug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated'); self.connectionDebug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated');
} else { } else {
@ -736,11 +736,11 @@ TelnetClient.prototype.handleMiscCommand = function(evt) {
EnigAssert(evt.command !== 'undefined' && evt.command.length > 0); EnigAssert(evt.command !== 'undefined' && evt.command.length > 0);
// //
// See: // See:
// * RFC 854 @ http://tools.ietf.org/html/rfc854 // * RFC 854 @ http://tools.ietf.org/html/rfc854
// //
if('ip' === evt.command) { if('ip' === evt.command) {
// Interrupt Process (IP) // Interrupt Process (IP)
this.log.debug('Interrupt Process (IP) - Ending'); this.log.debug('Interrupt Process (IP) - Ending');
this.input.end(); this.input.end();
@ -817,33 +817,33 @@ TelnetClient.prototype.banner = function() {
}; };
function Command(command, client) { function Command(command, client) {
this.command = COMMANDS[command.toUpperCase()]; this.command = COMMANDS[command.toUpperCase()];
this.client = client; this.client = client;
} }
// Create Command objects with echo, transmit_binary, ... // Create Command objects with echo, transmit_binary, ...
Object.keys(OPTIONS).forEach(function(name) { Object.keys(OPTIONS).forEach(function(name) {
const code = OPTIONS[name]; const code = OPTIONS[name];
Command.prototype[name.toLowerCase()] = function() { Command.prototype[name.toLowerCase()] = function() {
const buf = Buffer.alloc(3); const buf = Buffer.alloc(3);
buf[0] = COMMANDS.IAC; buf[0] = COMMANDS.IAC;
buf[1] = this.command; buf[1] = this.command;
buf[2] = code; buf[2] = code;
return this.client.output.write(buf); return this.client.output.write(buf);
}; };
}); });
// Create do, dont, etc. methods on Client // Create do, dont, etc. methods on Client
['do', 'dont', 'will', 'wont'].forEach(function(command) { ['do', 'dont', 'will', 'wont'].forEach(function(command) {
const get = function() { const get = function() {
return new Command(command, this); return new Command(command, this);
}; };
Object.defineProperty(TelnetClient.prototype, command, { Object.defineProperty(TelnetClient.prototype, command, {
get : get, get : get,
enumerable : true, enumerable : true,
configurable : true configurable : true
}); });
}); });
@ -861,9 +861,9 @@ exports.getModule = class TelnetServerModule extends LoginServerModule {
this.handleNewClient(client, sock, ModuleInfo); this.handleNewClient(client, sock, ModuleInfo);
// //
// Set a timeout and attempt to proceed even if we don't know // Set a timeout and attempt to proceed even if we don't know
// the term type yet, which is the preferred trigger // the term type yet, which is the preferred trigger
// for moving along // for moving along
// //
setTimeout( () => { setTimeout( () => {
if(!client.didReady) { if(!client.didReady) {

View File

@ -1,25 +1,25 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('../../config.js').get; const Config = require('../../config.js').get;
const TelnetClient = require('./telnet.js').TelnetClient; const TelnetClient = require('./telnet.js').TelnetClient;
const Log = require('../../logger.js').log; const Log = require('../../logger.js').log;
const LoginServerModule = require('../../login_server_module.js'); const LoginServerModule = require('../../login_server_module.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const WebSocketServer = require('ws').Server; const WebSocketServer = require('ws').Server;
const http = require('http'); const http = require('http');
const https = require('https'); const https = require('https');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const Writable = require('stream'); const Writable = require('stream');
const ModuleInfo = exports.moduleInfo = { const ModuleInfo = exports.moduleInfo = {
name : 'WebSocket', name : 'WebSocket',
desc : 'WebSocket Server', desc : 'WebSocket Server',
author : 'NuSkooler', author : 'NuSkooler',
packageName : 'codes.l33t.enigma.websocket.server', packageName : 'codes.l33t.enigma.websocket.server',
}; };
function WebSocketClient(ws, req, serverType) { function WebSocketClient(ws, req, serverType) {
@ -35,8 +35,8 @@ function WebSocketClient(ws, req, serverType) {
}; };
// //
// This bridge makes accessible various calls that client sub classes // This bridge makes accessible various calls that client sub classes
// want to access on I/O socket // want to access on I/O socket
// //
this.socketBridge = new class SocketBridge extends Writable { this.socketBridge = new class SocketBridge extends Writable {
constructor(ws) { constructor(ws) {
@ -49,12 +49,12 @@ function WebSocketClient(ws, req, serverType) {
} }
write(data, cb) { write(data, cb) {
cb = cb || ( () => { /* eat it up */} ); // handle data writes after close cb = cb || ( () => { /* eat it up */} ); // handle data writes after close
return this.ws.send(data, { binary : true }, cb); return this.ws.send(data, { binary : true }, cb);
} }
// we need to fake some streaming work // we need to fake some streaming work
unpipe() { unpipe() {
Log.trace('WebSocket SocketBridge unpipe()'); Log.trace('WebSocket SocketBridge unpipe()');
} }
@ -64,7 +64,7 @@ function WebSocketClient(ws, req, serverType) {
} }
get remoteAddress() { get remoteAddress() {
// Support X-Forwarded-For and X-Real-IP headers for proxied connections // Support X-Forwarded-For and X-Real-IP headers for proxied connections
return (self.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress; return (self.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress;
} }
}(ws); }(ws);
@ -72,12 +72,12 @@ function WebSocketClient(ws, req, serverType) {
ws.on('message', this.dataHandler); ws.on('message', this.dataHandler);
ws.on('close', () => { ws.on('close', () => {
// we'll remove client connection which will in turn end() via our SocketBridge above // we'll remove client connection which will in turn end() via our SocketBridge above
return this.emit('end'); return this.emit('end');
}); });
// //
// Montior connection status with ping/pong // Montior connection status with ping/pong
// //
ws.on('pong', () => { ws.on('pong', () => {
Log.trace(`Pong from ${this.socketBridge.remoteAddress}`); Log.trace(`Pong from ${this.socketBridge.remoteAddress}`);
@ -89,11 +89,11 @@ function WebSocketClient(ws, req, serverType) {
Log.trace( { headers : req.headers }, 'WebSocket connection headers' ); Log.trace( { headers : req.headers }, 'WebSocket connection headers' );
// //
// If the config allows it, look for 'x-forwarded-proto' as "https" // If the config allows it, look for 'x-forwarded-proto' as "https"
// to override |isSecure| // to override |isSecure|
// //
if(true === _.get(Config(), 'loginServers.webSocket.proxied') && if(true === _.get(Config(), 'loginServers.webSocket.proxied') &&
'https' === req.headers['x-forwarded-proto']) 'https' === req.headers['x-forwarded-proto'])
{ {
Log.debug(`Assuming secure connection due to X-Forwarded-Proto of "${req.headers['x-forwarded-proto']}"`); Log.debug(`Assuming secure connection due to X-Forwarded-Proto of "${req.headers['x-forwarded-proto']}"`);
this.proxied = true; this.proxied = true;
@ -101,7 +101,7 @@ function WebSocketClient(ws, req, serverType) {
this.proxied = false; this.proxied = false;
} }
// start handshake process // start handshake process
this.banner(); this.banner();
} }
@ -116,40 +116,40 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule {
createServer() { createServer() {
// //
// We will actually create up to two servers: // We will actually create up to two servers:
// * insecure websocket (ws://) // * insecure websocket (ws://)
// * secure (tls) websocket (wss://) // * secure (tls) websocket (wss://)
// //
const config = _.get(Config(), 'loginServers.webSocket'); const config = _.get(Config(), 'loginServers.webSocket');
if(!_.isObject(config)) { if(!_.isObject(config)) {
return; return;
} }
const wsPort = _.get(config, 'ws.port'); const wsPort = _.get(config, 'ws.port');
const wssPort = _.get(config, 'wss.port'); const wssPort = _.get(config, 'wss.port');
if(true === _.get(config, 'ws.enabled') && _.isNumber(wsPort)) { if(true === _.get(config, 'ws.enabled') && _.isNumber(wsPort)) {
const httpServer = http.createServer( (req, resp) => { const httpServer = http.createServer( (req, resp) => {
// dummy handler // dummy handler
resp.writeHead(200); resp.writeHead(200);
return resp.end('ENiGMA½ BBS WebSocket Server!'); return resp.end('ENiGMA½ BBS WebSocket Server!');
}); });
this.insecure = { this.insecure = {
httpServer : httpServer, httpServer : httpServer,
wsServer : new WebSocketServer( { server : httpServer } ), wsServer : new WebSocketServer( { server : httpServer } ),
}; };
} }
if(_.isObject(config, 'wss') && true === _.get(config, 'wss.enabled') && _.isNumber(wssPort)) { if(_.isObject(config, 'wss') && true === _.get(config, 'wss.enabled') && _.isNumber(wssPort)) {
const httpServer = https.createServer({ const httpServer = https.createServer({
key : fs.readFileSync(config.wss.keyPem), key : fs.readFileSync(config.wss.keyPem),
cert : fs.readFileSync(config.wss.certPem), cert : fs.readFileSync(config.wss.certPem),
}); });
this.secure = { this.secure = {
httpServer : httpServer, httpServer : httpServer,
wsServer : new WebSocketServer( { server : httpServer } ), wsServer : new WebSocketServer( { server : httpServer } ),
}; };
} }
} }
@ -161,8 +161,8 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule {
return; return;
} }
const serverName = `${ModuleInfo.name} (${serverType})`; const serverName = `${ModuleInfo.name} (${serverType})`;
const port = parseInt(_.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] )); const port = parseInt(_.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] ));
if(isNaN(port)) { if(isNaN(port)) {
Log.error( { server : serverName, port : port }, 'Cannot load server (invalid port)' ); Log.error( { server : serverName, port : port }, 'Cannot load server (invalid port)' );
@ -180,7 +180,7 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule {
}); });
// //
// Send pings every 30s // Send pings every 30s
// //
setInterval( () => { setInterval( () => {
WSS_SERVER_TYPES.forEach(serverType => { WSS_SERVER_TYPES.forEach(serverType => {
@ -191,10 +191,10 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule {
return ws.terminate(); return ws.terminate();
} }
ws.isConnectionAlive = false; // pong will reset this ws.isConnectionAlive = false; // pong will reset this
Log.trace('Ping to remote WebSocket client'); Log.trace('Ping to remote WebSocket client');
return ws.ping('', false); // false=don't mask return ws.ping('', false); // false=don't mask
}); });
} }
}); });

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