Pardon the noise. More tab to space conversion!
This commit is contained in:
parent
c3635bb26b
commit
1d8be6b014
|
@ -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();
|
||||
|
|
30
core/acs.js
30
core/acs.js
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
172
core/art.js
172
core/art.js
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
110
core/bbs_link.js
110
core/bbs_link.js
|
@ -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) {
|
||||
|
|
162
core/bbs_list.js
162
core/bbs_list.js
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
*/
|
||||
|
||||
|
|
414
core/client.js
414
core/client.js
|
@ -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' :
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
824
core/config.js
824
core/config.js
File diff suppressed because it is too large
Load Diff
|
@ -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) {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 ];
|
||||
|
|
350
core/database.js
350
core/database.js
|
@ -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);
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
);
|
||||
|
||||
|
|
72
core/door.js
72
core/door.js
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
246
core/dropfile.js
246
core/dropfile.js
|
@ -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');
|
||||
};
|
||||
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
160
core/exodus.js
160
core/exodus.js
|
@ -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,
|
||||
});
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
418
core/fse.js
418
core/fse.js
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
392
core/ftn_util.js
392
core/ftn_util.js
|
@ -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];
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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; }
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
};
|
|
@ -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() } ;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
440
core/message.js
440
core/message.js
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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})`;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
],
|
||||
|
|
124
core/msg_list.js
124
core/msg_list.js
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
};
|
||||
|
|
102
core/new_scan.js
102
core/new_scan.js
|
@ -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);
|
||||
|
|
72
core/nua.js
72
core/nua.js
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
exports.PluginModule = PluginModule;
|
||||
exports.PluginModule = PluginModule;
|
||||
|
||||
function PluginModule(/*options*/) {
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
150
core/sauce.js
150
core/sauce.js
|
@ -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
|
@ -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);
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue