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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,29 +1,29 @@
/* jslint node: true */
'use strict';
// enigma-bbs
const MenuModule = require('../core/menu_module.js').MenuModule;
const resetScreen = require('../core/ansi_term.js').resetScreen;
// enigma-bbs
const MenuModule = require('../core/menu_module.js').MenuModule;
const resetScreen = require('../core/ansi_term.js').resetScreen;
// deps
const async = require('async');
const _ = require('lodash');
// deps
const async = require('async');
const _ = require('lodash');
const RLogin = require('rlogin');
exports.moduleInfo = {
name : 'CombatNet',
desc : 'CombatNet Access Module',
author : 'Dave Stephens',
name : 'CombatNet',
desc : 'CombatNet Access Module',
author : 'Dave Stephens',
};
exports.getModule = class CombatNetModule extends MenuModule {
constructor(options) {
super(options);
// establish defaults
this.config = options.menuConfig.config;
this.config.host = this.config.host || 'bbs.combatnet.us';
this.config.rloginPort = this.config.rloginPort || 4513;
// establish defaults
this.config = options.menuConfig.config;
this.config.host = this.config.host || 'bbs.combatnet.us';
this.config.rloginPort = this.config.rloginPort || 4513;
}
initSequence() {
@ -51,7 +51,7 @@ exports.getModule = class CombatNetModule extends MenuModule {
};
const rlogin = new RLogin(
{ 'clientUsername' : self.config.password,
{ 'clientUsername' : self.config.password,
'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`,
'host' : self.config.host,
'port' : self.config.rloginPort,
@ -79,7 +79,7 @@ exports.getModule = class CombatNetModule extends MenuModule {
}
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. */
function(state) {
@ -101,7 +101,7 @@ exports.getModule = class CombatNetModule extends MenuModule {
// connect...
rlogin.connect();
// note: no explicit callback() until we're finished!
// note: no explicit callback() until we're finished!
}
],
err => {
@ -109,7 +109,7 @@ exports.getModule = class CombatNetModule extends MenuModule {
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();
}
);

View File

@ -1,15 +1,15 @@
/* jslint node: true */
'use strict';
// deps
const _ = require('lodash');
// deps
const _ = require('lodash');
exports.sortAreasOrConfs = sortAreasOrConfs;
exports.sortAreasOrConfs = sortAreasOrConfs;
//
// Method for sorting message, file, etc. areas and confs
// 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
// Method for sorting message, file, etc. areas and confs
// 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
//
function sortAreasOrConfs(areasOrConfs, type) {
let entryA;
@ -24,7 +24,7 @@ function sortAreasOrConfs(areasOrConfs, type) {
} else {
const keyA = entryA.sort ? entryA.sort.toString() : entryA.name;
const keyB = entryB.sort ? entryB.sort.toString() : entryB.name;
return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare
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 */
'use strict';
// deps
const paths = require('path');
const fs = require('graceful-fs');
const hjson = require('hjson');
const sane = require('sane');
// deps
const paths = require('path');
const fs = require('graceful-fs');
const hjson = require('hjson');
const sane = require('sane');
module.exports = new class ConfigCache
{
constructor() {
this.cache = new Map(); // path->parsed config
this.cache = new Map(); // path->parsed config
}
getConfigWithOptions(options, cb) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,20 @@
/* jslint node: true */
'use strict';
const paths = require('path');
const events = require('events');
const Log = require('./logger.js').log;
const SystemEvents = require('./system_events.js');
const paths = require('path');
const events = require('events');
const Log = require('./logger.js').log;
const SystemEvents = require('./system_events.js');
// deps
const _ = require('lodash');
const async = require('async');
const glob = require('glob');
// deps
const _ = require('lodash');
const async = require('async');
const glob = require('glob');
module.exports = new class Events extends events.EventEmitter {
constructor() {
super();
this.setMaxListeners(32); // :TODO: play with this...
this.setMaxListeners(32); // :TODO: play with this...
}
getSystemEvents() {
@ -60,7 +60,7 @@ module.exports = new class Events extends events.EventEmitter {
const mod = require(fullModulePath);
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);
}
} catch(e) {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,57 +1,57 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const Config = require('./config.js').get;
const Errors = require('./enig_error.js').Errors;
const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs;
const FileEntry = require('./file_entry.js');
const FileDb = require('./database.js').dbs.file;
const ArchiveUtil = require('./archive_util.js');
const CRC32 = require('./crc.js').CRC32;
const Log = require('./logger.js').log;
const resolveMimeType = require('./mime_util.js').resolveMimeType;
const stringFormat = require('./string_format.js');
const wordWrapText = require('./word_wrap.js').wordWrapText;
const StatLog = require('./stat_log.js');
// ENiGMA½
const Config = require('./config.js').get;
const Errors = require('./enig_error.js').Errors;
const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs;
const FileEntry = require('./file_entry.js');
const FileDb = require('./database.js').dbs.file;
const ArchiveUtil = require('./archive_util.js');
const CRC32 = require('./crc.js').CRC32;
const Log = require('./logger.js').log;
const resolveMimeType = require('./mime_util.js').resolveMimeType;
const stringFormat = require('./string_format.js');
const wordWrapText = require('./word_wrap.js').wordWrapText;
const StatLog = require('./stat_log.js');
// deps
const _ = require('lodash');
const async = require('async');
const fs = require('graceful-fs');
const crypto = require('crypto');
const paths = require('path');
const temptmp = require('temptmp').createTrackedSession('file_area');
const iconv = require('iconv-lite');
const execFile = require('child_process').execFile;
const moment = require('moment');
// deps
const _ = require('lodash');
const async = require('async');
const fs = require('graceful-fs');
const crypto = require('crypto');
const paths = require('path');
const temptmp = require('temptmp').createTrackedSession('file_area');
const iconv = require('iconv-lite');
const execFile = require('child_process').execFile;
const moment = require('moment');
exports.startup = startup;
exports.isInternalArea = isInternalArea;
exports.getAvailableFileAreas = getAvailableFileAreas;
exports.getAvailableFileAreaTags = getAvailableFileAreaTags;
exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas;
exports.isValidStorageTag = isValidStorageTag;
exports.getAreaStorageDirectoryByTag = getAreaStorageDirectoryByTag;
exports.getAreaDefaultStorageDirectory = getAreaDefaultStorageDirectory;
exports.getAreaStorageLocations = getAreaStorageLocations;
exports.getDefaultFileAreaTag = getDefaultFileAreaTag;
exports.getFileAreaByTag = getFileAreaByTag;
exports.getFileEntryPath = getFileEntryPath;
exports.changeFileAreaWithOptions = changeFileAreaWithOptions;
exports.scanFile = scanFile;
exports.scanFileAreaForChanges = scanFileAreaForChanges;
exports.getDescFromFileName = getDescFromFileName;
exports.getAreaStats = getAreaStats;
exports.cleanUpTempSessionItems = cleanUpTempSessionItems;
exports.startup = startup;
exports.isInternalArea = isInternalArea;
exports.getAvailableFileAreas = getAvailableFileAreas;
exports.getAvailableFileAreaTags = getAvailableFileAreaTags;
exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas;
exports.isValidStorageTag = isValidStorageTag;
exports.getAreaStorageDirectoryByTag = getAreaStorageDirectoryByTag;
exports.getAreaDefaultStorageDirectory = getAreaDefaultStorageDirectory;
exports.getAreaStorageLocations = getAreaStorageLocations;
exports.getDefaultFileAreaTag = getDefaultFileAreaTag;
exports.getFileAreaByTag = getFileAreaByTag;
exports.getFileEntryPath = getFileEntryPath;
exports.changeFileAreaWithOptions = changeFileAreaWithOptions;
exports.scanFile = scanFile;
exports.scanFileAreaForChanges = scanFileAreaForChanges;
exports.getDescFromFileName = getDescFromFileName;
exports.getAreaStats = getAreaStats;
exports.cleanUpTempSessionItems = cleanUpTempSessionItems;
// for scheduler:
exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent;
// for scheduler:
exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent;
const WellKnownAreaTags = exports.WellKnownAreaTags = {
Invalid : '',
MessageAreaAttach : 'system_message_attachment',
TempDownloads : 'system_temporary_download',
const WellKnownAreaTags = exports.WellKnownAreaTags = {
Invalid : '',
MessageAreaAttach : 'system_message_attachment',
TempDownloads : 'system_temporary_download',
};
function startup(cb) {
@ -65,7 +65,7 @@ function isInternalArea(areaTag) {
function getAvailableFileAreas(client, 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 } ));
return _.omitBy(allAreas, areaInfo => {
@ -74,11 +74,11 @@ function getAvailableFileAreas(client, options) {
}
if(options.skipAcsCheck) {
return false; // no ACS checks (below)
return false; // no ACS checks (below)
}
if(options.writeAcs && !client.acs.hasFileAreaWrite(areaInfo)) {
return true; // omit
return true; // omit
}
return !client.acs.hasFileAreaRead(areaInfo);
@ -116,8 +116,8 @@ function getDefaultFileAreaTag(client, disableAcsCheck) {
function getFileAreaByTag(areaTag) {
const areaInfo = Config().fileBase.areas[areaTag];
if(areaInfo) {
areaInfo.areaTag = areaTag; // convienence!
areaInfo.storage = getAreaStorageLocations(areaInfo);
areaInfo.areaTag = areaTag; // convienence!
areaInfo.storage = getAreaStorageLocations(areaInfo);
return areaInfo;
}
}
@ -183,8 +183,8 @@ function getAreaStorageLocations(areaInfo) {
return _.compact(storageTags.map(storageTag => {
if(avail[storageTag]) {
return {
storageTag : storageTag,
dir : getAreaStorageDirectoryByTag(storageTag),
storageTag : storageTag,
dir : getAreaStorageDirectoryByTag(storageTag),
};
}
}));
@ -202,14 +202,14 @@ function getExistingFileEntriesBySha256(sha256, cb) {
FileDb.each(
`SELECT file_id, area_tag
FROM file
WHERE file_sha256=?;`,
FROM file
WHERE file_sha256=?;`,
[ sha256 ],
(err, fileRow) => {
if(fileRow) {
entries.push({
fileId : fileRow.file_id,
areaTag : fileRow.area_tag,
fileId : fileRow.file_id,
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) {
let eof = data.length;
const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE)
let eof = data.length;
const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE)
for(let i = eof - 1; i > stopPos; i--) {
if(0x1a === data[i]) {
@ -234,8 +234,8 @@ function sliceAtSauceMarker(data) {
}
function attemptSetEstimatedReleaseDate(fileEntry) {
// :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'));
// :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'));
function getMatch(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)
// with the assumption that must be wrong.
// Throw out anything that is current_year + 2 (we give some leway)
// with the assumption that must be wrong.
//
const maxYear = moment().add(2, 'year').year();
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) {
if(Log) {
Log.debug(obj, msg);
@ -290,8 +290,8 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
async.waterfall(
[
function extractDescFiles(callback) {
// :TODO: would be nice if these RegExp's were cached
// :TODO: this is long winded...
// :TODO: would be nice if these RegExp's were cached
// :TODO: this is long winded...
const config = Config();
const extractList = [];
@ -327,8 +327,8 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
}
const descFiles = {
desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null,
descLong : longDescFile ? paths.join(tempDir, longDescFile.fileName) : null,
desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null,
descLong : longDescFile ? paths.join(tempDir, longDescFile.fileName) : null,
};
return callback(null, descFiles);
@ -348,7 +348,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
return next(null);
}
// skip entries that are too large
// skip entries that are too large
const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`;
if(config.fileBase[maxFileSizeKey] && stats.size > config.fileBase[maxFileSizeKey]) {
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...
fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437');
fileEntry[`${descType}Src`] = 'descFile';
// :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}Src`] = 'descFile';
return next(null);
});
});
}, () => {
// cleanup but don't wait
// cleanup but don't wait
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' );
});
return callback(null);
@ -390,7 +390,7 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries
async.waterfall(
[
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) => {
if(err) {
return callback(err);
@ -398,7 +398,7 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries
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);
archiveUtil.extractTo(filePath, tempDir, fileEntry.meta.archive_type, extractList, err => {
@ -413,8 +413,8 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries
function processSingleExtractedFile(extractedFile, callback) {
populateFileEntryInfoFromFile(fileEntry, extractedFile, err => {
if(!fileEntry.desc) {
fileEntry.desc = getDescFromFileName(filePath);
fileEntry.descSrc = 'fileName';
fileEntry.desc = getDescFromFileName(filePath);
fileEntry.descSrc = 'fileName';
}
return callback(err);
});
@ -427,8 +427,8 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries
}
function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, cb) {
const archiveUtil = ArchiveUtil.getInstance();
const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive()
const archiveUtil = ArchiveUtil.getInstance();
const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive()
async.waterfall(
[
@ -449,7 +449,7 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c
}
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) {
//
// If we have a -single- entry in the archive, extract that file
// and try retrieving info in the non-archive manor. This should
// work for things like zipped up .pdf files.
// If we have a -single- entry in the archive, extract that file
// and try retrieving info in the non-archive manor. This should
// work for things like zipped up .pdf files.
//
// Otherwise, try to find particular desc files such as FILE_ID.DIZ
// and README.1ST
// Otherwise, try to find particular desc files such as FILE_ID.DIZ
// and README.1ST
//
const archDescHandler = (1 === entries.length) ? extractAndProcessSingleArchiveEntry : extractAndProcessDescFiles;
archDescHandler(fileEntry, filePath, entries, err => {
@ -494,7 +494,7 @@ function getInfoExtractUtilForDesc(mimeType, filePath, descType) {
let fileType = _.get(config, [ 'fileTypes', mimeType ] );
if(Array.isArray(fileType)) {
// further refine by extention
// further refine by extention
fileType = fileType.find(ft => paths.extname(filePath) === ft.ext);
}
@ -542,17 +542,17 @@ function populateFileEntryInfoFromFile(fileEntry, filePath, cb) {
const key = 'short' === descType ? 'desc' : 'descLong';
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');
}
fileEntry[key] = stdout;
fileEntry[`${key}Src`] = 'infoTool';
fileEntry[key] = stdout;
fileEntry[`${key}Src`] = 'infoTool';
}
}
@ -574,8 +574,8 @@ function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb
function getDescriptions(callback) {
populateFileEntryInfoFromFile(fileEntry, filePath, err => {
if(!fileEntry.desc) {
fileEntry.desc = getDescFromFileName(filePath);
fileEntry.descSrc = 'fileName';
fileEntry.desc = getDescFromFileName(filePath);
fileEntry.descSrc = 'fileName';
}
return callback(err);
});
@ -592,7 +592,7 @@ function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, 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(
[
@ -611,26 +611,26 @@ const HASH_NAMES = [ 'sha1', 'sha256', 'md5', 'crc32' ];
function scanFile(filePath, options, iterator, cb) {
if(3 === arguments.length && _.isFunction(iterator)) {
cb = iterator;
iterator = null;
cb = iterator;
iterator = null;
} else if(2 === arguments.length && _.isFunction(options)) {
cb = options;
iterator = null;
options = {};
cb = options;
iterator = null;
options = {};
}
const fileEntry = new FileEntry({
areaTag : options.areaTag,
meta : options.meta,
hashTags : options.hashTags, // Set() or Array
fileName : paths.basename(filePath),
storageTag : options.storageTag,
fileSha256 : options.sha256, // caller may know this already
areaTag : options.areaTag,
meta : options.meta,
hashTags : options.hashTags, // Set() or Array
fileName : paths.basename(filePath),
storageTag : options.storageTag,
fileSha256 : options.sha256, // caller may know this already
});
const stepInfo = {
filePath : filePath,
fileName : paths.basename(filePath),
filePath : filePath,
fileName : paths.basename(filePath),
};
const callIter = (next) => {
@ -638,8 +638,8 @@ function scanFile(filePath, options, iterator, cb) {
};
const readErrorCallIter = (origError, next) => {
stepInfo.step = 'read_error';
stepInfo.error = origError.message;
stepInfo.step = 'read_error';
stepInfo.error = origError.message;
callIter( () => {
return next(origError);
@ -648,7 +648,7 @@ function scanFile(filePath, options, iterator, cb) {
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 => {
if('sha256' === hn && fileEntry.fileSha256) {
return false;
@ -669,8 +669,8 @@ function scanFile(filePath, options, iterator, cb) {
return readErrorCallIter(err, callback);
}
stepInfo.step = 'start';
stepInfo.byteSize = fileEntry.meta.byte_size = stats.size;
stepInfo.step = 'start';
stepInfo.byteSize = fileEntry.meta.byte_size = stats.size;
return callIter(callback);
});
@ -694,9 +694,9 @@ function scanFile(filePath, options, iterator, cb) {
};
//
// Note that we are not using fs.createReadStream() here:
// While convenient, it is quite a bit slower -- which adds
// up to many seconds in time for larger files.
// Note that we are not using fs.createReadStream() here:
// While convenient, it is quite a bit slower -- which adds
// up to many seconds in time for larger files.
//
const chunkSize = 1024 * 64;
const buffer = new Buffer(chunkSize);
@ -714,7 +714,7 @@ function scanFile(filePath, options, iterator, cb) {
}
if(0 === bytesRead) {
// done - finalize
// done - finalize
fileEntry.meta.byte_size = stepInfo.bytesProcessed;
for(let i = 0; i < hashesToCalc.length; ++i) {
@ -733,11 +733,11 @@ function scanFile(filePath, options, iterator, cb) {
return callIter(callback);
}
stepInfo.bytesProcessed += bytesRead;
stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100));
stepInfo.bytesProcessed += bytesRead;
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;
if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) {
@ -745,7 +745,7 @@ function scanFile(filePath, options, iterator, cb) {
return nextChunk();
} else {
lastCalcHashPercent = stepInfo.calcHashPercent;
stepInfo.step = 'hash_update';
stepInfo.step = 'hash_update';
callIter(err => {
if(err) {
@ -767,7 +767,7 @@ function scanFile(filePath, options, iterator, cb) {
archiveUtil.detectType(filePath, (err, archiveType) => {
if(archiveType) {
// save this off
// save this off
fileEntry.meta.archive_type = archiveType;
populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => {
@ -776,7 +776,7 @@ function scanFile(filePath, options, iterator, cb) {
if(err) {
logDebug( { error : err.message }, 'Non-archive file entry population failed');
}
return callback(null); // ignore err
return callback(null); // ignore err
});
} else {
return callback(null);
@ -787,7 +787,7 @@ function scanFile(filePath, options, iterator, cb) {
if(err) {
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) {
if(3 === arguments.length && _.isFunction(iterator)) {
cb = iterator;
iterator = null;
cb = iterator;
iterator = null;
} else if(2 === arguments.length && _.isFunction(options)) {
cb = options;
iterator = null;
options = {};
cb = options;
iterator = null;
options = {};
}
const storageLocations = getAreaStorageLocations(areaInfo);
@ -842,8 +842,8 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
fs.stat(fullPath, (err, stats) => {
if(err) {
// :TODO: Log me!
return nextFile(null); // always try next file
// :TODO: Log me!
return nextFile(null); // always try next file
}
if(!stats.isFile()) {
@ -853,18 +853,18 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
scanFile(
fullPath,
{
areaTag : areaInfo.areaTag,
storageTag : storageLoc.storageTag
areaTag : areaInfo.areaTag,
storageTag : storageLoc.storageTag
},
iterator,
(err, fileEntry, dupeEntries) => {
if(err) {
// :TODO: Log me!!!
return nextFile(null); // try next anyway
// :TODO: Log me!!!
return nextFile(null); // try next anyway
}
if(dupeEntries.length > 0) {
// :TODO: Handle duplidates -- what to do here???
// :TODO: Handle duplidates -- what to do here???
} else {
if(Array.isArray(options.tags)) {
options.tags.forEach(tag => {
@ -872,7 +872,7 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
});
}
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);
});
}
@ -885,7 +885,7 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
});
},
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);
}
],
@ -900,7 +900,7 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
}
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 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>,
// totalBytes : <totalByteSize>,
// areas : {
// <areaTag> : {
// files : <fileCount>,
// bytes : <byteSize>
// }
// }
// }
// totalFiles : <totalFileCount>,
// totalBytes : <totalByteSize>,
// areas : {
// <areaTag> : {
// files : <fileCount>,
// bytes : <byteSize>
// }
// }
// }
//
function getAreaStats(cb) {
FileDb.all(
`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
WHERE f.file_id = m.file_id AND m.meta_name='byte_size'
GROUP BY f.area_tag;`,
FROM file f, file_meta m
WHERE f.file_id = m.file_id AND m.meta_name='byte_size'
GROUP BY f.area_tag;`,
(err, statRows) => {
if(err) {
return cb(err);
@ -946,8 +946,8 @@ function getAreaStats(cb) {
stats.areas = stats.areas || {};
stats.areas[v.area_tag] = {
files : v.total_files,
bytes : v.total_byte_size,
files : v.total_files,
bytes : v.total_byte_size,
};
return stats;
}, {})
@ -956,7 +956,7 @@ function getAreaStats(cb) {
);
}
// method exposed for event scheduler
// method exposed for event scheduler
function updateAreaStatsScheduledEvent(args, cb) {
getAreaStats( (err, stats) => {
if(!err) {
@ -968,13 +968,13 @@ function updateAreaStatsScheduledEvent(args, cb) {
}
function cleanUpTempSessionItems(cb) {
// find (old) temporary session items and nuke 'em
// find (old) temporary session items and nuke 'em
const filter = {
areaTag : WellKnownAreaTags.TempDownloads,
metaPairs : [
areaTag : WellKnownAreaTags.TempDownloads,
metaPairs : [
{
name : 'session_temp_dl',
value : 1
name : 'session_temp_dl',
value : 1
}
]
};

View File

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

View File

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

View File

@ -1,13 +1,13 @@
/* jslint node: true */
'use strict';
// deps
const _ = require('lodash');
const uuidV4 = require('uuid/v4');
// deps
const _ = require('lodash');
const uuidV4 = require('uuid/v4');
module.exports = class FileBaseFilters {
constructor(client) {
this.client = client;
this.client = client;
this.load();
}
@ -74,7 +74,7 @@ module.exports = class FileBaseFilters {
try {
this.filters = JSON.parse(filtersProperty);
} 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;
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() {
const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329';
const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329';
const filters = {
[ U_LATEST ] : {
name : 'By Date Added',
areaTag : '', // all
terms : '', // *
tags : '', // *
order : 'descending',
sort : 'upload_timestamp',
uuid : U_LATEST,
system : true,
name : 'By Date Added',
areaTag : '', // all
terms : '', // *
tags : '', // *
order : 'descending',
sort : 'upload_timestamp',
uuid : U_LATEST,
system : true,
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
/* jslint node: true */
'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 {
constructor(data) {
this.hash = 0x811c9dc5;
@ -29,8 +29,8 @@ module.exports = class FNV1a {
for(let b of data) {
this.hash = this.hash ^ b;
this.hash +=
(this.hash << 24) + (this.hash << 8) + (this.hash << 7) +
(this.hash << 4) + (this.hash << 1);
(this.hash << 24) + (this.hash << 8) + (this.hash << 7) +
(this.hash << 4) + (this.hash << 1);
}
return this;

View File

@ -1,118 +1,118 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController;
const ansi = require('./ansi_term.js');
const theme = require('./theme.js');
const Message = require('./message.js');
const updateMessageAreaLastReadId = require('./message_area.js').updateMessageAreaLastReadId;
const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag;
const User = require('./user.js');
const StatLog = require('./stat_log.js');
const stringFormat = require('./string_format.js');
const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher;
const { isAnsi, cleanControlCodes, insert } = require('./string_util.js');
const Config = require('./config.js').get;
const { getAddressedToInfo } = require('./mail_util.js');
// ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController;
const ansi = require('./ansi_term.js');
const theme = require('./theme.js');
const Message = require('./message.js');
const updateMessageAreaLastReadId = require('./message_area.js').updateMessageAreaLastReadId;
const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag;
const User = require('./user.js');
const StatLog = require('./stat_log.js');
const stringFormat = require('./string_format.js');
const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher;
const { isAnsi, cleanControlCodes, insert } = require('./string_util.js');
const Config = require('./config.js').get;
const { getAddressedToInfo } = require('./mail_util.js');
// deps
const async = require('async');
const assert = require('assert');
const _ = require('lodash');
const moment = require('moment');
// deps
const async = require('async');
const assert = require('assert');
const _ = require('lodash');
const moment = require('moment');
exports.moduleInfo = {
name : 'Full Screen Editor (FSE)',
desc : 'A full screen editor/viewer',
author : 'NuSkooler',
name : 'Full Screen Editor (FSE)',
desc : 'A full screen editor/viewer',
author : 'NuSkooler',
};
const MciViewIds = {
header : {
from : 1,
to : 2,
subject : 3,
errorMsg : 4,
modTimestamp : 5,
msgNum : 6,
msgTotal : 7,
from : 1,
to : 2,
subject : 3,
errorMsg : 4,
modTimestamp : 5,
msgNum : 6,
msgTotal : 7,
customRangeStart : 10, // 10+ = customs
customRangeStart : 10, // 10+ = customs
},
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 : {
MsgNum : 6,
MsgTotal : 7,
// :TODO: Just use custom ranges
MsgNum : 6,
MsgTotal : 7,
// :TODO: Just use custom ranges
},
quoteBuilder : {
quotedMsg : 1,
// 2 NYI
quoteLines : 3,
quotedMsg : 1,
// 2 NYI
quoteLines : 3,
}
};
/*
Custom formatting:
header
fromUserName
toUserName
Custom formatting:
header
fromUserName
toUserName
fromRealName (may be fromUserName) NYI
toRealName (may be toUserName) NYI
fromRealName (may be fromUserName) NYI
toRealName (may be toUserName) NYI
fromRemoteUser (may be "N/A")
toRemoteUser (may be "N/A")
subject
modTimestamp
msgNum
msgTotal (in area)
messageId
fromRemoteUser (may be "N/A")
toRemoteUser (may be "N/A")
subject
modTimestamp
msgNum
msgTotal (in area)
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) {
constructor(options) {
super(options);
const self = this;
const config = this.menuConfig.config;
const self = this;
const config = this.menuConfig.config;
//
// menuConfig.config:
// editorType : email | area
// editorMode : view | edit | quote
// menuConfig.config:
// editorType : email | area
// editorMode : view | edit | quote
//
// menuConfig.config or extraArgs
// messageAreaTag
// messageIndex / messageTotal
// toUserId
// menuConfig.config or extraArgs
// messageAreaTag
// messageIndex / messageTotal
// toUserId
//
this.editorType = config.editorType;
this.editorMode = config.editorMode;
this.editorType = config.editorType;
this.editorMode = config.editorMode;
if(config.messageAreaTag) {
// :TODO: swtich to this.config.messageAreaTag so we can follow Object.assign pattern for config/extraArgs
this.messageAreaTag = config.messageAreaTag;
// :TODO: swtich to this.config.messageAreaTag so we can follow Object.assign pattern for config/extraArgs
this.messageAreaTag = config.messageAreaTag;
}
this.messageIndex = config.messageIndex || 0;
this.messageTotal = config.messageTotal || 0;
this.toUserId = config.toUserId || 0;
this.messageIndex = config.messageIndex || 0;
this.messageTotal = config.messageTotal || 0;
this.toUserId = config.toUserId || 0;
// extraArgs can override some config
// extraArgs can override some config
if(_.isObject(options.extraArgs)) {
if(options.extraArgs.messageAreaTag) {
this.messageAreaTag = options.extraArgs.messageAreaTag;
@ -140,7 +140,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
this.menuMethods = {
//
// Validation stuff
// Validation stuff
//
viewValidationListener : function(err, cb) {
var errMsgView = self.viewControllers.header.getView(MciViewIds.header.errorMsg);
@ -150,7 +150,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
errMsgView.setText(err.message);
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 {
errMsgView.clearText();
@ -201,19 +201,19 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
if(self.newQuoteBlock) {
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());
}
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);
//
// 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
// 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
//
if(quoteListView.getData() !== quoteListView.getCount() - 1) {
@ -229,18 +229,18 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
return cb(null);
},
/*
replyDiscard : function(formData, extraArgs) {
// :TODO: need to prompt yes/no
// :TODO: @method for fallback would be better
self.prevMenu();
},
*/
replyDiscard : function(formData, extraArgs) {
// :TODO: need to prompt yes/no
// :TODO: @method for fallback would be better
self.prevMenu();
},
*/
editModeMenuHelp : function(formData, extraArgs, cb) {
self.viewControllers.footerEditorMenu.setFocus(false);
return self.displayHelp(cb);
},
///////////////////////////////////////////////////////////////////////
// View Mode
// View Mode
///////////////////////////////////////////////////////////////////////
viewModeMenuHelp : function(formData, extraArgs, cb) {
self.viewControllers.footerView.setFocus(false);
@ -266,43 +266,43 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}
getFooterName() {
return 'footer' + _.upperFirst(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ...
return 'footer' + _.upperFirst(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ...
}
getFormId(name) {
return {
header : 0,
body : 1,
footerEditor : 2,
footerEditorMenu : 3,
footerView : 4,
quoteBuilder : 5,
header : 0,
body : 1,
footerEditor : 2,
footerEditorMenu : 3,
footerView : 4,
quoteBuilder : 5,
help : 50,
help : 50,
}[name];
}
getHeaderFormatObj() {
const remoteUserNotAvail = this.menuConfig.config.remoteUserNotAvail || 'N/A';
const localUserIdNotAvail = this.menuConfig.config.localUserIdNotAvail || 'N/A';
const modTimestampFormat = this.menuConfig.config.modTimestampFormat || this.client.currentTheme.helpers.getDateTimeFormat();
const remoteUserNotAvail = this.menuConfig.config.remoteUserNotAvail || 'N/A';
const localUserIdNotAvail = this.menuConfig.config.localUserIdNotAvail || 'N/A';
const modTimestampFormat = this.menuConfig.config.modTimestampFormat || this.client.currentTheme.helpers.getDateTimeFormat();
return {
// :TODO: ensure we show real names for form/to if they are enforced in the area
fromUserName : this.message.fromUserName,
toUserName : this.message.toUserName,
// :TODO:
// :TODO: ensure we show real names for form/to if they are enforced in the area
fromUserName : this.message.fromUserName,
toUserName : this.message.toUserName,
// :TODO:
//fromRealName
//toRealName
fromUserId : _.get(this.message, 'meta.System.local_from_user_id', localUserIdNotAvail),
toUserId : _.get(this.message, 'meta.System.local_to_user_id', localUserIdNotAvail),
fromRemoteUser : _.get(this.message, 'meta.System.remote_from_user', remoteUserNotAvail),
toRemoteUser : _.get(this.messgae, 'meta.System.remote_to_user', remoteUserNotAvail),
subject : this.message.subject,
modTimestamp : this.message.modTimestamp.format(modTimestampFormat),
msgNum : this.messageIndex + 1,
msgTotal : this.messageTotal,
messageId : this.message.messageId,
fromUserId : _.get(this.message, 'meta.System.local_from_user_id', localUserIdNotAvail),
toUserId : _.get(this.message, 'meta.System.local_to_user_id', localUserIdNotAvail),
fromRemoteUser : _.get(this.message, 'meta.System.remote_from_user', remoteUserNotAvail),
toRemoteUser : _.get(this.messgae, 'meta.System.remote_to_user', remoteUserNotAvail),
subject : this.message.subject,
modTimestamp : this.message.modTimestamp.format(modTimestampFormat),
msgNum : this.messageIndex + 1,
msgTotal : this.messageTotal,
messageId : this.message.messageId,
};
}
@ -317,26 +317,26 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
const headerValues = this.viewControllers.header.getFormData().value;
const msgOpts = {
areaTag : this.messageAreaTag,
toUserName : headerValues.to,
fromUserName : this.client.user.username,
subject : headerValues.subject,
// :TODO: don't hard code 1 here:
message : this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ),
areaTag : this.messageAreaTag,
toUserName : headerValues.to,
fromUserName : this.client.user.username,
subject : headerValues.subject,
// :TODO: don't hard code 1 here:
message : this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ),
};
if(this.isReply()) {
msgOpts.replyToMsgId = this.replyToMessage.messageId;
msgOpts.replyToMsgId = this.replyToMessage.messageId;
if(this.replyIsAnsi) {
//
// Ensure first characters indicate ANSI for detection down
// the line (other boards/etc.). We also set explicit_encoding
// to packetAnsiMsgEncoding (generally cp437) as various boards
// really don't like ANSI messages in UTF-8 encoding (they should!)
// Ensure first characters indicate ANSI for detection down
// the line (other boards/etc.). We also set explicit_encoding
// to packetAnsiMsgEncoding (generally cp437) as various boards
// 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.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${msgOpts.message}`;
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}`;
}
}
@ -363,18 +363,18 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
this.initHeaderViewMode();
this.initFooterViewMode();
const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message);
let msg = this.message.message;
const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message);
let msg = this.message.message;
if(bodyMessageView && _.has(this, 'message.message')) {
//
// 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
// how the author wanted it
// 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
// how the author wanted it
//
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);
@ -383,10 +383,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}
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,
forceLineTerm : true,
prepped : false,
forceLineTerm : true,
}
);
} else {
@ -404,7 +404,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
[
function buildIfNecessary(callback) {
if(self.isEditMode()) {
return self.buildMessage(callback); // creates initial self.message
return self.buildMessage(callback); // creates initial self.message
}
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
// don't try to look up the local user ID. Instead, mark the mail
// for export with the remote to address.
// 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
// for export with the remote to address.
//
if(self.replyToMessage && self.replyToMessage.isFromRemoteUser()) {
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);
if(addressedToInfo.name && Message.AddressFlavor.FTN === addressedToInfo.flavor) {
self.message.setRemoteToUser(addressedToInfo.remote);
@ -444,7 +444,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
return callback(null);
}
// we need to look it up
// we need to look it up
User.getUserIdAndNameByLookup(self.message.toUserName, (err, toUserId) => {
if(err) {
return callback(err);
@ -466,7 +466,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
if(cb) {
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);
@ -479,9 +479,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
[
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;
self.client.term.rawWrite(ansi.goto(footerRow, 1));
@ -489,10 +489,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
},
function clearFooterArea(callback) {
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
// in NetRunner:
// :TODO: We'd like to delete up to N rows, but this does not work
// in NetRunner:
self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3));
self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2));
@ -519,9 +519,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}
redrawScreen(cb) {
var comps = [ 'header', 'body' ];
const self = this;
var art = self.menuConfig.config.art;
var comps = [ 'header', 'body' ];
const self = this;
var art = self.menuConfig.config.art;
self.client.term.rawWrite(ansi.resetScreen());
@ -543,7 +543,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
});
},
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) {
callback(err);
});
@ -577,9 +577,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
if(_.isUndefined(this.viewControllers[footerName])) {
var menuLoadOpts = {
callingMenu : this,
formId : formId,
mciMap : artData.mciMap
callingMenu : this,
formId : formId,
mciMap : artData.mciMap
};
this.addViewController(
@ -597,8 +597,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
initSequence() {
var mciData = { };
const self = this;
var art = self.menuConfig.config.art;
const self = this;
var art = self.menuConfig.config.art;
assert(_.isObject(art));
@ -659,7 +659,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
[
function header(callback) {
menuLoadOpts.formId = self.getFormId('header');
menuLoadOpts.mciMap = mciData.header.mciMap;
menuLoadOpts.mciMap = mciData.header.mciMap;
self.addViewController(
'header',
@ -669,8 +669,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
});
},
function body(callback) {
menuLoadOpts.formId = self.getFormId('body');
menuLoadOpts.mciMap = mciData.body.mciMap;
menuLoadOpts.formId = self.getFormId('body');
menuLoadOpts.mciMap = mciData.body.mciMap;
self.addViewController(
'body',
@ -698,12 +698,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
from.acceptsFocus = false;
//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);
self.updateTextEditMode(body.getTextEditMode());
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);
},
@ -767,22 +767,22 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
mciReadyHandler(mciData, cb) {
this.createInitialViews(mciData, err => {
// :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in
// place - if this is for existing usernames else validate spec
// :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in
// 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
self.validateToUserName(view.getData(), function result(err) {
if(err) {
// :TODO: display a error in a %TL area or such
view.clearText();
self.viewControllers.headers.switchFocus(2);
}
});
}
});*/
if(2 === view.id) { // "to" field
self.validateToUserName(view.getData(), function result(err) {
if(err) {
// :TODO: display a error in a %TL area or such
view.clearText();
self.viewControllers.headers.switchFocus(2);
}
});
}
});*/
cb(err);
});
@ -793,7 +793,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
var posView = this.viewControllers.footerEditor.getView(1);
if(posView) {
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'));
this.client.term.rawWrite(ansi.restorePos());
}
@ -816,16 +816,16 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}
initHeaderViewMode() {
this.setHeaderText(MciViewIds.header.from, this.message.fromUserName);
this.setHeaderText(MciViewIds.header.to, this.message.toUserName);
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.msgNum, (this.messageIndex + 1).toString());
this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString());
this.setHeaderText(MciViewIds.header.from, this.message.fromUserName);
this.setHeaderText(MciViewIds.header.to, this.message.toUserName);
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.msgNum, (this.messageIndex + 1).toString());
this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString());
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' ] );
}
@ -835,15 +835,15 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
this.setHeaderText(MciViewIds.header.to, this.replyToMessage.fromUserName);
//
// We want to prefix the subject with "RE: " only if it's not already
// that way -- avoid RE: RE: RE: RE: ...
// We want to prefix the subject with "RE: " only if it's not already
// that way -- avoid RE: RE: RE: RE: ...
//
let newSubj = this.replyToMessage.subject;
if(false === /^RE:\s+/i.test(newSubj)) {
newSubj = `RE: ${newSubj}`;
}
this.setHeaderText(MciViewIds.header.subject, newSubj);
this.setHeaderText(MciViewIds.header.subject, newSubj);
}
initFooterViewMode() {
@ -869,7 +869,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
displayQuoteBuilder() {
//
// Clear body area
// Clear body area
//
this.newQuoteBlock = true;
const self = this;
@ -877,10 +877,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
async.waterfall(
[
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(
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) {
callback(err, artData);
@ -891,9 +891,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
if(_.isUndefined(self.viewControllers.quoteBuilder)) {
var menuLoadOpts = {
callingMenu : self,
formId : formId,
mciMap : artData.mciMap,
callingMenu : self,
formId : formId,
mciMap : artData.mciMap,
};
self.addViewController(
@ -909,16 +909,16 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
},
function loadQuoteLines(callback) {
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(
{
termWidth : self.client.term.termWidth,
termHeight : self.client.term.termHeight,
cols : quoteView.dimens.width,
startCol : quoteView.position.col,
ansiResetSgr : bodyView.styleSGR1,
ansiFocusPrefixSgr : quoteView.styleSGR2,
termWidth : self.client.term.termWidth,
termHeight : self.client.term.termHeight,
cols : quoteView.dimens.width,
startCol : quoteView.position.col,
ansiResetSgr : bodyView.styleSGR1,
ansiFocusPrefixSgr : quoteView.styleSGR2,
},
(err, quoteLines, focusQuoteLines, replyIsAnsi) => {
if(err) {
@ -959,16 +959,16 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}
/*
this.observeViewPosition = function() {
self.viewControllers.body.getView(MciViewIds.body.message).on('edit position', function positionUpdate(pos) {
console.log(pos.percent + ' / ' + pos.below)
});
};
*/
this.observeViewPosition = function() {
self.viewControllers.body.getView(MciViewIds.body.message).on('edit position', function positionUpdate(pos) {
console.log(pos.percent + ' / ' + pos.below)
});
};
*/
switchToHeader() {
this.viewControllers.body.setFocus(false);
this.viewControllers.header.switchFocus(2); // to
this.viewControllers.header.switchFocus(2); // to
}
switchToBody() {
@ -982,7 +982,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
this.viewControllers.header.setFocus(false);
this.viewControllers.body.setFocus(false);
this.viewControllers[this.getFooterName()].switchFocus(1); // HM1
this.viewControllers[this.getFooterName()].switchFocus(1); // HM1
}
switchFromQuoteBuilderToBody() {
@ -991,7 +991,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
body.redraw();
this.viewControllers.body.switchFocus(1);
// :TODO: create method (DRY)
// :TODO: create method (DRY)
this.updateTextEditMode(body.getTextEditMode());
this.updateEditModePosition(body.getEditPosition());
@ -1000,11 +1000,11 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}
quoteBuilderFinalize() {
// :TODO: fix magic #'s
const quoteMsgView = this.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg);
const msgView = this.viewControllers.body.getView(MciViewIds.body.message);
// :TODO: fix magic #'s
const quoteMsgView = this.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg);
const msgView = this.viewControllers.body.getView(MciViewIds.body.message);
let quoteLines = quoteMsgView.getData().trim();
let quoteLines = quoteMsgView.getData().trim();
if(quoteLines.length > 0) {
if(this.replyIsAnsi) {
@ -1034,8 +1034,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
const dtFormat = this.menuConfig.config.quoteDateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat();
return stringFormat(quoteFormat, {
dateTime : moment(this.replyToMessage.modTimestamp).format(dtFormat),
userName : this.replyToMessage.fromUserName,
dateTime : moment(this.replyToMessage.modTimestamp).format(dtFormat),
userName : this.replyToMessage.fromUserName,
});
}

View File

@ -1,7 +1,7 @@
/* jslint node: true */
'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_PATTERN_REGEXP = /^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i;
@ -25,7 +25,7 @@ module.exports = class Address {
}
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);
}
@ -36,10 +36,10 @@ module.exports = class Address {
return (
this.net === other.net &&
this.node === other.node &&
this.zone === other.zone &&
this.point === other.point &&
this.domain === other.domain
this.node === other.node &&
this.zone === other.zone &&
this.point === other.point &&
this.domain === other.domain
);
}
@ -95,36 +95,36 @@ module.exports = class Address {
}
/*
getMatchScore(pattern) {
let score = 0;
const addr = this.getMatchAddr(pattern);
if(addr) {
const PARTS = [ 'net', 'node', 'zone', 'point', 'domain' ];
for(let i = 0; i < PARTS.length; ++i) {
const member = PARTS[i];
if(this[member] === addr[member]) {
score += 2;
} else if('*' === addr[member]) {
score += 1;
} else {
break;
}
}
}
getMatchScore(pattern) {
let score = 0;
const addr = this.getMatchAddr(pattern);
if(addr) {
const PARTS = [ 'net', 'node', 'zone', 'point', 'domain' ];
for(let i = 0; i < PARTS.length; ++i) {
const member = PARTS[i];
if(this[member] === addr[member]) {
score += 2;
} else if('*' === addr[member]) {
score += 1;
} else {
break;
}
}
}
return score;
}
*/
return score;
}
*/
isPatternMatch(pattern) {
const addr = this.getMatchAddr(pattern);
if(addr) {
return (
('*' === addr.net || this.net === addr.net) &&
('*' === addr.node || this.node === addr.node) &&
('*' === addr.zone || this.zone === addr.zone) &&
('*' === addr.point || this.point === addr.point) &&
('*' === addr.domain || this.domain === addr.domain)
('*' === addr.node || this.node === addr.node) &&
('*' === addr.zone || this.zone === addr.zone) &&
('*' === addr.point || this.point === addr.point) &&
('*' === addr.domain || this.domain === addr.domain)
);
}
@ -137,8 +137,8 @@ module.exports = class Address {
if(m) {
// start with a 2D
let addr = {
net : parseInt(m[2]),
node : parseInt(m[3].substr(1)),
net : parseInt(m[2]),
node : parseInt(m[3].substr(1)),
};
// 3D: Addition of zone if present
@ -165,14 +165,14 @@ module.exports = class Address {
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]);
if(dim >= 3) {
addrStr += `/${this.node}`;
}
// missing & .0 are equiv for point
// missing & .0 are equiv for point
if(dim >= 4 && this.point) {
addrStr += `.${this.point}`;
}

View File

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

View File

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

View File

@ -1,21 +1,21 @@
/* jslint node: true */
'use strict';
const MenuView = require('./menu_view.js').MenuView;
const strUtil = require('./string_util.js');
const formatString = require('./string_format');
const { pipeToAnsi } = require('./color_codes.js');
const { goto } = require('./ansi_term.js');
const MenuView = require('./menu_view.js').MenuView;
const strUtil = require('./string_util.js');
const formatString = require('./string_format');
const { pipeToAnsi } = require('./color_codes.js');
const { goto } = require('./ansi_term.js');
const assert = require('assert');
const _ = require('lodash');
const assert = require('assert');
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) {
options.cursor = options.cursor || 'hide';
options.cursor = options.cursor || 'hide';
if(!_.isNumber(options.itemSpacing)) {
options.itemSpacing = 1;
@ -23,7 +23,7 @@ function HorizontalMenuView(options) {
MenuView.call(this, options);
this.dimens.height = 1; // always the case
this.dimens.height = 1; // always the case
var self = this;
@ -33,8 +33,8 @@ function HorizontalMenuView(options) {
this.performAutoScale = function() {
if(self.autoScale.width) {
var spacer = self.getSpacer();
var width = self.items.join(spacer).length + (spacer.length * 2);
var spacer = self.getSpacer();
var width = self.items.join(spacer).length + (spacer.length * 2);
assert(width <= self.client.term.termWidth - self.position.col);
self.dimens.width = width;
}
@ -44,8 +44,8 @@ function HorizontalMenuView(options) {
this.cachePositions = function() {
if(this.positionCacheExpired) {
var col = self.position.col;
var spacer = self.getSpacer();
var col = self.position.col;
var spacer = self.getSpacer();
for(var i = 0; i < self.items.length; ++i) {
self.items[i].col = col;
@ -90,7 +90,7 @@ require('util').inherits(HorizontalMenuView, MenuView);
HorizontalMenuView.prototype.setHeight = function(height) {
height = parseInt(height, 10);
assert(1 === height); // nothing else allowed here
assert(1 === height); // nothing else allowed here
HorizontalMenuView.super_.prototype.setHeight(this, height);
};
@ -130,7 +130,7 @@ HorizontalMenuView.prototype.focusNext = function() {
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();
HorizontalMenuView.super_.prototype.focusNext.call(this);
@ -144,7 +144,7 @@ HorizontalMenuView.prototype.focusPrevious = function() {
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();
HorizontalMenuView.super_.prototype.focusPrevious.call(this);

View File

@ -1,12 +1,12 @@
/* jslint node: true */
'use strict';
const View = require('./view.js').View;
const valueWithDefault = require('./misc_util.js').valueWithDefault;
const isPrintable = require('./string_util.js').isPrintable;
const stylizeString = require('./string_util.js').stylizeString;
const View = require('./view.js').View;
const valueWithDefault = require('./misc_util.js').valueWithDefault;
const isPrintable = require('./string_util.js').isPrintable;
const stylizeString = require('./string_util.js').stylizeString;
const _ = require('lodash');
const _ = require('lodash');
module.exports = class KeyEntryView extends View {
constructor(options) {
@ -15,8 +15,8 @@ module.exports = class KeyEntryView extends View {
super(options);
this.eatTabKey = options.eatTabKey || true;
this.caseInsensitive = options.caseInsensitive || true;
this.eatTabKey = options.eatTabKey || true;
this.caseInsensitive = options.caseInsensitive || true;
if(Array.isArray(options.keys)) {
if(this.caseInsensitive) {
@ -35,7 +35,7 @@ module.exports = class KeyEntryView extends View {
}
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));
}
@ -46,7 +46,7 @@ module.exports = class KeyEntryView extends View {
}
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) {
@ -73,5 +73,5 @@ module.exports = class KeyEntryView extends View {
super.setPropertyValue(propName, propValue);
}
getData() { return this.keyEntered; }
getData() { return this.keyEntered; }
};

View File

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

View File

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

View File

@ -1,21 +1,21 @@
/* jslint node: true */
'use strict';
// deps
const bunyan = require('bunyan');
const paths = require('path');
const fs = require('graceful-fs');
const _ = require('lodash');
// deps
const bunyan = require('bunyan');
const paths = require('path');
const fs = require('graceful-fs');
const _ = require('lodash');
module.exports = class Log {
static init() {
const Config = require('./config.js').get();
const logPath = Config.paths.logs;
const Config = require('./config.js').get();
const logPath = Config.paths.logs;
const err = this.checkLogPath(logPath);
if(err) {
console.error(err.message); // eslint-disable-line no-console
console.error(err.message); // eslint-disable-line no-console
return process.exit();
}
@ -26,18 +26,18 @@ module.exports = class Log {
}
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 => {
serializers[keyName] = (fd) => Log.hideSensitive(fd);
});
this.log = bunyan.createLogger({
name : 'ENiGMA½ BBS',
streams : logStreams,
serializers : serializers,
name : 'ENiGMA½ BBS',
streams : logStreams,
serializers : serializers,
});
}
@ -59,7 +59,7 @@ module.exports = class Log {
static hideSensitive(obj) {
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(
JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => {
@ -67,7 +67,7 @@ module.exports = class Log {
})
);
} catch(e) {
// be safe and return empty obj!
// be safe and return empty obj!
return {};
}
}

View File

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

View File

@ -1,16 +1,16 @@
/* jslint node: true */
'use strict';
var events = require('events');
var assert = require('assert');
var _ = require('lodash');
var events = require('events');
var assert = require('assert');
var _ = require('lodash');
module.exports = MailPacket;
function MailPacket(options) {
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 || {};
}
@ -18,19 +18,19 @@ require('util').inherits(MailPacket, events.EventEmitter);
MailPacket.prototype.read = function(options) {
//
// options.packetPath | opts.packetBuffer: supplies a path-to-file
// or a buffer containing packet data
// options.packetPath | opts.packetBuffer: supplies a path-to-file
// 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));
};
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));
};

View File

@ -1,25 +1,25 @@
/* jslint node: true */
'use strict';
const Address = require('./ftn_address.js');
const Message = require('./message.js');
const Address = require('./ftn_address.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,}))$/;
/*
Input Output
----------------------------------------------------------------------------------------------------
User { name : 'User', flavor : 'local' }
Some User { name : 'Some User', flavor : 'local' }
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' }
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' }
43:20/100.2 { flavor : 'ftn', remote : '43:20/100.2' }
foo@host.com { name : 'foo', flavor : 'email', remote : 'foo@host.com' }
Bar <baz@foobar.net> { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' }
Input Output
----------------------------------------------------------------------------------------------------
User { name : 'User', flavor : 'local' }
Some User { name : 'Some User', flavor : 'local' }
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' }
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' }
43:20/100.2 { flavor : 'ftn', remote : '43:20/100.2' }
foo@host.com { name : 'foo', flavor : 'email', remote : 'foo@host.com' }
Bar <baz@foobar.net> { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' }
*/
function getAddressedToInfo(input) {
input = input.trim();
@ -50,8 +50,8 @@ function getAddressedToInfo(input) {
return { name : input, flavor : Message.AddressFlavor.Local };
}
const lessThanPos = input.indexOf('<');
const greaterThanPos = input.indexOf('>');
const lessThanPos = input.indexOf('<');
const greaterThanPos = input.indexOf('>');
if(lessThanPos > 0 && greaterThanPos > lessThanPos) {
const addr = input.slice(lessThanPos + 1, greaterThanPos);
const m = addr.match(EMAIL_REGEX);
@ -67,7 +67,7 @@ function getAddressedToInfo(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)) {
return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,96 +1,96 @@
/* jslint node: true */
'use strict';
const msgDb = require('./database.js').dbs.message;
const wordWrapText = require('./word_wrap.js').wordWrapText;
const ftnUtil = require('./ftn_util.js');
const createNamedUUID = require('./uuid_util.js').createNamedUUID;
const Errors = require('./enig_error.js').Errors;
const ANSI = require('./ansi_term.js');
const msgDb = require('./database.js').dbs.message;
const wordWrapText = require('./word_wrap.js').wordWrapText;
const ftnUtil = require('./ftn_util.js');
const createNamedUUID = require('./uuid_util.js').createNamedUUID;
const Errors = require('./enig_error.js').Errors;
const ANSI = require('./ansi_term.js');
const {
sanatizeString,
getISOTimestampString } = require('./database.js');
getISOTimestampString } = require('./database.js');
const {
isAnsi, isFormattedLine,
splitTextAtTerms,
renderSubstr
} = require('./string_util.js');
} = require('./string_util.js');
const ansiPrep = require('./ansi_prep.js');
const ansiPrep = require('./ansi_prep.js');
// deps
const uuidParse = require('uuid-parse');
const async = require('async');
const _ = require('lodash');
const assert = require('assert');
const moment = require('moment');
const iconvEncode = require('iconv-lite').encode;
// deps
const uuidParse = require('uuid-parse');
const async = require('async');
const _ = require('lodash');
const assert = require('assert');
const moment = require('moment');
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 = {
Invalid : '',
Private : 'private_mail',
Bulletin : 'local_bulletin',
Invalid : '',
Private : 'private_mail',
Bulletin : 'local_bulletin',
};
const SYSTEM_META_NAMES = {
LocalToUserID : 'local_to_user_id',
LocalFromUserID : 'local_from_user_id',
StateFlags0 : 'state_flags0', // See Message.StateFlags0
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
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
LocalToUserID : 'local_to_user_id',
LocalFromUserID : 'local_from_user_id',
StateFlags0 : 'state_flags0', // See Message.StateFlags0
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
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
};
// Types for Message.SystemMetaNames.ExternalFlavor meta
// Types for Message.SystemMetaNames.ExternalFlavor meta
const ADDRESS_FLAVOR = {
Local : 'local', // local / non-remote addressing
FTN : 'ftn', // FTN style
Email : 'email',
Local : 'local', // local / non-remote addressing
FTN : 'ftn', // FTN style
Email : 'email',
};
const STATE_FLAGS0 = {
None : 0x00000000,
Imported : 0x00000001, // imported from foreign system
Exported : 0x00000002, // exported to foreign system
None : 0x00000000,
Imported : 0x00000001, // imported from foreign system
Exported : 0x00000002, // exported to foreign system
};
// :TODO: these should really live elsewhere...
// :TODO: these should really live elsewhere...
const FTN_PROPERTY_NAMES = {
// packet header oriented
FtnOrigNode : 'ftn_orig_node',
FtnDestNode : 'ftn_dest_node',
// :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping
FtnOrigNetwork : 'ftn_orig_network',
FtnDestNetwork : 'ftn_dest_network',
FtnAttrFlags : 'ftn_attr_flags',
FtnCost : 'ftn_cost',
FtnOrigZone : 'ftn_orig_zone',
FtnDestZone : 'ftn_dest_zone',
FtnOrigPoint : 'ftn_orig_point',
FtnDestPoint : 'ftn_dest_point',
// packet header oriented
FtnOrigNode : 'ftn_orig_node',
FtnDestNode : 'ftn_dest_node',
// :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping
FtnOrigNetwork : 'ftn_orig_network',
FtnDestNetwork : 'ftn_dest_network',
FtnAttrFlags : 'ftn_attr_flags',
FtnCost : 'ftn_cost',
FtnOrigZone : 'ftn_orig_zone',
FtnDestZone : 'ftn_dest_zone',
FtnOrigPoint : 'ftn_orig_point',
FtnDestPoint : 'ftn_dest_point',
// message header oriented
FtnMsgOrigNode : 'ftn_msg_orig_node',
FtnMsgDestNode : 'ftn_msg_dest_node',
FtnMsgOrigNet : 'ftn_msg_orig_net',
FtnMsgDestNet : 'ftn_msg_dest_net',
// message header oriented
FtnMsgOrigNode : 'ftn_msg_orig_node',
FtnMsgDestNode : 'ftn_msg_dest_node',
FtnMsgOrigNet : 'ftn_msg_orig_net',
FtnMsgDestNet : 'ftn_msg_dest_net',
FtnAttribute : 'ftn_attribute',
FtnAttribute : 'ftn_attribute',
FtnTearLine : 'ftn_tear_line', // 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
FtnSeenBy : 'ftn_seen_by', // 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
FtnArea : 'ftn_area', // 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 = {
reply_to_message_id : 'replyToMsgId',
modified_timestamp : 'modTimestamp'
reply_to_message_id : 'replyToMsgId',
modified_timestamp : 'modTimestamp'
};
module.exports = class Message {
@ -102,28 +102,28 @@ module.exports = class Message {
} = { }
)
{
this.messageId = messageId;
this.areaTag = areaTag;
this.uuid = uuid;
this.replyToMsgId = replyToMsgId;
this.toUserName = toUserName;
this.fromUserName = fromUserName;
this.subject = subject;
this.message = message;
this.messageId = messageId;
this.areaTag = areaTag;
this.uuid = uuid;
this.replyToMsgId = replyToMsgId;
this.toUserName = toUserName;
this.fromUserName = fromUserName;
this.subject = subject;
this.message = message;
if(_.isDate(modTimestamp) || _.isString(modTimestamp)) {
modTimestamp = moment(modTimestamp);
}
this.modTimestamp = modTimestamp;
this.modTimestamp = modTimestamp;
this.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) {
return areaTag.toLowerCase() === Message.WellKnownAreaTags.Private;
@ -187,10 +187,10 @@ module.exports = class Message {
modTimestamp = moment(modTimestamp);
}
areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437');
modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437');
subject = iconvEncode(subject.toUpperCase().trim(), 'CP437');
body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437');
areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437');
modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437');
subject = iconvEncode(subject.toUpperCase().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 ] )));
}
@ -198,7 +198,7 @@ module.exports = class Message {
static getMessageFromRow(row) {
const msg = {};
_.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);
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.ids - use with resultType='uuid'
filter.toUserName
filter.fromUserName
filter.replyToMesageId
filter.newerThanTimestamp
filter.newerThanMessageId
filter.areaTag - note if you want by conf, send in all areas for a conf
*filter.metaTuples - {category, name, value}
filter.uuids - use with resultType='id'
filter.ids - use with resultType='uuid'
filter.toUserName
filter.fromUserName
filter.replyToMesageId
filter.newerThanTimestamp
filter.newerThanMessageId
filter.areaTag - note if you want by conf, send in all areas for a conf
*filter.metaTuples - {category, name, value}
filter.terms - FTS search
filter.terms - FTS search
filter.sort = modTimestamp | messageId
filter.order = ascending | (descending)
filter.sort = modTimestamp | messageId
filter.order = ascending | (descending)
filter.limit
filter.resultType = (id) | uuid | count
filter.extraFields = []
filter.limit
filter.resultType = (id) | uuid | count
filter.extraFields = []
filter.privateTagUserId = <userId> - if set, only private messages belonging to <userId> are processed
- any other areaTag or confTag filters will be ignored
- if NOT present, private areas are skipped
filter.privateTagUserId = <userId> - if set, only private messages belonging to <userId> are processed
- any other areaTag or confTag filters will be ignored
- if NOT present, private areas are skipped
*=NYI
*/
*=NYI
*/
static findMessages(filter, cb) {
filter = filter || {};
filter.resultType = filter.resultType || 'id';
filter.extraFields = filter.extraFields || [];
filter.resultType = filter.resultType || 'id';
filter.extraFields = filter.extraFields || [];
if('messageList' === filter.resultType) {
filter.extraFields = _.uniq(filter.extraFields.concat(
@ -254,13 +254,13 @@ module.exports = class Message {
let sql;
if('count' === filter.resultType) {
sql =
`SELECT COUNT() AS count
FROM message m`;
`SELECT COUNT() AS count
FROM message m`;
} else {
sql =
`SELECT DISTINCT m.${field}${filter.extraFields.length > 0 ? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ') : ''}
FROM message m`;
`SELECT DISTINCT m.${field}${filter.extraFields.length > 0 ? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ') : ''}
FROM message m`;
}
const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC';
@ -276,7 +276,7 @@ module.exports = class Message {
sqlWhere += clause;
}
// currently only avail sort
// currently only avail sort
if('modTimestamp' === filter.sort) {
sqlOrderBy = `ORDER BY m.modified_timestamp ${sqlOrderDir}`;
} else {
@ -297,10 +297,10 @@ module.exports = class Message {
appendWhereClause(`m.area_tag = "${Message.WellKnownAreaTags.Private}"`);
appendWhereClause(
`m.message_id IN (
SELECT message_id
FROM message_meta
WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId}
)`);
SELECT message_id
FROM message_meta
WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId}
)`);
} else {
if(filter.areaTag && filter.areaTag.length > 0) {
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}"`);
}
@ -338,13 +338,13 @@ module.exports = class Message {
}
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(
`m.message_id IN (
SELECT rowid
FROM message_fts
WHERE message_fts MATCH ":${sanatizeString(filter.terms)}"
)`
SELECT rowid
FROM message_fts
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) {
msgDb.get(
`SELECT message_id
FROM message
WHERE message_uuid = ?
LIMIT 1;`,
FROM message
WHERE message_uuid = ?
LIMIT 1;`,
[ uuid ],
(err, row) => {
if(err) {
@ -398,27 +398,27 @@ module.exports = class Message {
);
}
// :TODO: use findMessages
// :TODO: use findMessages
static getMessageIdsByMetaValue(category, name, value, cb) {
msgDb.all(
`SELECT message_id
FROM message_meta
WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`,
FROM message_meta
WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`,
[ category, name, value ],
(err, rows) => {
if(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) {
const sql =
`SELECT meta_value
FROM message_meta
WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`;
`SELECT meta_value
FROM message_meta
WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`;
msgDb.all(sql, [ messageId, category, name ], (err, rows) => {
if(err) {
@ -429,12 +429,12 @@ module.exports = class Message {
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) {
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) {
/*
Example of loaded this.meta:
Example of loaded this.meta:
meta: {
System: {
local_to_user_id: 1234,
},
FtnProperty: {
ftn_seen_by: [ "1/102 103", "2/42 52 65" ]
}
}
*/
meta: {
System: {
local_to_user_id: 1234,
},
FtnProperty: {
ftn_seen_by: [ "1/102 103", "2/42 52 65" ]
}
}
*/
const sql =
`SELECT meta_category, meta_name, meta_value
FROM message_meta
WHERE message_id = ?;`;
`SELECT meta_category, meta_name, meta_value
FROM message_meta
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) => {
if(!(row.meta_category in self.meta)) {
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) {
assert(_.isString(options.uuid));
@ -508,10 +508,10 @@ module.exports = class Message {
function loadMessage(callback) {
msgDb.get(
`SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject,
message, modified_timestamp, view_count
FROM message
WHERE message_uuid=?
LIMIT 1;`,
message, modified_timestamp, view_count
FROM message
WHERE message_uuid=?
LIMIT 1;`,
[ options.uuid ],
(err, msgRow) => {
if(err) {
@ -522,15 +522,15 @@ module.exports = class Message {
return callback(Errors.DoesNotExist('Message (no longer) available'));
}
self.messageId = msgRow.message_id;
self.areaTag = msgRow.area_tag;
self.messageUuid = msgRow.message_uuid;
self.replyToMsgId = msgRow.reply_to_message_id;
self.toUserName = msgRow.to_user_name;
self.fromUserName = msgRow.from_user_name;
self.subject = msgRow.subject;
self.message = msgRow.message;
self.modTimestamp = moment(msgRow.modified_timestamp);
self.messageId = msgRow.message_id;
self.areaTag = msgRow.area_tag;
self.messageUuid = msgRow.message_uuid;
self.replyToMsgId = msgRow.reply_to_message_id;
self.toUserName = msgRow.to_user_name;
self.fromUserName = msgRow.from_user_name;
self.subject = msgRow.subject;
self.message = msgRow.message;
self.modTimestamp = moment(msgRow.modified_timestamp);
return callback(err);
}
@ -542,7 +542,7 @@ module.exports = class Message {
});
},
function loadHashTags(callback) {
// :TODO:
// :TODO:
return callback(null);
}
],
@ -560,7 +560,7 @@ module.exports = class Message {
const metaStmt = transOrDb.prepare(
`INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value)
VALUES (?, ?, ?, ?);`);
VALUES (?, ?, ?, ?);`);
if(!_.isArray(value)) {
value = [ value ];
@ -590,7 +590,7 @@ module.exports = class Message {
return msgDb.beginTransaction(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();
if(!self.uuid) {
self.uuid = Message.createMessageUUID(
@ -603,9 +603,9 @@ module.exports = class Message {
trans.run(
`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) ],
function inserted(err) { // use non-arrow function for 'this' scope
function inserted(err) { // use non-arrow function for 'this' scope
if(!err) {
self.messageId = this.lastID;
}
@ -619,17 +619,17 @@ module.exports = class Message {
return callback(null, trans);
}
/*
Example of self.meta:
Example of self.meta:
meta: {
System: {
local_to_user_id: 1234,
},
FtnProperty: {
ftn_seen_by: [ "1/102 103", "2/42 52 65" ]
}
}
*/
meta: {
System: {
local_to_user_id: 1234,
},
FtnProperty: {
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]), (name, nextName) => {
self.persistMetaValue(category, name, self.meta[category][name], trans, err => {
@ -644,7 +644,7 @@ module.exports = class Message {
});
},
function storeHashTags(trans, callback) {
// :TODO: hash tag support
// :TODO: hash tag support
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) {
source = source || 'fromUserName';
@ -677,32 +677,32 @@ module.exports = class Message {
return cb(Errors.MissingParam());
}
options.startCol = options.startCol || 1;
options.includePrefix = _.get(options, 'includePrefix', 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.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting
options.startCol = options.startCol || 1;
options.includePrefix = _.get(options, 'includePrefix', 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.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
doing so, don't ya think? yeah I think so
Some long text that needs to be wrapped and quoted should look right after
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> after 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> 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> right 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> right after doing so, don't ya think? yeah I think so
*/
*/
const quotePrefix = options.includePrefix ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') : '';
function getWrapped(text, extraPrefix) {
extraPrefix = extraPrefix ? ` ${extraPrefix}` : '';
const wrapOpts = {
width : options.cols - (quotePrefix.length + extraPrefix.length),
tabHandling : 'expand',
tabWidth : 4,
width : options.cols - (quotePrefix.length + extraPrefix.length),
tabHandling : 'expand',
tabWidth : 4,
};
return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => {
@ -711,7 +711,7 @@ module.exports = class Message {
}
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;
const total = line.length + quotePrefix.length;
@ -726,14 +726,14 @@ module.exports = class Message {
if(options.isAnsi) {
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,
termHeight : options.termHeight,
cols : options.cols,
rows : 'auto',
startCol : options.startCol,
forceLineTerm : true,
termWidth : options.termWidth,
termHeight : options.termHeight,
cols : options.cols,
rows : 'auto',
startCol : options.startCol,
forceLineTerm : true,
},
(err, prepped) => {
prepped = prepped || this.message;
@ -741,20 +741,20 @@ module.exports = class Message {
let lastSgr = '';
const split = splitTextAtTerms(prepped);
const quoteLines = [];
const focusQuoteLines = [];
const quoteLines = [];
const focusQuoteLines = [];
//
// 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
// strip colors, colorize the lines, etc. If we exclude the prefixes, this seems to do
// the trick and allow them to leave them alone!
// 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
// strip colors, colorize the lines, etc. If we exclude the prefixes, this seems to do
// the trick and allow them to leave them alone!
//
split.forEach(l => {
quoteLines.push(`${lastSgr}${l}`);
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;
@ -763,23 +763,23 @@ module.exports = class Message {
}
);
} else {
const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */;
const quoted = [];
const input = _.trimEnd(this.message).replace(/\b/g, '');
const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */;
const quoted = [];
const input = _.trimEnd(this.message).replace(/\b/g, '');
// find *last* tearline
// find *last* tearline
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 => {
//
// For each paragraph, a state machine:
// - New line - line
// - New (pre)quoted line - quote_line
// - Continuation of new/quoted line
// For each paragraph, a state machine:
// - New line - line
// - New (pre)quoted line - quote_line
// - Continuation of new/quoted line
//
// Also:
// - Detect pre-formatted lines & try to keep them as-is
// Also:
// - Detect pre-formatted lines & try to keep them as-is
//
let state;
let buf = '';
@ -787,18 +787,18 @@ module.exports = class Message {
if(quoted.length > 0) {
//
// Preserve paragraph seperation.
// Preserve paragraph seperation.
//
// FSC-0032 states something about leaving blank lines fully blank
// (without a prefix) but it seems nicer (and more consistent with other systems)
// to put 'em in.
// FSC-0032 states something about leaving blank lines fully blank
// (without a prefix) but it seems nicer (and more consistent with other systems)
// to put 'em in.
//
quoted.push(quotePrefix);
}
paragraph.split(/\r?\n/).forEach(line => {
if(0 === line.trim().length) {
// see blank line notes above
// see blank line notes above
return quoted.push(quotePrefix);
}
@ -839,8 +839,8 @@ module.exports = class Message {
if(isFormattedLine(line)) {
quoted.push(getFormattedLine(line));
} else {
state = quoteMatch ? 'quote_line' : 'line';
buf = 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any
state = quoteMatch ? 'quote_line' : 'line';
buf = 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any
}
break;
}

View File

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

View File

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

View File

@ -1,26 +1,26 @@
/* jslint node: true */
'use strict';
// deps
const _ = require('lodash');
// deps
const _ = require('lodash');
const mimeTypes = require('mime-types');
const mimeTypes = require('mime-types');
exports.startup = startup;
exports.resolveMimeType = resolveMimeType;
exports.startup = startup;
exports.resolveMimeType = resolveMimeType;
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 = {
ans : 'text/x-ansi',
gz : 'application/gzip', // not in mime-types 2.1.15 :(
lzx : 'application/x-lzx', // :TODO: submit to mime-types
ans : 'text/x-ansi',
gz : 'application/gzip', // not in mime-types 2.1.15 :(
lzx : 'application/x-lzx', // :TODO: submit to mime-types
};
_.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => {
// don't override any entries
// don't override any entries
if(!_.isString(mimeTypes.types[ext])) {
mimeTypes[ext] = mimeType;
}
@ -35,8 +35,8 @@ function startup(cb) {
function resolveMimeType(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 */
'use strict';
const paths = require('path');
const paths = require('path');
const os = require('os');
const packageJson = require('../package.json');
const os = require('os');
const packageJson = require('../package.json');
exports.isProduction = isProduction;
exports.isDevelopment = isDevelopment;
exports.valueWithDefault = valueWithDefault;
exports.resolvePath = resolvePath;
exports.getCleanEnigmaVersion = getCleanEnigmaVersion;
exports.getEnigmaUserAgent = getEnigmaUserAgent;
exports.isProduction = isProduction;
exports.isDevelopment = isDevelopment;
exports.valueWithDefault = valueWithDefault;
exports.resolvePath = resolvePath;
exports.getCleanEnigmaVersion = getCleanEnigmaVersion;
exports.getEnigmaUserAgent = getEnigmaUserAgent;
function isProduction() {
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() {
// 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 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})`;
}

View File

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

View File

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

View File

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

View File

@ -1,16 +1,16 @@
/* jslint node: true */
'use strict';
const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
const persistMessage = require('./message_area.js').persistMessage;
const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
const persistMessage = require('./message_area.js').persistMessage;
const _ = require('lodash');
const async = require('async');
const _ = require('lodash');
const async = require('async');
exports.moduleInfo = {
name : 'Message Area Post',
desc : 'Module for posting a new message to an area',
author : 'NuSkooler',
name : 'Message Area Post',
desc : 'Module for posting a new message to an area',
author : 'NuSkooler',
};
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
@ -19,7 +19,7 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
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.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) {
@ -42,9 +42,9 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
],
function complete(err) {
if(err) {
// :TODO:... sooooo now what?
// :TODO:... sooooo now what?
} 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(
{ to : msg.toUserName, subject : msg.subject, uuid : msg.uuid },
'Message persisted'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +1,24 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule;
const User = require('./user.js');
const theme = require('./theme.js');
const login = require('./system_menu_method.js').login;
const Config = require('./config.js').get;
const messageArea = require('./message_area.js');
// ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule;
const User = require('./user.js');
const theme = require('./theme.js');
const login = require('./system_menu_method.js').login;
const Config = require('./config.js').get;
const messageArea = require('./message_area.js');
exports.moduleInfo = {
name : 'NUA',
desc : 'New User Application',
name : 'NUA',
desc : 'New User Application',
};
const MciViewIds = {
userName : 1,
password : 9,
confirm : 10,
errMsg : 11,
userName : 1,
password : 9,
confirm : 10,
errMsg : 11,
};
exports.getModule = class NewUserAppModule extends MenuModule {
@ -30,7 +30,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
this.menuMethods = {
//
// Validation stuff
// Validation stuff
//
validatePassConfirmMatch : function(data, cb) {
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) {
const newUser = new User();
@ -67,33 +67,33 @@ exports.getModule = class NewUserAppModule extends MenuModule {
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 areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck
let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck
let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck
// can't store undefined!
confTag = confTag || '';
areaTag = areaTag || '';
newUser.properties = {
real_name : formData.value.realName,
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format
sex : formData.value.sex,
location : formData.value.location,
affiliation : formData.value.affils,
email_address : formData.value.email,
web_address : formData.value.web,
account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format
real_name : formData.value.realName,
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format
sex : formData.value.sex,
location : formData.value.location,
affiliation : formData.value.affils,
email_address : formData.value.email,
web_address : formData.value.web,
account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format
message_conf_tag : confTag,
message_area_tag : areaTag,
term_height : self.client.term.termHeight,
term_width : self.client.term.termWidth,
term_height : self.client.term.termHeight,
term_width : self.client.term.termWidth,
// :TODO: Other defaults
// :TODO: should probably have a place to create defaults/etc.
// :TODO: Other defaults
// :TODO: should probably have a place to create defaults/etc.
};
if('*' === config.defaults.theme) {
@ -102,7 +102,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
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 => {
if(err) {
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 {
self.client.log.info( { username : formData.value.username, userId : newUser.userId }, 'New user created');
// Cache SysOp information now
// :TODO: Similar to bbs.js. DRY
// Cache SysOp information now
// :TODO: Similar to bbs.js. DRY
if(newUser.isSysOp()) {
config.general.sysOp = {
username : formData.value.username,
properties : newUser.properties,
username : formData.value.username,
properties : newUser.properties,
};
}
@ -129,7 +129,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
return self.gotoMenu(extraArgs.inactive, cb);
} 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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,25 +1,25 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const Config = require('../../config.js').get;
const TelnetClient = require('./telnet.js').TelnetClient;
const Log = require('../../logger.js').log;
const LoginServerModule = require('../../login_server_module.js');
// ENiGMA½
const Config = require('../../config.js').get;
const TelnetClient = require('./telnet.js').TelnetClient;
const Log = require('../../logger.js').log;
const LoginServerModule = require('../../login_server_module.js');
// deps
const _ = require('lodash');
const WebSocketServer = require('ws').Server;
const http = require('http');
const https = require('https');
const fs = require('graceful-fs');
const Writable = require('stream');
// deps
const _ = require('lodash');
const WebSocketServer = require('ws').Server;
const http = require('http');
const https = require('https');
const fs = require('graceful-fs');
const Writable = require('stream');
const ModuleInfo = exports.moduleInfo = {
name : 'WebSocket',
desc : 'WebSocket Server',
author : 'NuSkooler',
packageName : 'codes.l33t.enigma.websocket.server',
name : 'WebSocket',
desc : 'WebSocket Server',
author : 'NuSkooler',
packageName : 'codes.l33t.enigma.websocket.server',
};
function WebSocketClient(ws, req, serverType) {
@ -35,8 +35,8 @@ function WebSocketClient(ws, req, serverType) {
};
//
// This bridge makes accessible various calls that client sub classes
// want to access on I/O socket
// This bridge makes accessible various calls that client sub classes
// want to access on I/O socket
//
this.socketBridge = new class SocketBridge extends Writable {
constructor(ws) {
@ -49,12 +49,12 @@ function WebSocketClient(ws, req, serverType) {
}
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);
}
// we need to fake some streaming work
// we need to fake some streaming work
unpipe() {
Log.trace('WebSocket SocketBridge unpipe()');
}
@ -64,7 +64,7 @@ function WebSocketClient(ws, req, serverType) {
}
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;
}
}(ws);
@ -72,12 +72,12 @@ function WebSocketClient(ws, req, serverType) {
ws.on('message', this.dataHandler);
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');
});
//
// Montior connection status with ping/pong
// Montior connection status with ping/pong
//
ws.on('pong', () => {
Log.trace(`Pong from ${this.socketBridge.remoteAddress}`);
@ -89,11 +89,11 @@ function WebSocketClient(ws, req, serverType) {
Log.trace( { headers : req.headers }, 'WebSocket connection headers' );
//
// If the config allows it, look for 'x-forwarded-proto' as "https"
// to override |isSecure|
// If the config allows it, look for 'x-forwarded-proto' as "https"
// to override |isSecure|
//
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']}"`);
this.proxied = true;
@ -101,7 +101,7 @@ function WebSocketClient(ws, req, serverType) {
this.proxied = false;
}
// start handshake process
// start handshake process
this.banner();
}
@ -116,40 +116,40 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule {
createServer() {
//
// We will actually create up to two servers:
// * insecure websocket (ws://)
// * secure (tls) websocket (wss://)
// We will actually create up to two servers:
// * insecure websocket (ws://)
// * secure (tls) websocket (wss://)
//
const config = _.get(Config(), 'loginServers.webSocket');
if(!_.isObject(config)) {
return;
}
const wsPort = _.get(config, 'ws.port');
const wssPort = _.get(config, 'wss.port');
const wsPort = _.get(config, 'ws.port');
const wssPort = _.get(config, 'wss.port');
if(true === _.get(config, 'ws.enabled') && _.isNumber(wsPort)) {
const httpServer = http.createServer( (req, resp) => {
// dummy handler
// dummy handler
resp.writeHead(200);
return resp.end('ENiGMA½ BBS WebSocket Server!');
});
this.insecure = {
httpServer : httpServer,
wsServer : new WebSocketServer( { server : httpServer } ),
httpServer : httpServer,
wsServer : new WebSocketServer( { server : httpServer } ),
};
}
if(_.isObject(config, 'wss') && true === _.get(config, 'wss.enabled') && _.isNumber(wssPort)) {
const httpServer = https.createServer({
key : fs.readFileSync(config.wss.keyPem),
cert : fs.readFileSync(config.wss.certPem),
key : fs.readFileSync(config.wss.keyPem),
cert : fs.readFileSync(config.wss.certPem),
});
this.secure = {
httpServer : httpServer,
wsServer : new WebSocketServer( { server : httpServer } ),
httpServer : httpServer,
wsServer : new WebSocketServer( { server : httpServer } ),
};
}
}
@ -161,8 +161,8 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule {
return;
}
const serverName = `${ModuleInfo.name} (${serverType})`;
const port = parseInt(_.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] ));
const serverName = `${ModuleInfo.name} (${serverType})`;
const port = parseInt(_.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] ));
if(isNaN(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( () => {
WSS_SERVER_TYPES.forEach(serverType => {
@ -191,10 +191,10 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule {
return ws.terminate();
}
ws.isConnectionAlive = false; // pong will reset this
ws.isConnectionAlive = false; // pong will reset this
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